solve problem tree rerender
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("");
|
||||
|
||||
|
||||
@@ -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("");
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user