/* ============================================================
GoToMorocco — accounts, tracking, signature, WhatsApp
(loaded after shared.jsx — relies on its React hook bindings)
============================================================ */
/* ---------------- auth hook ---------------- */
function useAuth() {
const [user, setUser] = useState(window.ATLAS.currentUser());
useEffect(() => window.ATLAS.subscribeSession(() => setUser(window.ATLAS.currentUser())), []);
return user;
}
/* ---------------- WhatsApp floating widget ---------------- */
function WhatsAppWidget({ lang }) {
return (
);
}
/* ---------------- Modal base ---------------- */
function Modal({ children, onClose, wide, lang }) {
useEffect(() => {
const h = (e) => { if (e.key === 'Escape') onClose(); };
window.addEventListener('keydown', h);
document.body.style.overflow = 'hidden';
return () => { window.removeEventListener('keydown', h); document.body.style.overflow = ''; };
}, []);
return (
e.stopPropagation()}>
{children}
);
}
/* ---------------- Auth modal (login / signup) ---------------- */
function AuthModal({ lang, mode, prefill, onClose, onAuthed }) {
const [tab, setTab] = useState(mode || 'login');
const [f, setF] = useState({ name: prefill?.name || '', email: prefill?.email || '', phone: prefill?.phone || '', password: '' });
const [err, setErr] = useState('');
const set = (k, v) => setF(s => Object.assign({}, s, { [k]: v }));
const submit = () => {
setErr('');
if (tab === 'login') {
const res = window.ATLAS.login(f.email, f.password);
if (res.error) return setErr(T(lang, 'ac_bad_login'));
onAuthed && onAuthed(res.user); onClose();
} else {
if (!f.name || !f.email || !f.password) return setErr(T(lang, 'ac_fields'));
const res = window.ATLAS.signup(f);
if (res.error === 'exists') return setErr(T(lang, 'ac_exists'));
if (res.error) return setErr(T(lang, 'ac_fields'));
onAuthed && onAuthed(res.user); onClose();
}
};
return (
{T(lang, tab === 'login' ? 'ac_login_title' : 'ac_signup_title')}
{tab === 'signup' && (
set('name', e.target.value)} autoFocus />
)}
set('email', e.target.value)} />
{tab === 'signup' && (
set('phone', e.target.value)} />
)}
set('password', e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') submit(); }} />
{err &&
{err}
}
{tab === 'login' &&
{T(lang, 'ac_demo')}
}
);
}
/* ---------------- Signature pad ---------------- */
function SignaturePad({ lang, defaultName, onConfirm, onCancel }) {
const canvasRef = useRef(null);
const [name, setName] = useState(defaultName || '');
const [drawn, setDrawn] = useState(false);
const [err, setErr] = useState('');
const drawing = useRef(false);
const last = useRef(null);
useEffect(() => {
const c = canvasRef.current;
const dpr = window.devicePixelRatio || 1;
const rect = c.getBoundingClientRect();
c.width = rect.width * dpr; c.height = rect.height * dpr;
const ctx = c.getContext('2d');
ctx.scale(dpr, dpr);
ctx.strokeStyle = '#f0e6d2'; ctx.lineWidth = 2.4; ctx.lineCap = 'round'; ctx.lineJoin = 'round';
}, []);
const pos = (e) => {
const c = canvasRef.current; const rect = c.getBoundingClientRect();
const p = e.touches ? e.touches[0] : e;
return { x: p.clientX - rect.left, y: p.clientY - rect.top };
};
const start = (e) => { e.preventDefault(); drawing.current = true; last.current = pos(e); };
const move = (e) => {
if (!drawing.current) return; e.preventDefault();
const ctx = canvasRef.current.getContext('2d'); const p = pos(e);
ctx.beginPath(); ctx.moveTo(last.current.x, last.current.y); ctx.lineTo(p.x, p.y); ctx.stroke();
last.current = p; setDrawn(true);
};
const end = () => { drawing.current = false; };
const clear = () => {
const c = canvasRef.current; const ctx = c.getContext('2d');
ctx.clearRect(0, 0, c.width, c.height); setDrawn(false);
};
const confirm = () => {
if (!drawn || !name.trim()) { setErr(T(lang, 'sig_empty')); return; }
onConfirm(canvasRef.current.toDataURL('image/png'), name.trim());
};
return (
{T(lang, 'sig_title')}
{T(lang, 'sig_hint')}
setName(e.target.value)} />
{err && {err}
}
);
}
/* ---------------- Status timeline ---------------- */
function StatusTimeline({ lang, status }) {
if (status === 'refused') {
return {T(lang, 'st_refused')}
;
}
const steps = ['pending', 'confirmed', 'progress', 'done'];
const idx = steps.indexOf(status);
return (
{steps.map((s, i) => (
{i <= idx ? : i + 1}
{T(lang, 'st_' + s)}
))}
);
}
/* ---------------- Client quote box (accept + sign) ---------------- */
function ClientQuoteBox({ lang, r, user, onChanged }) {
const [signing, setSigning] = useState(false);
const [paying, setPaying] = useState(false);
const [method, setMethod] = useState('card');
const [err, setErr] = useState('');
const payLabel = (key) => ({
fr: { pay: 'Payer maintenant', paid: 'Paiement confirme', pending: 'Paiement en attente', choose: 'Mode de paiement', card: 'Carte bancaire', cash: 'Paiement agence', transfer: 'Virement', confirm: 'Confirmer le paiement', ref: 'Reference paiement', hint: 'Paiement local de demonstration. Aucune donnee carte n\u2019est stockee.' },
en: { pay: 'Pay now', paid: 'Payment confirmed', pending: 'Payment pending', choose: 'Payment method', card: 'Bank card', cash: 'Agency payment', transfer: 'Bank transfer', confirm: 'Confirm payment', ref: 'Payment reference', hint: 'Local demo payment. No card data is stored.' },
ar: { pay: 'ادفع الآن', paid: 'تم تأكيد الدفع', pending: 'الدفع في الانتظار', choose: 'طريقة الدفع', card: 'بطاقة بنكية', cash: 'الدفع في الوكالة', transfer: 'تحويل بنكي', confirm: 'تأكيد الدفع', ref: 'مرجع الدفع', hint: 'دفع محلي تجريبي. لا يتم تخزين بيانات البطاقة.' },
}[lang] || {})[key] || key;
if (!r.quote) {
return {T(lang, 'q_awaiting')}
;
}
const accept = (sig, name) => {
window.ATLAS.acceptQuote(r.id, sig, name);
setSigning(false); onChanged && onChanged();
};
const pay = () => {
setErr(''); setPaying(true);
window.ATLAS.api.createPayment(r.id, { method, amount: r.quote, payerName: user?.name || r.name })
.then(() => { setPaying(false); onChanged && onChanged(); })
.catch(() => { setPaying(false); setErr(lang === 'ar' ? 'تعذر الدفع' : 'Paiement impossible'); });
};
return (
{T(lang, 'q_proposed')}
{window.ATLAS.fmtMoney(r.quote, lang)}
{r.quoteAccepted ? (
{T(lang, 'q_accepted')}
{r.payment ?
{r.payment.status === 'paid' ? payLabel('paid') : payLabel('pending')}
: null}
{T(lang, 'q_signed_by')} {r.signerName}
{T(lang, 'q_accepted_on')} {new Date(r.acceptedAt).toLocaleDateString(lang === 'ar' ? 'ar-MA' : lang === 'en' ? 'en-GB' : 'fr-FR')}
{r.payment ? (
{payLabel('ref')}
{r.payment.id}
{r.payment.method} · {window.ATLAS.fmtMoney(r.payment.amount, lang)}
) : (
{['card', 'cash', 'transfer'].map(m => (
))}
{payLabel('hint')}
{err &&
{err}
}
)}
) : (
)}
{signing &&
setSigning(false)} />}
);
}
/* ---------------- Client chat ---------------- */
function ClientChat({ lang, r, onChanged }) {
const [msg, setMsg] = useState('');
const threadRef = useRef(null);
useEffect(() => { if (threadRef.current) threadRef.current.scrollTop = threadRef.current.scrollHeight; }, [r.messages.length]);
const send = () => {
if (!msg.trim()) return;
window.ATLAS.addMessage(r.id, 'client', msg.trim());
setMsg(''); onChanged && onChanged();
};
return (
{(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(); }} />
);
}
/* ---------------- Client request view (shared by track + account) ---------------- */
function ClientRequestView({ lang, r, user, onChanged }) {
const drv = window.ATLAS.drivers.find(d => d.id === r.driver);
return (
{r.id}
{T(lang, 'svc_' + r.service + '_t')}
{T(lang, 'ad_trip')}
{r.pickup}
↓
{r.dropoff}
{T(lang, 'ad_col_when')}
{window.ATLAS.fmtDate(r.date, lang)}
{r.time}
{T(lang, 'ad_col_vehicle')}
{r.vehicle ? T(lang, 'veh_' + r.vehicle + '_t') : T(lang, 'ad_unassigned')}
{T(lang, 'ad_assign_driver')}
{drv ? drv.name : T(lang, 'ad_unassigned')}
{T(lang, 'ad_quote')}
{T(lang, 'ad_chat')}
);
}
/* ---------------- Morocco map picker ---------------- */
const MOROCCO_PATH = "M40 7 L52 8 L64 12 L76 16 L80 30 L74 46 L70 60 L63 70 L52 80 L40 92 L26 104 L14 119 L8 127 L12 116 L17 96 L20 78 L17 62 L21 50 L17 45 L24 38 L29 30 L33 22 L37 13 Z";
function MapPicker({ lang, initDep, initArr, onConfirm, onCancel }) {
const cities = window.ATLAS.mapCities;
const [mode, setMode] = useState('dep');
const [dep, setDep] = useState(initDep || null); // {name,x,y}
const [arr, setArr] = useState(initArr || null);
const boxRef = useRef(null);
const nearest = (px, py) => {
let best = null, bd = 1e9;
cities.forEach(c => { const d = (c.x - px) ** 2 + (c.y - py) ** 2; if (d < bd) { bd = d; best = c; } });
return best;
};
const place = (e) => {
const r = boxRef.current.getBoundingClientRect();
const p = e.touches ? e.touches[0] : e;
const px = (p.clientX - r.left) / r.width, py = (p.clientY - r.top) / r.height;
const c = nearest(px, py);
if (!c) return;
if (mode === 'dep') { setDep(c); setMode('arr'); } else { setArr(c); }
};
const confirm = () => onConfirm({ pickup: dep ? dep.name : '', dropoff: arr ? arr.name : '' });
const pin = (c, kind) => c ? (
) : null;
return (
{T(lang, 'map_title')}
{cities.map(c => (
{c.name}
))}
{pin(dep, 'dep')}
{pin(arr, 'arr')}
{T(lang, 'map_hint')}
);
}
Object.assign(window, {
useAuth, WhatsAppWidget, Modal, AuthModal, SignaturePad, MapPicker,
StatusTimeline, ClientQuoteBox, ClientChat, ClientRequestView,
});