136 lines
5.1 KiB
Vue
136 lines
5.1 KiB
Vue
<script setup>
|
|
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
|
import StatCard from '../components/StatCard.vue'
|
|
import ConfirmDialog from '../components/ConfirmDialog.vue'
|
|
import * as transitApi from '../api/transit'
|
|
import { ApiError } from '../api/client'
|
|
|
|
const loading = ref(true)
|
|
const error = ref('')
|
|
const data = ref(null)
|
|
let poll = null
|
|
|
|
const statusMeta = {
|
|
scheduled: { label: 'مجدولة', cls: 'pill-neutral' },
|
|
started: { label: 'منطلقة', cls: 'pill-good' },
|
|
completed: { label: 'مكتملة', cls: 'pill-info' },
|
|
cancelled: { label: 'ملغاة', cls: 'pill-bad' },
|
|
no_show: { label: 'لم يحضر', cls: 'pill-warn' },
|
|
}
|
|
|
|
async function load() {
|
|
try {
|
|
data.value = await transitApi.getDashboard()
|
|
error.value = ''
|
|
} catch (e) {
|
|
error.value = e instanceof ApiError ? e.message : 'تعذّر تحميل البيانات'
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const cancelTarget = ref(null)
|
|
async function confirmCancel() {
|
|
try {
|
|
await transitApi.cancelTrip(cancelTarget.value.id, 'أُلغيت من لوحة المؤسسة')
|
|
cancelTarget.value = null
|
|
load()
|
|
} catch (e) {
|
|
error.value = e instanceof ApiError ? e.message : 'تعذّر إلغاء الرحلة'
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
load()
|
|
poll = setInterval(load, 15000) // تحديث كل 15 ثانية كما في المواصفة
|
|
})
|
|
onBeforeUnmount(() => clearInterval(poll))
|
|
</script>
|
|
|
|
<template>
|
|
<div class="view">
|
|
<div class="head">
|
|
<h2>اليوم</h2>
|
|
<span class="date num" v-if="data">{{ data.date }}</span>
|
|
</div>
|
|
|
|
<p v-if="error" class="error-banner">{{ error }}</p>
|
|
|
|
<template v-if="data">
|
|
<div class="stats-grid">
|
|
<StatCard label="رحلات اليوم" :value="data.stats.total_trips" />
|
|
<StatCard label="نشطة الآن" :value="data.stats.active_now" tone="good" />
|
|
<StatCard label="مكتملة" :value="data.stats.completed_today" />
|
|
<StatCard label="لم يحضر السائق" :value="data.stats.no_show_today" tone="warn" />
|
|
<StatCard label="أعضاء نشطون" :value="data.stats.active_members" />
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="table-wrap">
|
|
<table class="tbl">
|
|
<thead>
|
|
<tr>
|
|
<th>الخط</th><th>الانطلاق</th><th>السائق</th><th>المركبة</th>
|
|
<th>الحالة</th><th>التأخير</th><th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="t in data.trips" :key="t.id">
|
|
<td>{{ t.route_name }}</td>
|
|
<td class="num">{{ t.departure_time }}</td>
|
|
<td>{{ t.driver_name }}</td>
|
|
<td class="num">{{ t.vehicle_plate || '—' }}</td>
|
|
<td><span class="pill" :class="statusMeta[t.status]?.cls">{{ statusMeta[t.status]?.label || t.status }}</span></td>
|
|
<td class="num">{{ t.delay_minutes > 0 ? `+${t.delay_minutes} د` : '—' }}</td>
|
|
<td>
|
|
<button
|
|
v-if="['scheduled','started'].includes(t.status)"
|
|
class="btn btn-danger btn-sm"
|
|
@click="cancelTarget = t"
|
|
>إلغاء</button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="!data.trips.length"><td colspan="7" class="empty">لا توجد رحلات اليوم</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card card-pad" v-if="data.recent_broadcasts.length">
|
|
<h3 class="section-title">آخر الإعلانات</h3>
|
|
<ul class="bc-list">
|
|
<li v-for="b in data.recent_broadcasts" :key="b.id">
|
|
<strong>{{ b.title_ar || 'إعلان' }}</strong>
|
|
<span class="bc-body">{{ b.body_ar }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</template>
|
|
|
|
<ConfirmDialog
|
|
:open="!!cancelTarget"
|
|
title="إلغاء الرحلة"
|
|
:body="`سيتم إلغاء رحلة خط ${cancelTarget?.route_name} وإشعار المشتركين.`"
|
|
confirm-label="إلغاء الرحلة"
|
|
danger
|
|
@confirm="confirmCancel"
|
|
@cancel="cancelTarget = null"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.view { display: flex; flex-direction: column; gap: 20px; }
|
|
.head { display: flex; align-items: baseline; gap: 12px; }
|
|
.date { color: var(--ink-faint); font-size: 14px; }
|
|
.stats-grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 14px; }
|
|
@media (max-width: 900px) { .stats-grid { grid-template-columns: repeat(2, 1fr); } }
|
|
.empty { text-align: center; color: var(--ink-faint); padding: 32px !important; }
|
|
.section-title { font-size: 15px; margin-bottom: 14px; }
|
|
.bc-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 12px; }
|
|
.bc-list li { display: flex; flex-direction: column; gap: 3px; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
|
|
.bc-list li:last-child { border-bottom: none; padding-bottom: 0; }
|
|
.bc-body { color: var(--ink-soft); font-size: 13.5px; }
|
|
.error-banner { background: var(--bad-soft); color: var(--bad); padding: 12px 16px; border-radius: var(--r-sm); font-size: 14px; }
|
|
</style>
|