enterprise_assest_managemen.../frontend/src/views/AssetDetail.vue

621 lines
20 KiB
Vue

<!-- frontend/src/views/AssetDetail.vue -->
<template>
<div class="asset-detail-page">
<!-- Page Header -->
<div class="page-header fluent-entrance mb-8">
<div class="d-flex align-center mb-4">
<v-btn
icon
variant="text"
color="neutral-primary"
class="mr-3"
@click="goBack"
>
<v-icon>mdi-arrow-left</v-icon>
</v-btn>
<div class="flex-grow-1">
<h1 class="text-title1 mb-1">{{ asset?.name || 'Loading...' }}</h1>
<p class="text-body1" style="color: #605E5C;">
Asset ID: {{ asset?.asset_identifier || '-' }}
</p>
</div>
<div class="d-flex gap-3">
<v-btn
v-if="canEditAssets && asset"
color="primary"
variant="outlined"
prepend-icon="mdi-pencil"
@click="editAsset"
>
Edit Asset
</v-btn>
<v-btn
v-if="asset"
variant="outlined"
prepend-icon="mdi-qrcode"
@click="showQRCode"
>
QR Code
</v-btn>
</div>
</div>
</div>
<!-- Loading State -->
<div v-if="isLoading" class="d-flex justify-center align-center" style="min-height: 400px;">
<v-progress-circular indeterminate size="64" />
</div>
<!-- Error State -->
<v-alert v-else-if="error" type="error" variant="tonal" class="mb-6">
<v-alert-title>Error Loading Asset</v-alert-title>
{{ error }}
<template #append>
<v-btn variant="text" @click="loadAsset">Retry</v-btn>
</template>
</v-alert>
<!-- Asset Details -->
<div v-else-if="asset" class="asset-content">
<v-row>
<!-- Main Content -->
<v-col cols="12" lg="8">
<!-- Basic Information Card -->
<v-card class="fluent-layer-1 mb-6" elevation="0">
<v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-information</v-icon>
Basic Information
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<div class="info-item mb-4">
<label class="info-label">Asset Name</label>
<div class="info-value">{{ asset.name }}</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Asset Identifier</label>
<div class="info-value">{{ asset.asset_identifier }}</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Category</label>
<div class="info-value d-flex align-center">
<v-chip
:color="asset.category_id?.color || '#9E9E9E'"
size="small"
class="mr-2"
>
{{ asset.category_id?.name || 'Unknown' }}
</v-chip>
</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Status</label>
<div class="info-value">
<v-chip
:color="getStatusColor(asset.status)"
size="small"
variant="tonal"
>
{{ formatStatus(asset.status) }}
</v-chip>
</div>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="info-item mb-4">
<label class="info-label">Location</label>
<div class="info-value">
<div>{{ asset.location_id?.name || 'Unknown' }}</div>
<div v-if="asset.location_id?.building" class="text-caption" style="color: #605E5C;">
{{ asset.location_id.building }}
<span v-if="asset.location_id.floor"> - {{ asset.location_id.floor }}</span>
</div>
</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Vendor</label>
<div class="info-value">{{ asset.vendor_id?.name || 'Not specified' }}</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Manufacturer</label>
<div class="info-value">{{ asset.manufacturer || 'Not specified' }}</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Model Number</label>
<div class="info-value">{{ asset.model_number || 'Not specified' }}</div>
</div>
</v-col>
</v-row>
<div v-if="asset.description" class="info-item">
<label class="info-label">Description</label>
<div class="info-value">{{ asset.description }}</div>
</div>
</v-card-text>
</v-card>
<!-- Financial Information Card -->
<v-card class="fluent-layer-1 mb-6" elevation="0">
<v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-currency-usd</v-icon>
Financial Information
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<div class="info-item mb-4">
<label class="info-label">Acquisition Cost</label>
<div class="info-value text-h6">
{{ formatCurrency(asset.acquisition_cost) }}
</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Net Book Value</label>
<div class="info-value">
{{ formatCurrency(asset.net_book_value) }}
</div>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="info-item mb-4">
<label class="info-label">Acquisition Date</label>
<div class="info-value">
{{ formatDate(asset.acquisition_date) }}
</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Depreciation Method</label>
<div class="info-value">
{{ formatDepreciationMethod(asset.depreciation_method) }}
</div>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- Technical Details Card -->
<v-card class="fluent-layer-1 mb-6" elevation="0">
<v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-cog</v-icon>
Technical Details
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="6">
<div class="info-item mb-4">
<label class="info-label">Serial Number</label>
<div class="info-value">{{ asset.serial_number || 'Not specified' }}</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Condition Rating</label>
<div class="info-value">
<v-chip
:color="getConditionColor(asset.condition_rating)"
size="small"
variant="tonal"
>
{{ formatCondition(asset.condition_rating) }}
</v-chip>
</div>
</div>
</v-col>
<v-col cols="12" md="6">
<div class="info-item mb-4">
<label class="info-label">Warranty Start</label>
<div class="info-value">
{{ formatDate(asset.warranty_start_date) }}
</div>
</div>
<div class="info-item mb-4">
<label class="info-label">Warranty Expiration</label>
<div class="info-value">
{{ formatDate(asset.warranty_expiration_date) }}
<v-chip
v-if="isWarrantyExpired(asset.warranty_expiration_date)"
color="error"
size="x-small"
class="ml-2"
>
Expired
</v-chip>
</div>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<!-- Sidebar -->
<v-col cols="12" lg="4">
<!-- Asset Image Card -->
<v-card class="fluent-layer-1 mb-6" elevation="0">
<v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-image</v-icon>
Asset Image
</v-card-title>
<v-card-text>
<div class="asset-image-container">
<v-img
v-if="getAssetImageUrl(asset)"
:src="getAssetImageUrl(asset)"
aspect-ratio="1"
cover
class="asset-image"
@click="showImageDialog = true"
/>
<div v-else class="no-image-placeholder">
<v-icon size="64" color="grey-lighten-1">mdi-image-off</v-icon>
<div class="text-caption mt-2" style="color: #605E5C;">
No image available
</div>
</div>
</div>
</v-card-text>
</v-card>
<!-- Quick Stats Card -->
<v-card class="fluent-layer-1 mb-6" elevation="0">
<v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-chart-line</v-icon>
Quick Stats
</v-card-title>
<v-card-text>
<div class="stat-item mb-3">
<div class="stat-label">Age</div>
<div class="stat-value">{{ calculateAge(asset.acquisition_date) }}</div>
</div>
<div class="stat-item mb-3">
<div class="stat-label">Depreciation Rate</div>
<div class="stat-value">{{ asset.depreciation_rate || 0 }}%</div>
</div>
<div class="stat-item mb-3">
<div class="stat-label">Expected Life</div>
<div class="stat-value">{{ asset.expected_useful_life || 0 }} months</div>
</div>
<div class="stat-item">
<div class="stat-label">Created</div>
<div class="stat-value">{{ formatDate(asset.date_created) }}</div>
</div>
</v-card-text>
</v-card>
<!-- QR Code Card -->
<v-card v-if="asset.asset_qr_codes && asset.asset_qr_codes.length > 0" class="fluent-layer-1" elevation="0">
<v-card-title class="d-flex align-center">
<v-icon color="primary" class="mr-2">mdi-qrcode</v-icon>
QR Code
</v-card-title>
<v-card-text class="text-center">
<v-btn
variant="outlined"
prepend-icon="mdi-qrcode"
@click="showQRCode"
block
>
View QR Code
</v-btn>
</v-card-text>
</v-card>
</v-col>
</v-row>
</div>
<!-- Image Dialog -->
<v-dialog v-model="showImageDialog" max-width="800">
<v-card>
<v-card-title class="d-flex align-center justify-space-between">
{{ asset?.name }} - Image
<v-btn icon @click="showImageDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
<v-card-text class="pa-0">
<v-img
v-if="getAssetImageUrl(asset)"
:src="getAssetImageUrl(asset, { quality: 90 })"
contain
max-height="600"
/>
</v-card-text>
</v-card>
</v-dialog>
</div>
</template>
<script>
import { ref, computed, onMounted, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useAssetsStore } from '../stores/assets';
import { useUIStore } from '../stores/ui';
import { fileUploadService } from '../services/fileUpload';
export default {
name: 'AssetDetail',
setup() {
const route = useRoute();
const router = useRouter();
const assetsStore = useAssetsStore();
const uiStore = useUIStore();
const isLoading = ref(true);
const error = ref(null);
const showImageDialog = ref(false);
const canEditAssets = ref(false);
const assetId = computed(() => route.params.id);
// Use asset from store (automatically reactive to optimistic updates)
const asset = computed(() => {
console.log('🔍 AssetDetail computed - current asset:', {
id: assetsStore.currentAsset?.id,
name: assetsStore.currentAsset?.name,
lastUpdated: assetsStore.currentAsset?.date_updated
});
return assetsStore.currentAsset;
});
// Load asset data
const loadAsset = async () => {
if (!assetId.value) {
error.value = 'Invalid asset ID';
isLoading.value = false;
return;
}
try {
isLoading.value = true;
error.value = null;
// Load permissions first
const { permissionsService } = await import('../services/permissions');
await permissionsService.initialize();
canEditAssets.value = permissionsService.canEditAssets();
// Fetch asset details (store will update asset computed automatically)
console.log('📡 Fetching asset data for ID:', assetId.value);
const assetData = await assetsStore.fetchAssetById(assetId.value);
console.log('✅ Asset data loaded:', {
name: assetData.name,
lastUpdated: assetData.date_updated,
imageUrl: assetData.image_url
});
uiStore.setPageTitle(`Asset: ${assetData.name}`);
} catch (err) {
console.error('Failed to load asset:', err);
error.value = err.message || 'Failed to load asset details';
} finally {
isLoading.value = false;
}
};
// Navigation functions
const goBack = () => {
router.push('/assets');
};
const editAsset = () => {
router.push(`/assets/${assetId.value}/edit`);
};
const showQRCode = () => {
// TODO: Implement QR code display
uiStore.showInfo('QR Code functionality coming soon!');
};
// Formatting functions
const formatCurrency = (amount) => {
if (!amount && amount !== 0) return 'Not specified';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(amount);
};
const formatDate = (date) => {
if (!date) return 'Not specified';
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
};
const formatStatus = (status) => {
return status ? status.charAt(0).toUpperCase() + status.slice(1) : 'Unknown';
};
const formatCondition = (condition) => {
return condition ? condition.charAt(0).toUpperCase() + condition.slice(1) : 'Unknown';
};
const formatDepreciationMethod = (method) => {
const methods = {
'straight_line': 'Straight Line',
'declining_balance': 'Declining Balance',
'sum_of_years': 'Sum of Years',
'units_of_production': 'Units of Production'
};
return methods[method] || method || 'Not specified';
};
const getStatusColor = (status) => {
const colors = {
'active': 'success',
'inactive': 'default',
'maintenance': 'warning',
'retired': 'error'
};
return colors[status] || 'default';
};
const getConditionColor = (condition) => {
const colors = {
'excellent': 'success',
'good': 'info',
'fair': 'warning',
'poor': 'error',
'critical': 'error'
};
return colors[condition] || 'default';
};
const getAssetImageUrl = (asset, params = {}) => {
if (!asset?.image_url) return null;
const fileId = typeof asset.image_url === 'object'
? asset.image_url.id
: asset.image_url;
return fileId ? fileUploadService.getImageUrl(fileId, {
width: 400,
height: 400,
fit: 'cover',
quality: 80,
...params
}) : null;
};
const calculateAge = (acquisitionDate) => {
if (!acquisitionDate) return 'Unknown';
const now = new Date();
const acquired = new Date(acquisitionDate);
const diffTime = Math.abs(now - acquired);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays < 30) {
return `${diffDays} days`;
} else if (diffDays < 365) {
return `${Math.floor(diffDays / 30)} months`;
} else {
return `${Math.floor(diffDays / 365)} years`;
}
};
const isWarrantyExpired = (warrantyDate) => {
if (!warrantyDate) return false;
return new Date(warrantyDate) < new Date();
};
// Watch for route changes (simplified - optimistic updates handle most cases)
watch(assetId, loadAsset);
onMounted(loadAsset);
return {
asset,
isLoading,
error,
showImageDialog,
canEditAssets,
goBack,
editAsset,
showQRCode,
loadAsset,
formatCurrency,
formatDate,
formatStatus,
formatCondition,
formatDepreciationMethod,
getStatusColor,
getConditionColor,
getAssetImageUrl,
calculateAge,
isWarrantyExpired
};
}
};
</script>
<style scoped>
.asset-detail-page {
padding: 24px;
animation: fluent-entrance 0.3s cubic-bezier(0.1, 0.9, 0.2, 1);
max-width: 1400px;
margin: 0 auto;
}
.fluent-layer-1 {
border: 1px solid #E1DFDD;
border-radius: 4px;
background: #FFFFFF;
transition: all 0.2s cubic-bezier(0.1, 0.9, 0.2, 1);
}
.info-item {
.info-label {
font-size: 12px;
font-weight: 600;
color: #605E5C;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 4px;
display: block;
}
.info-value {
font-size: 14px;
color: #323130;
line-height: 1.4;
}
}
.stat-item {
display: flex;
justify-content: space-between;
align-items: center;
.stat-label {
font-size: 14px;
color: #605E5C;
}
.stat-value {
font-size: 14px;
font-weight: 600;
color: #323130;
}
}
.asset-image-container {
position: relative;
border-radius: 4px;
overflow: hidden;
.asset-image {
cursor: pointer;
transition: transform 0.2s ease;
&:hover {
transform: scale(1.02);
}
}
.no-image-placeholder {
aspect-ratio: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #F8F9FA;
border: 2px dashed #E1DFDD;
border-radius: 4px;
}
}
@media (max-width: 768px) {
.asset-detail-page {
padding: 16px;
}
.page-header .d-flex {
flex-direction: column;
align-items: stretch;
gap: 16px;
}
}
</style>