update client and client adresses pages also create branch pages and role pages and update some pages to be batter

This commit is contained in:
m7amedez5511
2026-06-30 11:18:49 +03:00
parent 0847bd23ee
commit eaae6810bd
49 changed files with 2559 additions and 3001 deletions

View File

@@ -0,0 +1,258 @@
"use client";
import type { ClientAddress } from "@/src/types/client_adresses";
// ─── Helpers ───────────────────────────────────────────────────────────────
function labelIcon(label: string): string {
const map: Record<string, string> = {
"فوترة": "💳",
"شحن": "📦",
"المقر الرئيسي": "🏢",
"فرع": "🏬",
"مستودع": "🏭",
billing: "💳",
shipping: "📦",
"head office": "🏢",
branch: "🏬",
warehouse: "🏭",
};
return map[label.toLowerCase()] ?? "📍";
}
// ─── Sub-components ────────────────────────────────────────────────────────
function DetailRow({
label,
value,
ltr = false,
}: {
label: string;
value?: string | null;
ltr?: boolean;
}) {
if (!value) return null;
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 4,
padding: "0.75rem 0",
borderBottom: "1px solid var(--color-border)",
}}
>
<span
style={{
fontSize: 11,
fontWeight: 700,
color: "var(--color-text-muted)",
textTransform: "uppercase",
letterSpacing: "0.07em",
}}
>
{label}
</span>
<span
dir={ltr ? "ltr" : "rtl"}
style={{
fontSize: 14,
fontWeight: 500,
color: "var(--color-text-primary)",
fontFamily: ltr ? "var(--font-mono)" : "var(--font-sans)",
}}
>
{value}
</span>
</div>
);
}
function SectionCard({
title,
icon,
children,
}: {
title: string;
icon: string;
children: React.ReactNode;
}) {
return (
<div
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
boxShadow: "var(--shadow-card)",
overflow: "hidden",
}}
>
{/* Card header */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
}}
>
<span style={{ fontSize: 16 }} aria-hidden="true">{icon}</span>
<p
style={{
margin: 0,
fontSize: 12,
fontWeight: 700,
color: "var(--color-text-secondary)",
letterSpacing: "0.05em",
textTransform: "uppercase",
}}
>
{title}
</p>
</div>
{/* Card body */}
<div
dir="rtl"
style={{ padding: "0 1.5rem" }}
>
{children}
</div>
</div>
);
}
// ─── Main component ────────────────────────────────────────────────────────
interface AddressDetailsProps {
address: ClientAddress;
}
export function AddressDetails({ address }: AddressDetailsProps) {
const { details, contactPerson, location } = address;
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
{/* Identity card — label + branch */}
<div
style={{
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
boxShadow: "var(--shadow-card)",
padding: "1.25rem 1.5rem",
display: "flex",
alignItems: "center",
gap: "1rem",
}}
dir="rtl"
>
<div
style={{
width: 52,
height: 52,
borderRadius: "var(--radius-lg)",
background: "var(--color-brand-50)",
border: "1px solid var(--color-brand-100)",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: 24,
flexShrink: 0,
}}
aria-hidden="true"
>
{labelIcon(address.label)}
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<p
style={{
margin: 0,
fontSize: 18,
fontWeight: 700,
color: "var(--color-text-primary)",
}}
>
{address.label}
</p>
{address.branchName && (
<p
style={{
margin: "3px 0 0",
fontSize: 13,
color: "var(--color-text-muted)",
}}
>
{address.branchName}
</p>
)}
</div>
</div>
{/* Address details */}
<SectionCard title="تفاصيل العنوان" icon="🗺️">
<DetailRow label="الشارع" value={details.street} />
<DetailRow label="المدينة" value={details.city} />
<DetailRow label="المنطقة / الولاية" value={details.state} />
<DetailRow label="الحي" value={details.district} />
<DetailRow label="رقم المبنى" value={details.buildingNo} ltr />
<DetailRow label="رقم الوحدة" value={details.unitNo} ltr />
<DetailRow label="الرقم الإضافي" value={details.additionalNo} ltr />
<DetailRow label="الرمز البريدي" value={details.zipCode} ltr />
<DetailRow label="الشقة / الطابق" value={details.apartment} />
<DetailRow label="الدولة" value={details.country} />
</SectionCard>
{/* Contact person — only when present */}
{(contactPerson?.name || contactPerson?.phone) && (
<SectionCard title="جهة الاتصال" icon="👤">
<DetailRow label="الاسم" value={contactPerson.name} />
<DetailRow label="رقم الهاتف" value={contactPerson.phone} ltr />
</SectionCard>
)}
{/* Coordinates */}
{location?.coordinates && (
<SectionCard title="الإحداثيات الجغرافية" icon="📡">
<DetailRow
label="خط الطول (Longitude)"
value={String(location.coordinates[0])}
ltr
/>
<DetailRow
label="خط العرض (Latitude)"
value={String(location.coordinates[1])}
ltr
/>
</SectionCard>
)}
{/* System metadata */}
<SectionCard title="معلومات النظام" icon="🕐">
<DetailRow
label="تاريخ الإنشاء"
value={new Date(address.createdAt).toLocaleString("ar-SA", {
dateStyle: "long",
timeStyle: "short",
})}
/>
<DetailRow
label="آخر تحديث"
value={new Date(address.updatedAt).toLocaleString("ar-SA", {
dateStyle: "long",
timeStyle: "short",
})}
/>
</SectionCard>
</div>
);
}

