/* ============================================================ MAISON ATLAS — admin dashboard app ============================================================ */ const svcLabel = (lang, id) => T(lang, 'svc_' + id + '_t'); const vehLabel = (lang, id) => id ? T(lang, 'veh_' + id + '_t') : T(lang, 'ad_unassigned'); const STATUSES = ['pending', 'confirmed', 'progress', 'done', 'refused']; const adminText = (lang, key) => ({ fr: { content: 'Contenu', services: 'Services', events: 'Evenements', news: 'Actualites', users: 'Utilisateurs', add: 'Ajouter', save: 'Sauvegarder', remove: 'Supprimer', duplicate: 'Dupliquer', edit: 'Modifier', cancel: 'Annuler', saved: 'Sauvegarde', invalid: 'JSON invalide', config: 'Coordonnees', whatsapp: 'WhatsApp', phone: 'Telephone', email: 'Email', address: 'Adresse', platformUsers: 'Utilisateurs plateforme', passwordHint: 'Mot de passe optionnel, jamais affiche apres sauvegarde.' }, en: { content: 'Content', services: 'Services', events: 'Events', news: 'News', users: 'Users', add: 'Add', save: 'Save', remove: 'Delete', duplicate: 'Duplicate', edit: 'Edit', cancel: 'Cancel', saved: 'Saved', invalid: 'Invalid JSON', config: 'Contact details', whatsapp: 'WhatsApp', phone: 'Phone', email: 'Email', address: 'Address', platformUsers: 'Platform users', passwordHint: 'Optional password, never shown after save.' }, ar: { content: 'المحتوى', services: 'الخدمات', events: 'الفعاليات', news: 'الأخبار', users: 'المستخدمون', add: 'إضافة', save: 'حفظ', remove: 'حذف', duplicate: 'نسخ', edit: 'تعديل', cancel: 'إلغاء', saved: 'تم الحفظ', invalid: 'JSON غير صالح', config: 'بيانات التواصل', whatsapp: 'واتساب', phone: 'الهاتف', email: 'البريد', address: 'العنوان', platformUsers: 'مستخدمو المنصة', passwordHint: 'كلمة المرور اختيارية ولا تظهر بعد الحفظ.' }, }[lang] || {})[key] || key; /* ---------------- Sidebar ---------------- */ function Sidebar({ lang, setLang, view, setView, counts, onClose }) { const items = [ { id: 'overview', ic: 'grid', label: 'ad_nav_overview' }, { id: 'requests', ic: 'list', label: 'ad_nav_requests', count: counts.pending }, { id: 'config', ic: 'phone', labelText: adminText(lang, 'config') }, { id: 'services', ic: 'grid', labelText: adminText(lang, 'services') }, { id: 'events', ic: 'cal', labelText: adminText(lang, 'events') }, { id: 'news', ic: 'chat', labelText: adminText(lang, 'news') }, { id: 'fleet', ic: 'car', label: 'ad_nav_fleet' }, { id: 'drivers', ic: 'users', label: 'ad_nav_drivers' }, { id: 'users', ic: 'user', labelText: adminText(lang, 'users') }, { id: 'content', ic: 'chat', labelText: adminText(lang, 'content') }, ]; return ( ); } /* ---------------- Stat cards ---------------- */ function StatCards({ lang, requests }) { const by = (s) => requests.filter(r => r.status === s).length; const revenue = requests.filter(r => r.quote && r.status !== 'refused').reduce((a, r) => a + r.quote, 0); const cards = [ { lbl: 'ad_stat_pending', val: by('pending'), color: 'var(--st-pending)' }, { lbl: 'ad_stat_confirmed', val: by('confirmed'), color: 'var(--st-confirmed)' }, { lbl: 'ad_stat_progress', val: by('progress'), color: 'var(--st-progress)' }, { lbl: 'ad_stat_done', val: by('done'), color: 'var(--st-done)' }, { lbl: 'ad_stat_revenue', val: window.ATLAS.fmtMoney(revenue, lang, 'MAD'), color: 'var(--accent)', big: true }, ]; return (
{cards.map((c, i) => (
{T(lang, c.lbl)}
{c.val}
))}
); } /* ---------------- Request list item ---------------- */ function ReqItem({ lang, r, active, onClick }) { return ( ); } /* ---------------- Requests view ---------------- */ function RequestsView({ lang, requests, selId, setSelId, search }) { const [filter, setFilter] = useState('all'); const filters = [{ id: 'all' }].concat(STATUSES.map(s => ({ id: s }))); let list = requests; if (search) { const q = search.toLowerCase(); list = list.filter(r => (r.name + r.id + r.pickup + r.dropoff).toLowerCase().includes(q)); } const filtered = filter === 'all' ? list : list.filter(r => r.status === filter); const sel = requests.find(r => r.id === selId); return (
{filters.map(f => { const n = f.id === 'all' ? list.length : list.filter(r => r.status === f.id).length; return ( ); })}
{filtered.length === 0 && (
{T(lang, 'ad_empty')}
{T(lang, 'ad_empty_sub')}
)} {filtered.map(r => ( setSelId(r.id)} /> ))}
{sel ? : (

{T(lang, 'ad_select_prompt')}

{T(lang, 'ad_select_sub')}

)}
); } /* ---------------- Request detail ---------------- */ function RequestDetail({ lang, r, requests }) { const [quoteDraft, setQuoteDraft] = useState(r.quote || ''); const [msg, setMsg] = useState(''); const [payBusy, setPayBusy] = useState(false); const threadRef = useRef(null); useEffect(() => { setQuoteDraft(r.quote || ''); }, [r.id]); useEffect(() => { if (r.unreadByAgency) window.ATLAS.updateRequest(r.id, { unreadByAgency: false }); }, [r.id]); useEffect(() => { if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight; }, [r.messages.length, r.id]); const upd = (patch) => window.ATLAS.updateRequest(r.id, patch); const busyVehicles = new Set(requests.filter(x => x.id !== r.id && ['confirmed', 'progress'].includes(x.status) && x.vehicle).map(x => x.vehicle)); const busyDrivers = new Set(requests.filter(x => x.id !== r.id && ['confirmed', 'progress'].includes(x.status) && x.driver).map(x => x.driver)); const sendQuote = () => { if (quoteDraft) upd({ quote: Number(quoteDraft) }); }; const payLabel = (key) => ({ fr: { title: 'Paiement', none: 'Aucun paiement encore.', paid: 'Paye', pending: 'En attente', failed: 'Echoue', cash: 'agence', card: 'carte', transfer: 'virement', markPaid: 'Marquer paye', markPending: 'Remettre en attente' }, en: { title: 'Payment', none: 'No payment yet.', paid: 'Paid', pending: 'Pending', failed: 'Failed', cash: 'agency', card: 'card', transfer: 'transfer', markPaid: 'Mark paid', markPending: 'Set pending' }, ar: { title: 'الدفع', none: 'لا يوجد دفع بعد.', paid: 'مدفوع', pending: 'في الانتظار', failed: 'فشل', cash: 'وكالة', card: 'بطاقة', transfer: 'تحويل', markPaid: 'تأكيد الدفع', markPending: 'إرجاع للانتظار' }, }[lang] || {})[key] || key; const setPayment = (status) => { setPayBusy(true); window.ATLAS.api.updatePayment(r.id, status) .then(() => setPayBusy(false)) .catch(() => setPayBusy(false)); }; const send = () => { if (!msg.trim()) return; window.ATLAS.addMessage(r.id, 'agency', msg.trim()); setMsg(''); }; return (
{r.id}
{r.name}
{svcLabel(lang, r.service)}
{T(lang, 'ad_trip')}
{r.pickup}

{r.dropoff}
{T(lang, 'ad_col_when')}
{window.ATLAS.fmtDate(r.date, lang)}
{r.time}
{T(lang, 'ad_passengers')}
{r.passengers}{r.billing ? · {T(lang, 'bill_label')}: {T(lang, 'bill_' + r.billing)} : null}
{T(lang, 'ad_client_info')}
{r.phone}
{r.email || '—'}
{r.notes ? (
Notes
{r.notes}
) : null}
{/* Actions */}

{T(lang, 'ad_actions')}

{/* Quote */}

{T(lang, 'ad_quote')}

{r.quote ? (
{T(lang, 'ad_quote_label')} {window.ATLAS.fmtMoney(r.quote, lang, 'MAD')}
) : null} {r.quoteAccepted ? (
{T(lang, 'q_accepted')}
signature
{T(lang, 'q_signed_by')} {r.signerName}
{T(lang, 'q_accepted_on')} {window.ATLAS.fmtDate(new Date(r.acceptedAt).toISOString().slice(0,10), lang)}
) : (
setQuoteDraft(e.target.value)} />
)}
{/* Payment */}

{payLabel('title')}

{r.payment ? (
{r.payment.id}
{payLabel(r.payment.method)} · {window.ATLAS.fmtMoney(r.payment.amount, lang, 'MAD')}
{payLabel(r.payment.status)}
) :
{payLabel('none')}
}
{/* Status */}

{T(lang, 'ad_set_status')}

{STATUSES.map(s => ( ))}
{/* Chat */}

{T(lang, 'ad_chat')}

{(r.messages || []).map((m, i) => (
{m.text}
{T(lang, 'sender_' + m.from)} · {window.ATLAS.timeAgo(m.at, lang)}
))}
setMsg(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') send(); }} />
); } /* ---------------- Overview ---------------- */ function Overview({ lang, requests, setView, setSelId }) { const recent = requests.slice(0, 6); const open = (id) => { setSelId(id); setView('requests'); }; return (

{T(lang, 'ad_requests_title')}

{recent.map(r => (
open(r.id)}>
{r.name} · {svcLabel(lang, r.service)}
{r.pickup} → {r.dropoff}
{window.ATLAS.fmtDate(r.date, lang)}
))}

{T(lang, 'ad_drivers_title')}

{window.ATLAS.drivers.slice(0, 4).map(d => (
{d.name[0]}
{d.name}
{d.zone} · {d.rating}
))}
); } /* ---------------- Fleet (admin) ---------------- */ function FleetAdmin({ lang, requests }) { const busy = new Set(requests.filter(r => ['confirmed', 'progress'].includes(r.status) && r.vehicle).map(r => r.vehicle)); const maint = new Set(['bus']); return (
{window.ATLAS.vehicles.map(v => { const state = maint.has(v.id) ? 'maint' : busy.has(v.id) ? 'busy' : 'available'; return (

{vehLabel(lang, v.id)}

{v.seats} {T(lang, 'fleet_seats')} · {window.ATLAS.fmtMoney(v.priceDay, lang, 'MAD')}{T(lang, 'fleet_perday')}
{T(lang, 'ad_' + state)} {v.cls}
); })}
); } /* ---------------- Drivers (admin) ---------------- */ function DriversAdmin({ lang }) { return (
{window.ATLAS.drivers.map(d => (
{d.name.split(' ').map(w => w[0]).slice(0, 2).join('')}

{d.name}

{d.zone}
{d.rating} {d.trips} {T(lang, 'ad_trips')}
{d.langs.map(l => ( {window.Flag ? : null}{l} ))}
))}
); } /* ---------------- Generic collection CRUD ---------------- */ function itemTitle(key, item, lang) { if (!item) return ''; if (item.name) return item.name; if (item.t) return window.ATLAS.pick(item.t, lang); if (item.id && key === 'vehicles') return vehLabel(lang, item.id); if (item.id && key === 'services') return svcLabel(lang, item.id); return item.id || 'item'; } function blankItem(key) { const n = Date.now().toString(36).slice(-5); if (key === 'services') return { id: 'service-' + n, icon: '✦', t: { fr: 'Nouveau service', en: 'New service', ar: 'خدمة جديدة' }, d: { fr: 'Description du service.', en: 'Service description.', ar: 'وصف الخدمة.' } }; if (key === 'vehicles') return { id: 'vehicle-' + n, seats: 4, bags: 4, cls: 'standard', priceDay: 900, priceKm: 9, t: { fr: 'Nouveau vehicule', en: 'New vehicle', ar: 'مركبة جديدة' }, d: { fr: 'Modele ou similaire', en: 'Model or similar', ar: 'نموذج أو مشابه' } }; if (key === 'drivers') return { id: 'd-' + n, name: 'Nouveau chauffeur', langs: ['FR', 'AR'], rating: 4.8, trips: 0, zone: 'Marrakech' }; if (key === 'events') return { id: 'ev-' + n, date: new Date().toISOString().slice(0, 10), city: 'Marrakech', img: 'ev-marathon', t: { fr: 'Nouvel evenement', en: 'New event', ar: 'فعالية جديدة' }, d: { fr: 'Description evenement.', en: 'Event description.', ar: 'وصف الفعالية.' } }; if (key === 'news') return { id: 'nw-' + n, date: new Date().toISOString().slice(0, 10), img: 'nw-app', tag: { fr: 'Info', en: 'Info', ar: 'خبر' }, t: { fr: 'Nouvelle actualite', en: 'New article', ar: 'خبر جديد' }, d: { fr: 'Description actualite.', en: 'Article description.', ar: 'وصف الخبر.' } }; return { id: 'item-' + n }; } function CollectionAdmin({ lang, contentKey }) { const [items, setItems] = useState(() => JSON.parse(JSON.stringify(window.ATLAS[contentKey] || []))); const [selected, setSelected] = useState(0); const [draft, setDraft] = useState(''); const [msg, setMsg] = useState(''); useEffect(() => { const next = JSON.parse(JSON.stringify(window.ATLAS[contentKey] || [])); setItems(next); setSelected(0); setDraft(JSON.stringify(next[0] || blankItem(contentKey), null, 2)); setMsg(''); }, [contentKey]); useEffect(() => { setDraft(JSON.stringify(items[selected] || blankItem(contentKey), null, 2)); }, [selected]); const persist = (next) => { setItems(next); window.ATLAS.api.saveContent(contentKey, next) .then(() => { window.ATLAS[contentKey] = next; setMsg(adminText(lang, 'saved')); }) .catch(() => setMsg('Erreur sauvegarde')); }; const saveOne = () => { let value; try { value = JSON.parse(draft); } catch (e) { setMsg(adminText(lang, 'invalid')); return; } const next = items.slice(); next[selected] = value; persist(next); }; const add = () => { const next = items.concat([blankItem(contentKey)]); setItems(next); setSelected(next.length - 1); setDraft(JSON.stringify(next[next.length - 1], null, 2)); persist(next); }; const duplicate = () => { const copy = JSON.parse(JSON.stringify(items[selected] || blankItem(contentKey))); copy.id = (copy.id || contentKey) + '-copy-' + Date.now().toString(36).slice(-4); const next = items.concat([copy]); setItems(next); setSelected(next.length - 1); setDraft(JSON.stringify(copy, null, 2)); persist(next); }; const remove = () => { const next = items.filter((_, i) => i !== selected); const nextIndex = Math.max(0, selected - 1); setItems(next); setSelected(nextIndex); setDraft(JSON.stringify(next[nextIndex] || blankItem(contentKey), null, 2)); persist(next); }; return (

{adminText(lang, contentKey)}

{items.map((item, i) => ( ))}

{itemTitle(contentKey, items[selected], lang)}

{contentKey} · JSON structure