chore: استيراد أولي من سيرو (ecfe7568) — بلا أي تعديل

نسخة كاملة من مستودع سيرو عند ecfe7568 لتكون أساس تطبيق «انطلق».
نُسخ المتعقَّب في git فقط (12,509 ملفاً / 302 م.ب) بـ git archive، لا
`cp -r` — فاستُثنيت تلقائياً مخلفات البناء (build · node_modules ·
.dart_tool · .gradle · Pods ≈ 10.7 غ.ب) وكل ما يستثنيه .gitignore.

هذا الكوميت **بلا أي تعديل عمداً** حتى يكون كل ما يليه فرقاً مقروءاً
مقابل سيرو الأصلي. سيرو نفسه لم يُمسّ.

⚠️ لا يبني بعد: `.env` و`lib/env/env.g.dart` غير متعقَّبين في سيرو (وهذا
صحيح — أسرار لكل مستأجر). كل تطبيق فلاتر هنا يحتاج .env خاصاً بانطلق ثم
توليد env.g.dart عبر build_runner. لا تُنسخ أسرار سيرو.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:10:29 +03:00
co-authored by Claude Opus 5
commit 92dc6b3641
3654 changed files with 896478 additions and 0 deletions
@@ -0,0 +1,103 @@
<script setup>
import { computed } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const auth = useAuthStore()
const router = useRouter()
const navItems = [
{ to: '/dashboard', label: 'اليوم', icon: 'M4 4h7v7H4zM13 4h7v7h-7zM4 13h7v7H4zM13 13h7v7h-7z' },
{ to: '/fleet', label: 'الأسطول', icon: 'M3 13l1.5-5A2 2 0 0 1 6.4 6.5h11.2A2 2 0 0 1 19.5 8L21 13v6a1 1 0 0 1-1 1h-1a1 1 0 0 1-1-1v-1H6v1a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z' },
{ to: '/routes', label: 'الخطوط', icon: 'M9 20l-5-2V5l5 2m0 13l6-2m-6 2V7m6 11l5 2V6l-5-2m0 15V5m0 2L9 5' },
{ to: '/students', label: 'الطلاب', icon: 'M12 3l9 4.5-9 4.5-9-4.5L12 3zM3 12l9 4.5 9-4.5M3 16.5l9 4.5 9-4.5' },
{ to: '/broadcasts', label: 'الإعلانات', icon: 'M11 5L6 9H2v6h4l5 4V5zM19 8a5 5 0 0 1 0 8' },
]
const contractLabel = computed(() => ({
trial: 'تجريبي', active: 'نشط', suspended: 'معلَّق', terminated: 'منتهٍ',
}[auth.org?.contract_status] || auth.org?.contract_status))
const contractClass = computed(() => ({
trial: 'pill-info', active: 'pill-good', suspended: 'pill-warn', terminated: 'pill-bad',
}[auth.org?.contract_status] || 'pill-neutral'))
async function doLogout() {
await auth.logout()
router.replace('/login')
}
</script>
<template>
<div class="shell">
<aside class="sidebar">
<div class="brand">
<span class="brand-dot"></span>
<span>مواصلاتي</span>
</div>
<nav class="nav">
<router-link v-for="item in navItems" :key="item.to" :to="item.to" class="nav-item" active-class="active">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
<path :d="item.icon" />
</svg>
<span>{{ item.label }}</span>
</router-link>
</nav>
</aside>
<div class="main">
<header class="topbar">
<div class="org-info">
<strong>{{ auth.org?.name_ar }}</strong>
<span class="pill" :class="contractClass">{{ contractLabel }}</span>
</div>
<div class="account">
<span class="admin-name">{{ auth.admin?.name }}</span>
<button class="btn btn-ghost btn-sm" @click="doLogout">خروج</button>
</div>
</header>
<main class="content">
<router-view />
</main>
</div>
</div>
</template>
<style scoped>
.shell { display: flex; min-height: 100vh; }
.sidebar {
width: 232px; flex-shrink: 0; background: var(--surface); border-inline-start: 1px solid var(--line);
display: flex; flex-direction: column; padding: 20px 14px;
}
.brand {
display: flex; align-items: center; gap: 9px; font-weight: 800; font-size: 15.5px;
color: var(--brand-deep); padding: 8px 10px 24px;
}
.brand-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--brand); }
.nav { display: flex; flex-direction: column; gap: 3px; }
.nav-item {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: var(--r-sm);
color: var(--ink-soft); text-decoration: none; font-size: 14.5px; font-weight: 600;
transition: background .15s ease, color .15s ease;
}
.nav-item svg { width: 19px; height: 19px; flex-shrink: 0; }
.nav-item:hover { background: var(--surface-2); color: var(--ink); }
.nav-item.active { background: var(--brand-soft); color: var(--brand-deep); }
.main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
.topbar {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 28px; border-bottom: 1px solid var(--line); background: var(--surface);
}
.org-info { display: flex; align-items: center; gap: 12px; font-size: 15px; }
.account { display: flex; align-items: center; gap: 14px; }
.admin-name { color: var(--ink-soft); font-size: 13.5px; }
.content { flex: 1; padding: 28px; max-width: 1180px; width: 100%; margin: 0 auto; }
@media (max-width: 860px) {
.sidebar { position: fixed; inset-inline-start: 0; top: 0; bottom: 0; z-index: 20; transform: translateX(100%); }
.content { padding: 18px; }
}
</style>
@@ -0,0 +1,41 @@
<script setup>
defineProps({
open: Boolean,
title: String,
body: String,
confirmLabel: { type: String, default: 'تأكيد' },
danger: { type: Boolean, default: false },
})
const emit = defineEmits(['confirm', 'cancel'])
</script>
<template>
<Teleport to="body">
<div v-if="open" class="overlay" @click.self="emit('cancel')">
<div class="dialog card">
<div class="dialog-body">
<h3>{{ title }}</h3>
<p>{{ body }}</p>
</div>
<div class="dialog-actions">
<button class="btn btn-ghost" @click="emit('cancel')">إلغاء</button>
<button :class="danger ? 'btn btn-danger' : 'btn btn-primary'" @click="emit('confirm')">
{{ confirmLabel }}
</button>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
.overlay {
position: fixed; inset: 0; background: rgba(10,14,20,.45); backdrop-filter: blur(2px);
display: flex; align-items: center; justify-content: center; z-index: 100; padding: 20px;
}
.dialog { width: 100%; max-width: 380px; box-shadow: var(--shadow-lg); }
.dialog-body { padding: 22px 22px 6px; }
.dialog-body h3 { font-size: 17px; margin-bottom: 8px; }
.dialog-body p { color: var(--ink-soft); font-size: 14px; margin: 0; }
.dialog-actions { display: flex; justify-content: flex-end; gap: 10px; padding: 16px 22px 22px; }
</style>
@@ -0,0 +1,46 @@
<script setup>
// قناع الأيام: bit0=أحد … bit6=سبت (62 = الأحد–الخميس)
const props = defineProps({ modelValue: { type: Number, default: 62 } })
const emit = defineEmits(['update:modelValue'])
const days = [
{ bit: 0, label: 'أحد' },
{ bit: 1, label: 'اثنين' },
{ bit: 2, label: 'ثلاثاء' },
{ bit: 3, label: 'أربعاء' },
{ bit: 4, label: 'خميس' },
{ bit: 5, label: 'جمعة' },
{ bit: 6, label: 'سبت' },
]
function isOn(bit) {
return ((props.modelValue >> bit) & 1) === 1
}
function toggle(bit) {
const next = isOn(bit) ? props.modelValue & ~(1 << bit) : props.modelValue | (1 << bit)
emit('update:modelValue', next)
}
</script>
<template>
<div class="day-mask">
<button
v-for="d in days" :key="d.bit" type="button"
class="day-chip" :class="{ on: isOn(d.bit) }"
@click="toggle(d.bit)"
>
{{ d.label }}
</button>
</div>
</template>
<style scoped>
.day-mask { display: flex; gap: 6px; flex-wrap: wrap; }
.day-chip {
font-family: inherit; font-size: 13px; font-weight: 600; padding: 7px 13px;
border-radius: 100px; border: 1px solid var(--line-strong); background: var(--surface);
color: var(--ink-soft); cursor: pointer; transition: all .15s ease;
}
.day-chip.on { background: var(--brand); border-color: var(--brand); color: white; }
.day-chip:hover:not(.on) { background: var(--surface-2); }
</style>
@@ -0,0 +1,110 @@
<script setup>
import { ref, onMounted, onBeforeUnmount, watch } from 'vue'
import L from 'leaflet'
import { haversineMeters } from '../utils/polyline'
const props = defineProps({
stops: { type: Array, required: true }, // [{lat,lng,name_ar,name_en,radius,eta_offset_min,is_major}]
center: { type: Array, default: () => [31.9539, 35.9106] }, // عمّان افتراضياً
})
const emit = defineEmits(['update:stops'])
const mapEl = ref(null)
let map = null
let markers = []
let polyline = null
function iconFor(i, isMajor) {
return L.divIcon({
className: 'stop-marker',
html: `<div class="stop-pin ${isMajor ? 'major' : ''}">${i + 1}</div>`,
iconSize: [28, 28],
iconAnchor: [14, 14],
})
}
function redraw() {
markers.forEach((m) => m.remove())
markers = []
if (polyline) { polyline.remove(); polyline = null }
props.stops.forEach((s, i) => {
const marker = L.marker([s.lat, s.lng], { icon: iconFor(i, s.is_major), draggable: true })
.addTo(map)
.bindTooltip(s.name_ar || `محطة ${i + 1}`)
marker.on('dragend', () => {
const pos = marker.getLatLng()
const next = [...props.stops]
next[i] = { ...next[i], lat: pos.lat, lng: pos.lng }
emit('update:stops', next)
})
markers.push(marker)
})
if (props.stops.length > 1) {
const latlngs = props.stops.map((s) => [s.lat, s.lng])
polyline = L.polyline(latlngs, { color: '#0E7C86', weight: 4, opacity: 0.85 }).addTo(map)
}
}
function onMapClick(e) {
const next = [...props.stops, {
lat: Number(e.latlng.lat.toFixed(7)),
lng: Number(e.latlng.lng.toFixed(7)),
name_ar: `محطة ${props.stops.length + 1}`,
name_en: '',
radius: 150,
eta_offset_min: null,
is_major: 0,
}]
emit('update:stops', next)
}
onMounted(() => {
map = L.map(mapEl.value).setView(props.center, 13)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; OpenStreetMap',
maxZoom: 19,
}).addTo(map)
map.on('click', onMapClick)
redraw()
})
onBeforeUnmount(() => { map?.remove() })
watch(() => props.stops, redraw, { deep: true })
defineExpose({
totalDistanceKm: () => {
let d = 0
for (let i = 1; i < props.stops.length; i++) {
d += haversineMeters(props.stops[i - 1].lat, props.stops[i - 1].lng, props.stops[i].lat, props.stops[i].lng)
}
return Math.round((d / 1000) * 10) / 10
},
routePoints: () => props.stops.map((s) => [s.lat, s.lng]),
})
</script>
<template>
<div class="map-builder">
<div ref="mapEl" class="map-canvas"></div>
<p class="hint">انقر على الخريطة لإضافة محطة بالترتيب — اسحب أي علامة لتعديل موقعها</p>
</div>
</template>
<style scoped>
.map-builder { display: flex; flex-direction: column; gap: 8px; }
.map-canvas { height: 420px; border-radius: var(--r-md); overflow: hidden; border: 1px solid var(--line); }
.hint { font-size: 12.5px; color: var(--ink-faint); margin: 0; }
</style>
<style>
/* غير scoped — leaflet ينشئ العناصر خارج شجرة Vue */
.stop-pin {
width: 26px; height: 26px; border-radius: 50%; background: var(--brand, #0E7C86);
color: white; display: flex; align-items: center; justify-content: center;
font-size: 12px; font-weight: 800; border: 2px solid white;
box-shadow: 0 2px 6px rgba(0,0,0,.3); font-family: "IBM Plex Mono", monospace;
}
.stop-pin.major { background: #C9821D; width: 30px; height: 30px; font-size: 13px; }
</style>
@@ -0,0 +1,23 @@
<script setup>
defineProps({
label: String,
value: [String, Number],
tone: { type: String, default: 'neutral' }, // neutral | good | warn | bad
})
</script>
<template>
<div class="stat card card-pad" :class="`tone-${tone}`">
<div class="value num">{{ value }}</div>
<div class="label">{{ label }}</div>
</div>
</template>
<style scoped>
.stat { display: flex; flex-direction: column; gap: 6px; }
.value { font-size: 26px; font-weight: 800; letter-spacing: -.01em; }
.label { font-size: 13px; color: var(--ink-faint); }
.tone-good .value { color: var(--good); }
.tone-warn .value { color: var(--warn); }
.tone-bad .value { color: var(--bad); }
</style>