solve problem tree rerender

This commit is contained in:
m7amedez5511
2026-08-19 18:00:55 +03:00
parent 55b03ad6f4
commit 2d05d3446c
11 changed files with 1441 additions and 479 deletions

View File

@@ -6,7 +6,11 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, FileInput, Input, Modal, Select } from "../UI";
import { get } from "@/src/services/api";
import { getStoredToken } from "@/src/lib/auth";
import { createDriverSchema, updateDriverSchema } from "@/src/validations/driver.validator";
import {
createDriverSchema,
updateDriverSchema,
} from "@/src/validations/driver.validator";
import { useEditFormSync } from "@/src/hooks/useEditFormSync";
import type {
Driver,
CreateDriverPayload,
@@ -105,7 +109,9 @@ export function DriverFormModal({
// have structurally different required fields (union type yupResolver's
// single-schema signature won't accept), and yup's inferred type for
// .optional() fields is structurally incompatible with DriverFormValues.
resolver: yupResolver((isNew ? createDriverSchema : updateDriverSchema) as any) as any,
resolver: yupResolver(
(isNew ? createDriverSchema : updateDriverSchema) as any,
) as any,
defaultValues: {
name: editDriver?.name ?? "",
phone: editDriver?.phone ?? "",
@@ -113,9 +119,12 @@ export function DriverFormModal({
address: editDriver?.address ?? "",
nationality: editDriver?.nationality ?? "",
nationalIdType: editDriver?.nationalIdType ?? "",
nationalId: (editDriver as Driver & { nationalId?: string })?.nationalId ?? "",
nationalId:
(editDriver as Driver & { nationalId?: string })?.nationalId ?? "",
nationalIdExpiry:
(editDriver as Driver & { nationalIdExpiry?: string })?.nationalIdExpiry?.slice(0, 10) ?? "",
(
editDriver as Driver & { nationalIdExpiry?: string }
)?.nationalIdExpiry?.slice(0, 10) ?? "",
gosiNumber: editDriver?.gosiNumber ?? "",
licenseNumber: editDriver?.licenseNumber ?? "",
licenseType: editDriver?.licenseType ?? "",
@@ -135,29 +144,30 @@ export function DriverFormModal({
// ── Local branches (auto-fetched if prop is empty) ─────────────────────────
const [branches, setBranches] = useState<Branch[]>(branchesProp);
useEffect(() => {
// defaultValues.branchId was applied before this list existed, so the
// <select> had no matching <option> yet and silently fell back to "" —
// reapply the saved id now that the option actually exists in the DOM.
const savedBranchId = (editDriver as Driver & { branchId?: string })?.branchId;
if (branchesProp.length > 0) {
queueMicrotask(() => {
setBranches(branchesProp);
if (savedBranchId) setValue("branchId", savedBranchId);
});
queueMicrotask(() => setBranches(branchesProp));
return;
}
const token = getStoredToken();
get<{ data: { data: Branch[] } }>("branches?limit=100", token)
.then((res) => {
const list = (res as unknown as { data: { data: Branch[] } }).data?.data ?? [];
const list =
(res as unknown as { data: { data: Branch[] } }).data?.data ?? [];
setBranches(list);
if (savedBranchId) setValue("branchId", savedBranchId);
})
.catch(() => {
/* silently ignore */
});
}, [branchesProp, editDriver, setValue]);
}, [branchesProp]);
// CHANGE: replaced the manual `if (savedBranchId) setValue("branchId", ...)`
// calls above with useEditFormSync — same fix, shared implementation.
useEditFormSync(
setValue,
"branchId",
(editDriver as Driver & { branchId?: string })?.branchId,
branches,
);
const [apiError, setApiError] = useState("");
@@ -171,20 +181,27 @@ export function DriverFormModal({
const submitHandler = useCallback(
async (data: DriverFormValues) => {
const payload: Record<string, unknown> = { name: data.name, phone: data.phone };
const payload: Record<string, unknown> = {
name: data.name,
phone: data.phone,
};
if (data.email) payload.email = data.email;
if (data.address) payload.address = data.address;
if (data.nationality) payload.nationality = data.nationality;
if (data.nationalIdType) payload.nationalIdType = data.nationalIdType;
if (data.nationalId) payload.nationalId = data.nationalId;
if (data.nationalIdExpiry) payload.nationalIdExpiry = toIsoDateTime(data.nationalIdExpiry);
if (data.nationalIdExpiry)
payload.nationalIdExpiry = toIsoDateTime(data.nationalIdExpiry);
if (data.gosiNumber) payload.gosiNumber = data.gosiNumber;
if (data.licenseNumber) payload.licenseNumber = data.licenseNumber;
if (data.licenseType) payload.licenseType = data.licenseType;
if (data.licenseExpiry) payload.licenseExpiry = toIsoDateTime(data.licenseExpiry);
if (data.driverCardNumber) payload.driverCardNumber = data.driverCardNumber;
if (data.licenseExpiry)
payload.licenseExpiry = toIsoDateTime(data.licenseExpiry);
if (data.driverCardNumber)
payload.driverCardNumber = data.driverCardNumber;
if (data.driverCardType) payload.driverCardType = data.driverCardType;
if (data.driverCardExpiry) payload.driverCardExpiry = toIsoDateTime(data.driverCardExpiry);
if (data.driverCardExpiry)
payload.driverCardExpiry = toIsoDateTime(data.driverCardExpiry);
if (data.driverType) payload.driverType = data.driverType;
if (data.branchId) payload.branchId = data.branchId;
if (!isNew) payload.status = data.status;
@@ -196,15 +213,23 @@ export function DriverFormModal({
setApiError("");
try {
const ok = await onSubmit(payload as unknown as CreateDriverPayload, isNew);
const ok = await onSubmit(
payload as unknown as CreateDriverPayload,
isNew,
);
if (ok) {
onClose();
} else {
setError("name", { message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً." });
setError("name", {
message: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.",
});
setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.");
}
} catch (err) {
const message = err instanceof Error ? err.message : "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.";
const message =
err instanceof Error
? err.message
: "حدث خطأ غير متوقع. يرجى المحاولة لاحقاً.";
setError("name", { message });
setApiError(message);
}
@@ -220,10 +245,15 @@ export function DriverFormModal({
onClose={onClose}
size="lg"
subtitle={isNew ? "إضافة سائق" : "تعديل سائق"}
title={isNew ? "سائق جديد" : editDriver?.name ?? ""}
title={isNew ? "سائق جديد" : (editDriver?.name ?? "")}
footer={
<>
<Button variant="secondary" type="button" onClick={onClose} disabled={isSubmitting}>
<Button
variant="secondary"
type="button"
onClick={onClose}
disabled={isSubmitting}
>
إلغاء
</Button>
<Button type="submit" form={FORM_ID} loading={isSubmitting}>
@@ -240,7 +270,11 @@ export function DriverFormModal({
className="flex flex-col gap-4"
>
{errors.name?.type === "manual" && (
<Alert type="error" message={errors.name.message ?? ""} onClose={() => setApiError("")} />
<Alert
type="error"
message={errors.name.message ?? ""}
onClose={() => setApiError("")}
/>
)}
{/* ── Section: Personal Info ── */}
@@ -252,7 +286,9 @@ export function DriverFormModal({
placeholder="محمد عبدالله"
dir="rtl"
autoComplete="off"
error={errors.name?.type !== "manual" ? errors.name?.message : undefined}
error={
errors.name?.type !== "manual" ? errors.name?.message : undefined
}
{...register("name")}
/>
@@ -404,7 +440,9 @@ export function DriverFormModal({
>
<option value="">اختر الفرع</option>
{branches.map((b) => (
<option key={b.id} value={b.id}>{b.name}</option>
<option key={b.id} value={b.id}>
{b.name}
</option>
))}
</Select>
@@ -429,7 +467,14 @@ export function DriverFormModal({
{/* ── Section: Photos ── */}
<p style={sectionHeadingStyle}>الصور والمستندات</p>
<p style={{ fontSize: 11, color: "var(--color-text-hint)", margin: "0.25rem 0 0", fontWeight: 400 }}>
<p
style={{
fontSize: 11,
color: "var(--color-text-hint)",
margin: "0.25rem 0 0",
fontWeight: 400,
}}
>
جميع حقول الصور اختيارية
</p>
@@ -440,25 +485,37 @@ export function DriverFormModal({
name="photo"
control={control}
render={({ field }) => (
<FileInput label="صورة السائق" current={field.value} onChange={field.onChange} />
<FileInput
label="صورة السائق"
current={field.value}
onChange={field.onChange}
/>
)}
/>
<Controller
name="nationalPhoto"
control={control}
render={({ field }) => (
<FileInput label="صورة الهوية" current={field.value} onChange={field.onChange} />
<FileInput
label="صورة الهوية"
current={field.value}
onChange={field.onChange}
/>
)}
/>
<Controller
name="driverCardPhoto"
control={control}
render={({ field }) => (
<FileInput label="صورة البطاقة" current={field.value} onChange={field.onChange} />
<FileInput
label="صورة البطاقة"
current={field.value}
onChange={field.onChange}
/>
)}
/>
</div>
</form>
</Modal>
);
}
}

View File

@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, Input, Modal, Select, Textarea } from "../UI";
import { getStoredToken } from "@/src/lib/auth";
import { useEditFormSync } from "@/src/hooks/useEditFormSync";
import {
createTripSchema,
updateTripSchema,
@@ -126,30 +127,25 @@ export function TripFormModal({
},
});
useEffect(() => {
const token = getStoredToken();
// ── Fetch dropdown options once on mount ────────────────────────────────────
// CHANGE: split fetching from syncing — fetching stays a plain useEffect,
// syncing the saved id back into the form is now handled by useEditFormSync
// below, once per field, instead of being inlined into each .then().
useEffect(() => {
const token = getStoredToken();
driverService.getActiveOptions(token).then((list) => {
setDrivers(list);
// defaultValues were applied before this list existed, so the <select>
// had no matching <option> yet and silently fell back to "" — reapply
// the saved id now that the option actually exists in the DOM.
const savedDriverId = editTrip?.driverId ?? editTrip?.driver?.id;
if (savedDriverId) setValue("driverId", savedDriverId);
}).catch(() => {});
driverService.getActiveOptions(token).then(setDrivers).catch(() => {});
carService.getActiveOptions(token).then(setCars).catch(() => {});
branchService.getOptions(token).then(setBranches).catch(() => {});
}, []);
carService.getActiveOptions(token).then((list) => {
setCars(list);
const savedCarId = editTrip?.carId ?? editTrip?.car?.id;
if (savedCarId) setValue("carId", savedCarId);
}).catch(() => {});
branchService.getOptions(token).then((list) => {
setBranches(list);
const savedBranchId = editTrip?.branchId ?? editTrip?.branch?.id;
if (savedBranchId) setValue("branchId", savedBranchId);
}).catch(() => {});
}, [editTrip, setValue]);
// CHANGE: replaced the three manual `if (savedXId) setValue(...)` calls
// (previously inlined inside each .then() above) with useEditFormSync —
// each hook call re-applies the saved id once its matching option list
// has loaded, regardless of which fetch resolves first.
useEditFormSync(setValue, "driverId", editTrip?.driverId ?? editTrip?.driver?.id, drivers);
useEditFormSync(setValue, "carId", editTrip?.carId ?? editTrip?.car?.id, cars);
useEditFormSync(setValue, "branchId", editTrip?.branchId ?? editTrip?.branch?.id, branches);
const [apiError, setApiError] = useState("");

View File

@@ -5,6 +5,7 @@ import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import { Alert, Button, Input, Modal, Select } from "../UI";
import { getStoredToken } from "@/src/lib/auth";
import { useEditFormSync } from "@/src/hooks/useEditFormSync";
import {
createCarSchema,
updateCarSchema,
@@ -134,38 +135,31 @@ export function CarFormModal({
},
});
// ── Local branches (auto-fetched if prop is empty) ─────────────────────────
const [branches, setBranches] = useState<Branch[]>(branchesProp);
const loadBranches = useCallback(() => {
// defaultValues.branchId was applied before this list existed, so the
// <select> had no matching <option> yet and silently fell back to "" —
// reapply the saved id now that the option actually exists in the DOM.
const savedBranchId = editCar?.branch?.id;
// ── Local branches (auto-fetched if prop is empty) ─────────────────────────
const [branches, setBranches] = useState<Branch[]>(branchesProp);
const loadBranches = useCallback(() => {
if (branchesProp.length > 0) {
queueMicrotask(() => setBranches(branchesProp));
return;
}
const token = getStoredToken();
branchService
.getOptions(token)
.then((list) => {
queueMicrotask(() => setBranches(list as unknown as Branch[]));
})
.catch(() => {
/* silently ignore */
});
}, [branchesProp]);
if (branchesProp.length > 0) {
queueMicrotask(() => {
setBranches(branchesProp);
if (savedBranchId) setValue("branchId", savedBranchId);
});
return;
}
const token = getStoredToken();
branchService
.getOptions(token)
.then((list) => {
queueMicrotask(() => {
setBranches(list as unknown as Branch[]);
if (savedBranchId) setValue("branchId", savedBranchId);
});
})
.catch(() => {
/* silently ignore */
});
}, [branchesProp, editCar, setValue]);
useEffect(() => {
loadBranches();
}, [loadBranches]);
useEffect(() => {
loadBranches();
}, [loadBranches]);
// CHANGE: replaced the manual `if (savedBranchId) setValue("branchId", ...)`
// calls above with useEditFormSync.
useEditFormSync(setValue, "branchId", editCar?.branch?.id, branches);
const [apiError, setApiError] = useState("");

View File

@@ -14,12 +14,14 @@ export function useCurrentUser() {
setLoading(true);
setError(null);
try {
//1. get token from local storage
const token = getStoredToken();
//2. call userService.getMe(token) to get the current user
const res = await userService.getMe(token);
// ملاحظة: مبقاش بنعمل res.data ?? res هنا قبل التمرير.
// extractMeUser هي المسؤولة الوحيدة عن تفكيك شكل الـ response،
// فبنبعتلها الـ res زي ما هو عشان نتجنب تفكيك مزدوج.
// console.log("useCurrentUser: got me", res);
//3. extract the user from the response using extractMeUser(res)
const u = extractMeUser(res);
//4. if user is null, throw an error
if (!u) throw new Error("لا توجد بيانات");
setUser(u as UserMe);
} catch {

View File

@@ -82,7 +82,7 @@ useEffect(() => {
}, [page, search, loadUsers]);
// create new user
const createUser = useCallback(async (data: UserFormData): Promise<boolean> => {
const createUser = useCallback(async (data: UserFormData): Promise<boolean> => {
try {
const token = getStoredToken();
const user = await userService.create(data as UserFormData & { password: string }, token);
@@ -100,7 +100,7 @@ useEffect(() => {
try {
const token = getStoredToken();
const res = await userService.update(id, data, token);
dispatch({ type: "UPDATE", user: res?.data });
dispatch({ type: "UPDATE", user: res });
notify({ type: "success", message: "تم تحديث بيانات المستخدم بنجاح." });
return true;
} catch (err) {