View File

@@ -10,7 +10,7 @@ import {
type CreateAddressFormValues,
type UpdateAddressFormValues,
} from "@/src/validations/client_address.validator";
import type { ClientAddress } from "@/src/types/client";
import type { ClientAddress } from "@/src/types/client_adresses";
// ── Styles ─────────────────────────────────────────────────────────────────
const S = {
@@ -50,12 +50,9 @@ const S = {
} as React.CSSProperties,
};
// FIX 2: Use only the `border` shorthand in both branches — no borderColor/border conflict
const withError = (hasError: boolean): React.CSSProperties => ({
...S.input,
border: hasError
? "1px solid var(--color-danger)"
: "1px solid var(--color-border)",
border: hasError ? "1px solid var(--color-danger)" : "1px solid var(--color-border)",
background: hasError ? "#FEF2F2" : S.input.background,
});
@@ -63,39 +60,27 @@ const withError = (hasError: boolean): React.CSSProperties => ({
interface AddressFormModalProps {
editAddress: ClientAddress | null;
onClose: () => void;
// FIX: removed the "isNew" second argument. The parent (page.tsx) now
// figures out create-vs-update itself from addrFormTarget, so it no longer
// needs us to tell it isNew. This avoids the two values (isNew here vs
// addrFormTarget there) ever disagreeing with each other.
onSubmit: (
data: CreateAddressFormValues | UpdateAddressFormValues
) => Promise<boolean>;
onSubmit: (data: CreateAddressFormValues | UpdateAddressFormValues) => Promise<boolean>;
}
// ── Component ──────────────────────────────────────────────────────────────
export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressFormModalProps) {
const isNew = editAddress === null;
// NOTE: "schema" variable removed — resolver now picks createAddressSchema
// or updateAddressSchema directly inline (see useForm below).
const {
register,
handleSubmit,
setError,
formState: { errors, isSubmitting },
} = useForm<CreateAddressFormValues | UpdateAddressFormValues>({
// FIX: "as never" was hiding all type errors on resolver/errors.
// yupResolver only works with ONE concrete schema type, not a union,
// so we still need a cast — but cast to the real Resolver type instead
// of "never", so formState.errors keeps proper typing.
resolver: (isNew ? yupResolver(createAddressSchema) : yupResolver(updateAddressSchema)) as Resolver<
CreateAddressFormValues | UpdateAddressFormValues
>,
resolver: (isNew
? yupResolver(createAddressSchema)
: yupResolver(updateAddressSchema)
) as Resolver<CreateAddressFormValues | UpdateAddressFormValues>,
defaultValues: {
label: editAddress?.label ?? "",
branchName: editAddress?.branchName ?? "",
isPrimary: editAddress?.isPrimary ?? false,
contactPerson: {
name: editAddress?.contactPerson?.name ?? "",
@@ -121,9 +106,9 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
},
});
const detailsErr = (errors as never as Record<string, Record<string, { message?: string }>>)?.details ?? {};
const contactErr = (errors as never as Record<string, Record<string, { message?: string }>>)?.contactPerson ?? {};
const locationErr = (errors as never as Record<string, Record<string, { message?: string }>>)?.location ?? {};
const detailsErr = (errors as any)?.details ?? {};
const contactErr = (errors as any)?.contactPerson ?? {};
const locationErr = (errors as any)?.location ?? {};
useEffect(() => {
const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); };
@@ -132,7 +117,7 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
}, [onClose]);
const submitHandler = async (data: CreateAddressFormValues | UpdateAddressFormValues) => {
const ok = await onSubmit(data); // FIX: no more isNew arg, see prop type above
const ok = await onSubmit(data);
if (ok) {
onClose();
} else {
@@ -173,7 +158,7 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
flexDirection: "column",
}}
>
{/* Header — pinned */}
{/* Header */}
<div
style={{
flexShrink: 0,
@@ -186,26 +171,12 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
}}
>
<div>
<p
style={{
fontSize: 11,
letterSpacing: "0.3em",
textTransform: "uppercase",
color: "#2563EB",
fontWeight: 600,
margin: 0,
}}
>
<p style={{ fontSize: 11, letterSpacing: "0.3em", textTransform: "uppercase", color: "#2563EB", fontWeight: 600, margin: 0 }}>
{isNew ? "إضافة عنوان" : "تعديل عنوان"}
</p>
<h2
id="addr-modal-title"
style={{
fontSize: 17,
fontWeight: 700,
color: "var(--color-text-primary)",
margin: "4px 0 0",
}}
style={{ fontSize: 17, fontWeight: 700, color: "var(--color-text-primary)", margin: "4px 0 0" }}
>
{isNew ? "عنوان جديد" : editAddress?.label}
</h2>
@@ -237,12 +208,7 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
<form
onSubmit={handleSubmit(submitHandler)}
noValidate
style={{
padding: "1.5rem",
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
style={{ padding: "1.5rem", display: "flex", flexDirection: "column", gap: "1rem" }}
>
{errors.label?.type === "manual" && (
<Alert type="error" message={errors.label.message ?? ""} onClose={() => {}} />
@@ -251,58 +217,22 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
{/* Address Meta */}
<p style={S.sectionTitle}>بيانات العنوان</p>
<div
style={{
display: "grid",
gridTemplateColumns: "1fr auto",
gap: "0.75rem",
alignItems: "end",
}}
>
{/* FIX 3: Replaced <select> with a free-text <input> */}
<label style={S.label}>
نوع العنوان
<input
{...register("label")}
style={withError(!!errors.label && errors.label.type !== "manual")}
placeholder="مقترح: منزل / مكتب"
dir="rtl"
/>
{errors.label && errors.label.type !== "manual" && (
<span style={S.errorText}>{errors.label.message}</span>
)}
</label>
<label
style={{
display: "flex",
alignItems: "center",
gap: 8,
fontSize: 12,
fontWeight: 600,
color: "var(--color-text-secondary)",
paddingBottom: 2,
cursor: "pointer",
whiteSpace: "nowrap",
}}
>
<input
type="checkbox"
{...register("isPrimary")}
style={{ width: 14, height: 14, cursor: "pointer" }}
/>
عنوان رئيسي
</label>
</div>
<label style={S.label}>
نوع العنوان
<input
{...register("label")}
style={withError(!!errors.label && errors.label.type !== "manual")}
placeholder="مثال: منزل / مكتب / شحن"
dir="rtl"
/>
{errors.label && errors.label.type !== "manual" && (
<span style={S.errorText}>{errors.label.message}</span>
)}
</label>
<label style={S.label}>
اسم الفرع (اختياري)
<input
{...register("branchName")}
style={S.input}
placeholder="فرع الرياض"
dir="rtl"
/>
<input {...register("branchName")} style={S.input} placeholder="فرع الرياض" dir="rtl" />
</label>
{/* Address Details */}
@@ -316,119 +246,60 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
placeholder="شارع الملك فهد، مبنى 12"
dir="rtl"
/>
{detailsErr.street && (
<span style={S.errorText}>{detailsErr.street.message}</span>
)}
{detailsErr.street && <span style={S.errorText}>{detailsErr.street.message}</span>}
</label>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={S.label}>
المدينة {isNew && "*"}
<input
{...register("details.city")}
style={withError(!!detailsErr.city)}
placeholder="الرياض"
dir="rtl"
/>
{detailsErr.city && (
<span style={S.errorText}>{detailsErr.city.message}</span>
)}
<input {...register("details.city")} style={withError(!!detailsErr.city)} placeholder="الرياض" dir="rtl" />
{detailsErr.city && <span style={S.errorText}>{detailsErr.city.message}</span>}
</label>
<label style={S.label}>
المنطقة
<input
{...register("details.state")}
style={withError(!!detailsErr.state)}
placeholder="منطقة الرياض"
dir="rtl"
/>
{detailsErr.state && (
<span style={S.errorText}>{detailsErr.state.message}</span>
)}
<input {...register("details.state")} style={withError(!!detailsErr.state)} placeholder="منطقة الرياض" dir="rtl" />
{detailsErr.state && <span style={S.errorText}>{detailsErr.state.message}</span>}
</label>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={S.label}>
الحي (اختياري)
<input
{...register("details.district")}
style={S.input}
placeholder="حي العليا"
dir="rtl"
/>
<input {...register("details.district")} style={S.input} placeholder="حي العليا" dir="rtl" />
</label>
<label style={S.label}>
رقم المبنى (اختياري)
<input
{...register("details.buildingNo")}
style={S.input}
placeholder="1234"
dir="ltr"
/>
<input {...register("details.buildingNo")} style={S.input} placeholder="1234" dir="ltr" />
</label>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={S.label}>
رقم الوحدة (اختياري)
<input
{...register("details.unitNo")}
style={S.input}
placeholder="5678"
dir="ltr"
/>
<input {...register("details.unitNo")} style={S.input} placeholder="5678" dir="ltr" />
</label>
<label style={S.label}>
الرقم الإضافي (اختياري)
<input
{...register("details.additionalNo")}
style={S.input}
placeholder="0000"
dir="ltr"
/>
<input {...register("details.additionalNo")} style={S.input} placeholder="0000" dir="ltr" />
</label>
</div>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={S.label}>
الرمز البريدي (اختياري)
<input
{...register("details.zipCode")}
style={withError(!!detailsErr.zipCode)}
placeholder="11564"
dir="ltr"
/>
{detailsErr.zipCode && (
<span style={S.errorText}>{detailsErr.zipCode.message}</span>
)}
<input {...register("details.zipCode")} style={withError(!!detailsErr.zipCode)} placeholder="11564" dir="ltr" />
{detailsErr.zipCode && <span style={S.errorText}>{detailsErr.zipCode.message}</span>}
</label>
<label style={S.label}>
الدولة
<input
{...register("details.country")}
style={withError(!!detailsErr.country)}
placeholder="SA"
dir="ltr"
/>
{detailsErr.country && (
<span style={S.errorText}>{detailsErr.country.message}</span>
)}
<input {...register("details.country")} style={withError(!!detailsErr.country)} placeholder="SA" dir="ltr" />
{detailsErr.country && <span style={S.errorText}>{detailsErr.country.message}</span>}
</label>
</div>
<label style={S.label}>
الشقة / الطابق (اختياري)
<input
{...register("details.apartment")}
style={S.input}
placeholder="الطابق الثالث"
dir="rtl"
/>
<input {...register("details.apartment")} style={S.input} placeholder="الطابق الثالث" dir="rtl" />
</label>
{/* Contact Person */}
@@ -437,29 +308,13 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={S.label}>
الاسم
<input
{...register("contactPerson.name")}
style={withError(!!contactErr.name)}
placeholder="أحمد محمد"
dir="rtl"
/>
{contactErr.name && (
<span style={S.errorText}>{contactErr.name.message}</span>
)}
<input {...register("contactPerson.name")} style={withError(!!contactErr.name)} placeholder="أحمد محمد" dir="rtl" />
{contactErr.name && <span style={S.errorText}>{contactErr.name.message}</span>}
</label>
<label style={S.label}>
رقم الهاتف
<input
{...register("contactPerson.phone")}
style={withError(!!contactErr.phone)}
type="tel"
placeholder="05xxxxxxxx"
dir="ltr"
/>
{contactErr.phone && (
<span style={S.errorText}>{contactErr.phone.message}</span>
)}
<input {...register("contactPerson.phone")} style={withError(!!contactErr.phone)} type="tel" placeholder="05xxxxxxxx" dir="ltr" />
{contactErr.phone && <span style={S.errorText}>{contactErr.phone.message}</span>}
</label>
</div>
@@ -469,41 +324,17 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "0.75rem" }}>
<label style={S.label}>
خط الطول (Longitude) {isNew && "*"}
<input
{...register("location.coordinates.0" as never)}
style={withError(!!locationErr.coordinates)}
type="number"
step="any"
placeholder="46.6753"
dir="ltr"
/>
<input {...register("location.coordinates.0" as never)} style={withError(!!locationErr.coordinates)} type="number" step="any" placeholder="46.6753" dir="ltr" />
</label>
<label style={S.label}>
خط العرض (Latitude) {isNew && "*"}
<input
{...register("location.coordinates.1" as never)}
style={withError(!!locationErr.coordinates)}
type="number"
step="any"
placeholder="24.7136"
dir="ltr"
/>
{locationErr.coordinates && (
<span style={S.errorText}>{locationErr.coordinates.message}</span>
)}
<input {...register("location.coordinates.1" as never)} style={withError(!!locationErr.coordinates)} type="number" step="any" placeholder="24.7136" dir="ltr" />
{locationErr.coordinates && <span style={S.errorText}>{locationErr.coordinates.message}</span>}
</label>
</div>
{/* Actions */}
<div
style={{
display: "flex",
gap: "0.5rem",
justifyContent: "flex-end",
paddingTop: "0.5rem",
}}
>
<div style={{ display: "flex", gap: "0.5rem", justifyContent: "flex-end", paddingTop: "0.5rem" }}>
<button
type="button"
onClick={onClose}
@@ -531,9 +362,7 @@ export function AddressFormModal({ editAddress, onClose, onSubmit }: AddressForm
padding: "0 1.5rem",
borderRadius: "var(--radius-md)",
border: "none",
background: isSubmitting
? "var(--color-brand-400)"
: "var(--color-brand-600)",
background: isSubmitting ? "var(--color-brand-400)" : "var(--color-brand-600)",
fontSize: 13,
fontWeight: 700,
color: "#FFF",