Files
tripz-llc/transit_dashboard/src/views/FleetView.vue
T
2026-08-09 16:56:13 +03:00

208 lines
8.8 KiB
Vue

<script setup>
import { ref, onMounted, reactive } from 'vue'
import * as transitApi from '../api/transit'
import { ApiError } from '../api/client'
const tab = ref('vehicles') // vehicles | drivers
const loading = ref(true)
const error = ref('')
const vehicles = ref([])
const drivers = ref([])
const vForm = reactive({ plate: '', make: '', model: '', year: '', color: '', capacity: 30, vehicle_type: 'bus' })
const dForm = reactive({ name: '', phone: '', license_number: '' })
const showVForm = ref(false)
const showDForm = ref(false)
const savingV = ref(false)
const savingD = ref(false)
const inviteResult = ref(null)
async function loadAll() {
loading.value = true
try {
const [v, d] = await Promise.all([transitApi.listVehicles(), transitApi.listDrivers('all')])
vehicles.value = v.vehicles
drivers.value = d.drivers
error.value = ''
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'تعذّر تحميل الأسطول'
} finally {
loading.value = false
}
}
async function submitVehicle() {
savingV.value = true
try {
await transitApi.addVehicle(vForm)
Object.assign(vForm, { plate: '', make: '', model: '', year: '', color: '', capacity: 30, vehicle_type: 'bus' })
showVForm.value = false
await loadAll()
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'تعذّر إضافة المركبة'
} finally {
savingV.value = false
}
}
async function toggleVehicle(v) {
try {
await transitApi.toggleVehicle(v.id, !v.is_active)
await loadAll()
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'تعذّر التحديث'
}
}
async function submitDriver() {
savingD.value = true
try {
const res = await transitApi.inviteDriver(dForm)
inviteResult.value = { name: dForm.name, ...res }
Object.assign(dForm, { name: '', phone: '', license_number: '' })
showDForm.value = false
await loadAll()
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'تعذّر دعوة السائق'
} finally {
savingD.value = false
}
}
async function toggleDriver(d) {
const action = d.status === 'suspended' ? 'activate' : 'suspend'
try {
await transitApi.suspendDriver(d.id, action)
await loadAll()
} catch (e) {
error.value = e instanceof ApiError ? e.message : 'تعذّر التحديث'
}
}
const driverStatusMeta = {
invited: { label: 'بانتظار التفعيل', cls: 'pill-warn' },
active: { label: 'نشط', cls: 'pill-good' },
suspended: { label: 'معلَّق', cls: 'pill-bad' },
}
onMounted(loadAll)
</script>
<template>
<div class="view">
<div class="head">
<h2>الأسطول</h2>
<div class="tabs">
<button class="tab" :class="{ active: tab === 'vehicles' }" @click="tab = 'vehicles'">المركبات</button>
<button class="tab" :class="{ active: tab === 'drivers' }" @click="tab = 'drivers'">السائقون</button>
</div>
</div>
<p v-if="error" class="error-banner">{{ error }}</p>
<!-- ── المركبات ── -->
<template v-if="tab === 'vehicles'">
<div class="toolbar">
<button class="btn btn-primary btn-sm" @click="showVForm = !showVForm">+ إضافة مركبة</button>
</div>
<form v-if="showVForm" class="inline-form card card-pad" @submit.prevent="submitVehicle">
<div class="grid-2">
<div class="field"><label>رقم اللوحة</label><input v-model="vForm.plate" class="input" required /></div>
<div class="field"><label>النوع</label>
<select v-model="vForm.vehicle_type" class="input">
<option value="bus">باص</option><option value="minibus">ميني باص</option>
<option value="van">فان</option><option value="other">أخرى</option>
</select>
</div>
<div class="field"><label>الصانع</label><input v-model="vForm.make" class="input" /></div>
<div class="field"><label>الموديل</label><input v-model="vForm.model" class="input" /></div>
<div class="field"><label>سنة الصنع</label><input v-model="vForm.year" type="number" class="input num" /></div>
<div class="field"><label>السعة</label><input v-model="vForm.capacity" type="number" class="input num" /></div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="savingV">{{ savingV ? 'جارٍ الحفظ...' : 'حفظ' }}</button>
</div>
</form>
<div class="card">
<div class="table-wrap">
<table class="tbl">
<thead><tr><th>اللوحة</th><th>الطراز</th><th>السعة</th><th>النوع</th><th>الحالة</th><th></th></tr></thead>
<tbody>
<tr v-for="v in vehicles" :key="v.id">
<td class="num">{{ v.plate }}</td>
<td>{{ [v.make, v.model, v.year].filter(Boolean).join(' ') || '—' }}</td>
<td class="num">{{ v.capacity }}</td>
<td>{{ v.vehicle_type }}</td>
<td><span class="pill" :class="v.is_active ? 'pill-good' : 'pill-neutral'">{{ v.is_active ? 'فعّالة' : 'معطّلة' }}</span></td>
<td><button class="btn btn-ghost btn-sm" @click="toggleVehicle(v)">{{ v.is_active ? 'تعطيل' : 'تفعيل' }}</button></td>
</tr>
<tr v-if="!vehicles.length"><td colspan="6" class="empty">لا توجد مركبات بعد</td></tr>
</tbody>
</table>
</div>
</div>
</template>
<!-- ── السائقون ── -->
<template v-else>
<div class="toolbar">
<button class="btn btn-primary btn-sm" @click="showDForm = !showDForm">+ دعوة سائق</button>
</div>
<div v-if="inviteResult" class="invite-note card card-pad">
تم إرسال دعوة إلى <strong>{{ inviteResult.name }}</strong> عبر واتساب — سيفعّل حسابه من تطبيق السائق.
</div>
<form v-if="showDForm" class="inline-form card card-pad" @submit.prevent="submitDriver">
<div class="grid-2">
<div class="field"><label>الاسم</label><input v-model="dForm.name" class="input" required /></div>
<div class="field"><label>الهاتف</label><input v-model="dForm.phone" class="input num" required /></div>
<div class="field"><label>رقم الرخصة</label><input v-model="dForm.license_number" class="input" /></div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" :disabled="savingD">{{ savingD ? 'جارٍ الإرسال...' : 'إرسال الدعوة' }}</button>
</div>
</form>
<div class="card">
<div class="table-wrap">
<table class="tbl">
<thead><tr><th>الاسم</th><th>الهاتف</th><th>الحالة</th><th></th></tr></thead>
<tbody>
<tr v-for="d in drivers" :key="d.id">
<td>{{ d.name }}</td>
<td class="num">{{ d.phone }}</td>
<td><span class="pill" :class="driverStatusMeta[d.status]?.cls">{{ driverStatusMeta[d.status]?.label }}</span></td>
<td>
<button v-if="d.status !== 'invited'" class="btn btn-ghost btn-sm" @click="toggleDriver(d)">
{{ d.status === 'suspended' ? 'إعادة تفعيل' : 'تعليق' }}
</button>
</td>
</tr>
<tr v-if="!drivers.length"><td colspan="4" class="empty">لا يوجد سائقون بعد</td></tr>
</tbody>
</table>
</div>
</div>
</template>
</div>
</template>
<style scoped>
.view { display: flex; flex-direction: column; gap: 18px; }
.head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
.tabs { display: flex; gap: 4px; background: var(--surface-2); padding: 4px; border-radius: var(--r-sm); }
.tab { font-family: inherit; border: none; background: transparent; padding: 8px 16px; border-radius: 6px; font-size: 13.5px; font-weight: 600; color: var(--ink-soft); cursor: pointer; }
.tab.active { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-sm); }
.toolbar { display: flex; justify-content: flex-end; }
.grid-2 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
@media (max-width: 700px) { .grid-2 { grid-template-columns: 1fr; } }
.form-actions { margin-top: 16px; display: flex; justify-content: flex-end; }
.empty { text-align: center; color: var(--ink-faint); padding: 32px !important; }
.invite-note { background: var(--brand-soft); color: var(--brand-deep); font-size: 14px; }
.error-banner { background: var(--bad-soft); color: var(--bad); padding: 12px 16px; border-radius: var(--r-sm); font-size: 14px; }
</style>