147 lines
7.2 KiB
Vue
147 lines
7.2 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<!-- RBAC guard: hide content without fleet:view -->
|
|
<template v-if="hasOrgPermission('fleet:view')">
|
|
<div class="flex items-center justify-between">
|
|
<div class="flex items-center gap-3">
|
|
<div class="p-2 rounded-lg bg-indigo-500/10 text-indigo-400">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0" />
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-base font-semibold text-white">{{ $t('garages.fleet.title') }}</h3>
|
|
<p class="text-xs text-slate-400 mt-0.5">{{ $t('garages.fleet.total_vehicles', { count: fleetVehicles.length }) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-slate-800/50 border border-slate-700 rounded-xl overflow-hidden">
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead>
|
|
<tr class="border-b border-slate-700">
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.plate') }}</th>
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.brand_model') }}</th>
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.year') }}</th>
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.vin') }}</th>
|
|
<th class="text-left px-4 py-3 text-xs font-medium text-slate-400 uppercase tracking-wider">{{ $t('garages.fleet.status') }}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="v in fleetVehicles" :key="v.id" class="border-b border-slate-700/50 last:border-b-0 hover:bg-slate-700/30 transition">
|
|
<td class="px-4 py-3">
|
|
<span class="text-sm font-bold text-white">{{ v.license_plate || '—' }}</span>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span class="text-sm text-slate-300">{{ v.brand || '—' }}</span>
|
|
<span v-if="v.brand && v.model" class="text-slate-500 mx-1">·</span>
|
|
<span v-if="v.model" class="text-sm text-slate-300">{{ v.model }}</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-sm text-slate-300">{{ v.year || '—' }}</td>
|
|
<td class="px-4 py-3">
|
|
<span class="text-xs font-mono text-slate-400">{{ v.vin || '—' }}</span>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<span
|
|
class="inline-flex items-center gap-1.5 px-2.5 py-0.5 rounded-full text-xs font-medium"
|
|
:class="fleetStatusBadgeClass(v.status)"
|
|
>
|
|
<span class="w-1.5 h-1.5 rounded-full" :class="fleetStatusDotClass(v.status)"></span>
|
|
{{ fleetStatusLabel(v.status) }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="fleetVehicles.length === 0 && !fleetLoading">
|
|
<td colspan="5" class="px-4 py-12 text-center">
|
|
<svg class="w-12 h-12 text-slate-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 17a2 2 0 11-4 0 2 2 0 014 0zM19 17a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M13 16V6a1 1 0 00-1-1H4a1 1 0 00-1 1v10a1 1 0 001 1h1m8-1a1 1 0 01-1 1H9m4-1V8a1 1 0 011-1h2.586a1 1 0 01.707.293l3.414 3.414a1 1 0 01.293.707V16a1 1 0 01-1 1h-1m-6-1a1 1 0 001 1h1M5 17a2 2 0 104 0m-4 0a2 2 0 114 0m6 0a2 2 0 104 0m-4 0a2 2 0 114 0" />
|
|
</svg>
|
|
<p class="text-sm text-slate-400">{{ $t('garages.fleet.no_vehicles') }}</p>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="fleetLoading">
|
|
<td colspan="5" class="px-4 py-12 text-center">
|
|
<svg class="w-8 h-8 text-indigo-400 animate-spin mx-auto" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
</svg>
|
|
<p class="text-sm text-slate-400 mt-2">{{ $t('garages.details.loading') }}</p>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<!-- RBAC: No fleet access fallback -->
|
|
<template v-else>
|
|
<div class="bg-slate-800/50 border border-slate-700 rounded-xl p-12 text-center">
|
|
<svg class="w-12 h-12 text-slate-600 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
|
</svg>
|
|
<p class="text-sm text-slate-400">{{ $t('garages.fleet.no_fleet_access') }}</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { useAuthStore } from '~/stores/auth'
|
|
|
|
const props = defineProps<{
|
|
garage: any
|
|
orgId: string
|
|
fleetVehicles: any[]
|
|
fleetLoading: boolean
|
|
}>()
|
|
|
|
const auth = useAuthStore()
|
|
|
|
// RBAC helper
|
|
function hasOrgPermission(perm: string): boolean {
|
|
if (auth.isAdmin) return true
|
|
if (auth.user?.system_capabilities?.[perm]) return true
|
|
if (auth.user?.org_capabilities?.[props.orgId]?.[perm]) return true
|
|
return false
|
|
}
|
|
|
|
function fleetStatusLabel(status: string): string {
|
|
const map: Record<string, string> = {
|
|
active: 'Active',
|
|
inactive: 'Inactive',
|
|
sold: 'Sold',
|
|
scrapped: 'Scrapped',
|
|
pending: 'Pending',
|
|
draft: 'Draft',
|
|
}
|
|
return map[status?.toLowerCase()] || status || 'Unknown'
|
|
}
|
|
|
|
function fleetStatusBadgeClass(status: string): string {
|
|
const map: Record<string, string> = {
|
|
active: 'bg-green-500/10 text-green-400',
|
|
inactive: 'bg-gray-500/10 text-gray-400',
|
|
sold: 'bg-red-500/10 text-red-400',
|
|
scrapped: 'bg-red-500/10 text-red-400',
|
|
pending: 'bg-yellow-500/10 text-yellow-400',
|
|
draft: 'bg-gray-500/10 text-gray-400',
|
|
}
|
|
return map[status?.toLowerCase()] || 'bg-gray-500/10 text-gray-400'
|
|
}
|
|
|
|
function fleetStatusDotClass(status: string): string {
|
|
const map: Record<string, string> = {
|
|
active: 'bg-green-400',
|
|
inactive: 'bg-gray-400',
|
|
sold: 'bg-red-400',
|
|
scrapped: 'bg-red-400',
|
|
pending: 'bg-yellow-400',
|
|
draft: 'bg-gray-400',
|
|
}
|
|
return map[status?.toLowerCase()] || 'bg-gray-400'
|
|
}
|
|
</script>
|