diff --git a/app/dashboard/cars/[carId]/maintenance/[maintenanceId]/page.tsx b/app/dashboard/cars/[carId]/maintenance/[maintenanceId]/page.tsx index 69ab459..9745255 100644 --- a/app/dashboard/cars/[carId]/maintenance/[maintenanceId]/page.tsx +++ b/app/dashboard/cars/[carId]/maintenance/[maintenanceId]/page.tsx @@ -1,8 +1,16 @@ "use client"; +import { useCallback, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { Alert, PageHeader, Spinner } from "@/src/Components/UI"; -import { useCarMaintenanceList } from "@/src/hooks/UseCarsMaintanance"; +import { Alert, ArchiveButton, Button, ConfirmDialog, EmptyState, PageHeader, Spinner, Toast } from "@/src/Components/UI"; +import { CarMaintenanceFormModal } from "@/src/Components/Car_Maintanance/CarMaintananceFormModal"; +import { ArchivedCarsMaintenanceModal } from "@/src/Components/Car_Maintanance/archive/ArchivedCarsMaintananceModal"; +import { + useCarMaintenanceList, + useCarMaintenanceMutations, + useMaintenanceToast, +} from "@/src/hooks/UseCarsMaintanance"; +import { useCarDetail } from "@/src/hooks/useCars"; import { MAINTENANCE_STATUS_MAP, fmtDate, @@ -10,92 +18,188 @@ import { durationDays, getMaintenanceStatus, } from "@/src/types/carMaintanance"; +import type { + CarMaintenance, + CreateMaintenancePayload, + UpdateMaintenancePayload, +} from "@/src/types/carMaintanance"; + +function MaintenanceCard({ + record, onView, onEdit, onDelete, +}: { + record: CarMaintenance; + onView: () => void; + onEdit: () => void; + onDelete: () => void; +}) { + const status = MAINTENANCE_STATUS_MAP[getMaintenanceStatus(record)]; -function DetailRow({ label, value, mono = false }: { label: string; value: string; mono?: boolean }) { return ( -
- {label} - - {value} - -
+
+
+
+

+ {record.reason} +

+ + {status.label} + +
+ +
+
+ التكلفة + {fmtCost(record.cost)} +
+
+ المدة + {durationDays(record.startAt, record.endAt)} يوم +
+
+ تاريخ البدء + {fmtDate(record.startAt)} +
+
+ تاريخ الانتهاء + {fmtDate(record.endAt)} +
+
+ +
+ + + {/* inline style keeps the original light-red look; Button's own + variant="danger" is solid red and would change the visual */} + +
+
+ ); } -export default function CarMaintenanceDetailPage() { - const { carId, maintenanceId } = useParams<{ carId: string; maintenanceId: string }>(); +export default function CarMaintenancePage() { + const { carId } = useParams<{ carId: string }>(); const router = useRouter(); - // NOTE: GET /cars/:carId/maintenance/:maintenanceId isn't wired up on the - // backend yet (returns "Route not found"), so this page resolves the - // record from the already-loaded list instead of a dedicated single-record - // call. includeDeleted: true so an archived record still resolves to a - // real "مؤرشفة" status instead of silently disappearing. - const { records, loading, error } = useCarMaintenanceList(carId, { includeDeleted: true }); - const record = records.find((r) => r.id === maintenanceId) ?? null; + const { car, refetch: refetchCar } = useCarDetail(carId); + const carLabel = car ? `${car.manufacturer} ${car.model} — ${car.plateLetters} ${car.plateNumber}` : undefined; + + const { records, loading, error, loadRecords, removeRecord, setError } = useCarMaintenanceList(carId); + const { toast, notify } = useMaintenanceToast(); + + const [formTarget, setFormTarget] = useState(false); // false = closed + const [deleteTarget, setDeleteTarget] = useState(null); + const [archiveOpen, setArchiveOpen] = useState(false); + + const getEditTarget = useCallback( + () => (formTarget instanceof Object && formTarget !== null ? (formTarget as CarMaintenance) : null), + [formTarget], + ); + + const { deleting, handleFormSubmit, handleDeleteConfirm } = useCarMaintenanceMutations({ + carId, + onSuccess: (msg) => { notify({ type: "success", message: msg }); loadRecords(); refetchCar(); }, + onError: (msg) => notify({ type: "error", message: msg }), + onDeleted: (id) => { removeRecord(id); setDeleteTarget(null); refetchCar(); }, + getEditTarget, + }); return ( -
- + + + {formTarget !== false && ( + setFormTarget(false)} + onSubmit={(payload: CreateMaintenancePayload | UpdateMaintenancePayload, isNew: boolean) => + handleFormSubmit(payload, isNew).then(ok => { if (ok) setFormTarget(false); return ok; }) + } + /> + )} + + setDeleteTarget(null)} + onConfirm={() => deleteTarget && handleDeleteConfirm(deleteTarget)} + title="حذف سجل الصيانة" + description={`هل أنت متأكد من حذف سجل ${deleteTarget?.reason ?? ""} (${deleteTarget ? fmtCost(deleteTarget.cost) : ""})؟ لا يمكن التراجع عن هذا الإجراء.`} /> -
- {loading && ( -
- - جارٍ التحميل… + {archiveOpen && ( + setArchiveOpen(false)} + /> + )} + +
+ setFormTarget(null)}> + + + + إضافة سجل صيانة + + } + /> + + {error && setError(null)} />} + + {loading ? ( +
+ + جارٍ تحميل سجلات الصيانة… +
+ ) : records.length === 0 ? ( + + ) : ( +
+ {records.map(record => ( + router.push(`/dashboard/cars/${carId}/maintenance/${record.id}`)} + onEdit={() => setFormTarget(record)} + onDelete={() => setDeleteTarget(record)} + /> + ))}
)} +
- {error && } - - {!loading && !error && !record && ( -

- لم يتم العثور على سجل الصيانة. -

- )} - - {!loading && !error && record && (() => { - const status = MAINTENANCE_STATUS_MAP[getMaintenanceStatus(record)]; - return ( - <> -
- - {status.label} - -
- - - - - - - {record.car && ( - - )} - - - - ); - })()} -
- -
- -
-
+ setArchiveOpen(true)} label="أرشيف الصيانة" /> + ); } \ No newline at end of file diff --git a/app/dashboard/cars/page.tsx b/app/dashboard/cars/page.tsx index 709579d..75a2319 100644 --- a/app/dashboard/cars/page.tsx +++ b/app/dashboard/cars/page.tsx @@ -1,46 +1,17 @@ "use client"; - import { useCallback, useState } from "react"; -import { Spinner, Alert, ArchiveButton, ConfirmDialog } from "@/src/Components/UI"; +import { Spinner, Alert, ArchiveButton, ConfirmDialog, Toast, EmptyState, Button } from "@/src/Components/UI"; import { CarFormModal,CarDetailPanel } from "@/src/Components/car"; import { ArchivedCarsModal } from "@/src/Components/car/archive/Archivedcarsmodal"; import { CarMaintenanceFormModal } from "@/src/Components/Car_Maintanance/CarMaintananceFormModal"; import { useCars, useCarMutations, useToast } from "@/src/hooks/useCars"; import { useCarMaintenanceMutations } from "@/src/hooks/UseCarsMaintanance"; import { fmtDateShort, isExpiringSoon, STATUS_MAP, INS_MAP } from "@/src/types/car"; -import type { Car, CreateCarPayload, ToastMsg, UpdateCarPayload } from "@/src/types/car"; +import type { Car, CreateCarPayload, UpdateCarPayload } from "@/src/types/car"; import type { CreateMaintenancePayload, UpdateMaintenancePayload } from "@/src/types/carMaintanance"; import Header from "@/src/Components/UI/Header"; -// ── Toast ───────────────────────────────────────────────────────────────────── - -function CarToast({ notification }: { notification: ToastMsg | null }) { - if (!notification) return null; - const ok = notification.type === "success"; - return ( -
-
- {ok ? "✓" : "⚠"} - {notification.message} -
-
- ); -} - // ── CarCard ─────────────────────────────────────────────────────────────────── function CarCard({ @@ -172,7 +143,11 @@ function CarCard({
- {/* Send to Maintenance quick action */} + {/* Send to Maintenance quick action — kept custom (amber accent isn't + one of Button's variants: primary/secondary/ghost/danger). Same + rationale as IconBtn: forcing it into an existing variant would + misrepresent this as a routine action rather than the + maintenance-triggering one it is. */} + )} - + /> ) : ( /* ── Card grid ── */
setPage(p => Math.max(1, p - 1)), disabled: page === 1 }, { label: "التالي →", action: () => setPage(p => Math.min(pages, p + 1)), disabled: page === pages }, ].map((btn) => ( - + ))}
diff --git a/app/dashboard/drivers/[driverId]/page.tsx b/app/dashboard/drivers/[driverId]/page.tsx index 7484f7a..d132014 100644 --- a/app/dashboard/drivers/[driverId]/page.tsx +++ b/app/dashboard/drivers/[driverId]/page.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { ConfirmDialog, Spinner } from "@/src/Components/UI"; +import { ConfirmDialog, Spinner, Button, Toast } from "@/src/Components/UI"; import { DriverFormModal , PhotoCard } from "@/src/Components/Driver"; import { DriverReportPanel } from "@/src/Components/Driver_Report/driverReport"; import type { Driver, CreateDriverPayload, UpdateDriverPayload } from "@/src/types/driver"; @@ -164,14 +164,9 @@ export default function DriverDetailPage() { return (

⚠ {error ?? "السائق غير موجود"}

- +
); } @@ -180,35 +175,18 @@ export default function DriverDetailPage() { <>
- {toast && ( -
- {toast.type === "success" ? "✓ " : "⚠ "}{toast.message} -
- )} + setToast(null)} />
- +
@@ -249,25 +227,16 @@ export default function DriverDetailPage() {
- - + +
diff --git a/app/dashboard/drivers/page.tsx b/app/dashboard/drivers/page.tsx index d9a24a2..ba8b90a 100644 --- a/app/dashboard/drivers/page.tsx +++ b/app/dashboard/drivers/page.tsx @@ -7,6 +7,8 @@ import { ArchiveButton, ConfirmDialog, Toast, + Button, + IconBtn, } from "@/src/Components/UI"; import { ArchivedDrivers } from "@/src/Components/Driver/archive/ArchivedDrivers"; import { useDrivers } from "@/src/hooks/useDriver"; @@ -59,17 +61,6 @@ const thStyle: React.CSSProperties = { // Shared across header + rows — keep these in sync or columns will misalign. const ROW_GRID_COLUMNS = "2fr 1.2fr 1fr 1.1fr 1.1fr 1fr 0.8fr"; -const iconBtnBase: React.CSSProperties = { - width: 30, - height: 30, - display: "flex", - alignItems: "center", - justifyContent: "center", - borderRadius: "var(--radius-md)", - cursor: "pointer", - flexShrink: 0, -}; - // ── Page Component ──────────────────────────────────────────────────────────── export default function DriversPage() { @@ -346,24 +337,16 @@ export default function DriversPage() { {/* ── Inline row actions: edit / delete ── - stopPropagation is required on both buttons so the - click doesn't bubble up to the
  • onClick and + IconBtn already calls stopPropagation internally so + the click doesn't bubble up to the
  • onClick and open the detail panel as well. */}
    - + - +
  • ); @@ -437,37 +412,22 @@ export default function DriversPage() {
    - {[ - { - label: "السابق", - action: () => setPage((p) => Math.max(1, p - 1)), - disabled: page === 1, - }, - { - label: "التالي", - action: () => setPage((p) => Math.min(pages, p + 1)), - disabled: page === pages, - }, - ].map((btn) => ( - - ))} + +
    )} diff --git a/app/dashboard/trips/[tripId]/page.tsx b/app/dashboard/trips/[tripId]/page.tsx index 7df8f52..77855ab 100644 --- a/app/dashboard/trips/[tripId]/page.tsx +++ b/app/dashboard/trips/[tripId]/page.tsx @@ -11,7 +11,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { useParams, useRouter } from "next/navigation"; -import { ConfirmDialog, Spinner } from "@/src/Components/UI"; +import { Button, ConfirmDialog, Spinner } from "@/src/Components/UI"; import { TripFormModal } from "@/src/Components/Trip"; import { TripReportPanel } from "@/src/Components/Trip_Report/Tripreportpanel"; import { tripService } from "@/src/services/trip.service"; @@ -304,25 +304,9 @@ export default function TripDetailPage() {

    ⚠ {error ?? "الرحلة غير موجودة"}

    - + ); } @@ -346,29 +330,17 @@ export default function TripDetailPage() { }} > {/* Back link */} - +
    {/* Icon + title */} diff --git a/src/Components/UI/Header.tsx b/src/Components/UI/Header.tsx index 37a1520..880fd9b 100644 --- a/src/Components/UI/Header.tsx +++ b/src/Components/UI/Header.tsx @@ -1,3 +1,7 @@ +import { Button } from "./Button"; +import { Input } from "./Input"; +import { Select } from "./Select"; + export default function Header({ state, search, @@ -75,50 +79,30 @@ export default function Header({ display: "flex", gap: "0.5rem", flexWrap: "wrap", - alignItems: "center", + alignItems: "flex-end", }} > - { setSearch(e.target.value); setPage(1); }} dir="rtl" - style={{ - width: 220, - height: 40, - padding: "0 0.75rem", - borderRadius: "var(--radius-lg)", - border: "1px solid var(--color-border)", - background: "var(--color-surface)", - fontSize: 13, - color: "var(--color-text-primary)", - outline: "none", - fontFamily: "var(--font-sans)", - }} + className="w-[220px]" /> {isAudit ? ( - + ) : ( - + )}