Replace manual useState + validate() wiring with react-hook-form and
yupResolver across CarFormModal, DriverFormModal, Tripformmodal,
OrderFormModal, BranchFormModal, CarMaintananceFormModal, RoleFormModal,
and UserFormModal, following the existing pattern in Addressformmodal.
- Pin yupResolver's generic explicitly (yupResolver<FormValues>(schema as any))
since create/update schemas differ structurally and yup can't unify them
into a single inferred type on its own.
- Replace manual ref-based autofocus (useRef + ref={firstRef}) with
autoFocus/setFocus to avoid ref collisions with register()'s own ref.
- DriverFormModal: bind photo/nationalPhoto/driverCardPhoto file fields via
Controller instead of register.
- Tripformmodal: convert startTime/endTime to ISO-8601 via setValueAs while
keeping the datetime-local input display unaffected.
- OrderFormModal: register nested deliveryAddress/pickupAddress fields via
dot-path syntax; keep pickupMode as local state; preserve conditional
address payload assembly.
- CarMaintananceFormModal: keep toNumberOrUndefined cost coercion and
ISO-8601 date conversion at submit time.
- RoleFormModal: bind permissionIds checkbox group via Controller.
- UserFormModal: add a custom resolver wrapper to preserve the
empty-password-on-edit behavior (skip password validation when editing
with a blank password field).
Also replace raw HTML elements with the shared UI kit components
(Modal, Input, Textarea, Select, Button, Alert, Badge, EmptyState) across
form modals, detail modals, and tables:
- RoleFormModal: Modal, Input, Textarea, Button
- OrderFormModal: Modal, Input, Select, Button
- UserDetailModal / Archiveduserdetailmodal: Modal, Alert, Button
- UserTable / Archivedusertable: Badge, Button, EmptyState
- RoleDetailModal / ArchivedRoleDetailModal: Modal, Alert, Select, Button
- RoleTable / ArchivedRoleTable: Button, EmptyState
- Tripreportpanel: Button with built-in loading state instead of manual
Spinner/disabled/color wiring
StatusBadge, IconBtn, and other per-action custom chips/checkboxes are kept
as-is in every file — the shared Badge/Button components don't support dot
indicators or per-action color-coding, so forcing them in would misshape or
strip semantics from these controls.
Archivedusersmodal, ArchivedRolesModal, and ArchivedTripList required no
changes — already fully compliant with the shared UI kit.
No changes to visual layout, CSS, or Arabic labels/section headings beyond
what's required by the component swaps above.
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
"use client";
|
||
|
||
import { useRef, useState } from "react";
|
||
import { Button, Toast } from "../UI";
|
||
import { tripService } from "@/src/services/trip.service";
|
||
import { getStoredToken } from "@/src/lib/auth";
|
||
import type { TripNotification } from "@/src/hooks/useTrip";
|
||
|
||
interface TripReportPanelProps {
|
||
tripId: string;
|
||
}
|
||
|
||
function extractApiMessage(err: unknown, fallback: string): string {
|
||
if (err && typeof err === "object") {
|
||
const e = err as Record<string, unknown>;
|
||
const rd = (e["response"] as Record<string, unknown> | undefined)?.["data"];
|
||
if (rd && typeof rd === "object") {
|
||
const msg = (rd as Record<string, unknown>)["message"];
|
||
if (typeof msg === "string" && msg.trim()) return msg;
|
||
}
|
||
if (typeof e["message"] === "string" && e["message"].trim()) return e["message"];
|
||
}
|
||
return fallback;
|
||
}
|
||
|
||
export function TripReportPanel({ tripId }: TripReportPanelProps) {
|
||
const [loading, setLoading] = useState(false);
|
||
const [notification, setNotification] = useState<TripNotification | null>(null);
|
||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
|
||
const notify = (n: TripNotification) => {
|
||
if (timerRef.current) clearTimeout(timerRef.current);
|
||
setNotification(n);
|
||
timerRef.current = setTimeout(() => setNotification(null), 4000);
|
||
};
|
||
|
||
const handleGenerate = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const token = getStoredToken();
|
||
const { reportUrl } = await tripService.getReport(tripId, token);
|
||
|
||
if (!reportUrl) {
|
||
notify({ type: "error", message: "لم يتم إرجاع رابط التقرير من الخادم." });
|
||
return;
|
||
}
|
||
|
||
window.open(reportUrl, "_blank", "noopener,noreferrer");
|
||
notify({ type: "success", message: "تم إنشاء التقرير بنجاح." });
|
||
} catch (err) {
|
||
notify({ type: "error", message: extractApiMessage(err, "تعذّر إنشاء التقرير.") });
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<>
|
||
<div
|
||
style={{
|
||
marginTop: "1.5rem",
|
||
borderRadius: "var(--radius-lg)",
|
||
border: "1px solid var(--color-border)",
|
||
background: "var(--color-surface-muted)",
|
||
padding: "1rem",
|
||
}}
|
||
>
|
||
<p
|
||
style={{
|
||
fontSize: 11,
|
||
letterSpacing: "0.25em",
|
||
textTransform: "uppercase",
|
||
color: "#2563EB",
|
||
fontWeight: 700,
|
||
margin: "0 0 0.75rem",
|
||
}}
|
||
>
|
||
إنشاء تقرير الرحلة
|
||
</p>
|
||
|
||
<Button type="button" onClick={handleGenerate} loading={loading}>
|
||
{loading ? "جارٍ الإنشاء…" : "إنشاء تقرير البيان"}
|
||
</Button>
|
||
</div>
|
||
|
||
<Toast
|
||
notification={notification}
|
||
onDismiss={() => {
|
||
if (timerRef.current) clearTimeout(timerRef.current);
|
||
setNotification(null);
|
||
}}
|
||
/>
|
||
</>
|
||
);
|
||
} |