208 lines
8.8 KiB
Vue
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>
|