Files
service-finder/frontend_admin/components/garages/GarageFleetTab.vue
2026-06-29 14:11:15 +00:00

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>