update order pages and create client pages also client adresses pages

This commit is contained in:
m7amedez5511
2026-06-25 21:34:31 +03:00
parent a18ed59ac1
commit 0847bd23ee
18 changed files with 2619 additions and 984 deletions

View File

@@ -1,23 +1,36 @@
"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Alert } from "@/src/Components/UI";
import { Toast } from "@/src/Components/Client/Toast";
// ── UI components (canonical barrel — no old Client/Toast) ─────────────────
import { Alert, Toast } from "@/src/Components/UI";
// ── Hooks & services ───────────────────────────────────────────────────────
import { useClientAddresses } from "@/src/hooks/useClientAddresses";
import { clientService } from "@/src/services/client.service";
import { getStoredToken } from "@/src/lib/auth";
// ── Types ──────────────────────────────────────────────────────────────────
import type {
Client,
ClientAddress,
ClientAddressFormData,
ClientFormData,
} from "@/src/types/client";
import { AddressFormModal } from "@/src/Components/Client/Addressformmodal";
import { DeleteConfirmModal } from "@/src/Components/Client/Deleteconfirmmodal";
// ── Helpers ────────────────────────────────────────────────────────────────
// ── Client-specific modals ─────────────────────────────────────────────────
import { AddressFormModal,DeleteConfirmModal ,ClientFormModal } from "@/src/Components/Client";
import { CreateAddressFormValues, UpdateAddressFormValues } from "@/src/validations/client_address.validator";
/** Returns a context-appropriate emoji for each common address label */
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
/** Emoji icon for known address label types */
function labelIcon(label: string): string {
const map: Record<string, string> = {
"فوترة": "💳",
@@ -34,16 +47,29 @@ function labelIcon(label: string): string {
return map[label.toLowerCase()] ?? "📍";
}
// ── Address card sub-component ─────────────────────────────────────────────
// Mirrors the AddressCard in the old page — kept co-located because it's only used here.
// ─────────────────────────────────────────────────────────────────────────────
// AddressCard sub-component
// ─────────────────────────────────────────────────────────────────────────────
interface AddressCardProps {
address: ClientAddress;
onEdit: () => void;
onDelete: () => void;
onMakePrimary: () => void;
/** Whether a "set primary" action is pending for this specific card */
settingPrimary?: boolean;
}
function AddressCard({ address, onEdit, onDelete, onMakePrimary }: AddressCardProps) {
/**
* AddressCard — Renders a single address in the grid.
*/
function AddressCard({
address,
onEdit,
onDelete,
onMakePrimary,
settingPrimary = false,
}: AddressCardProps) {
return (
<div
style={{
@@ -60,18 +86,34 @@ function AddressCard({ address, onEdit, onDelete, onMakePrimary }: AddressCardPr
boxShadow: address.isPrimary
? "0 0 0 2px #BFDBFE, var(--shadow-card)"
: "var(--shadow-card)",
transition: "box-shadow 200ms",
}}
>
{/* Label row */}
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span style={{ fontSize: 20 }} aria-hidden="true">
{labelIcon(address.label)}
</span>
<span style={{ fontSize: 13, fontWeight: 700, color: "var(--color-text-primary)" }}>
<span
style={{
fontSize: 13,
fontWeight: 700,
color: "var(--color-text-primary)",
}}
>
{address.label}
</span>
</div>
{/* Primary badge */}
{address.isPrimary && (
<span
style={{
@@ -82,9 +124,10 @@ function AddressCard({ address, onEdit, onDelete, onMakePrimary }: AddressCardPr
border: "1px solid #BFDBFE",
borderRadius: "var(--radius-full)",
padding: "0.15rem 0.5rem",
flexShrink: 0,
}}
>
رئيسي
رئيسي
</span>
)}
</div>
@@ -101,7 +144,7 @@ function AddressCard({ address, onEdit, onDelete, onMakePrimary }: AddressCardPr
>
<p style={{ margin: 0 }}>{address.street}</p>
<p style={{ margin: 0 }}>
{address.city}، {address.state} {address.postalCode}
{address.city}،{" "}{address.state} {address.postalCode}
</p>
<p style={{ margin: 0 }}>{address.country}</p>
</address>
@@ -117,18 +160,28 @@ function AddressCard({ address, onEdit, onDelete, onMakePrimary }: AddressCardPr
borderTop: "1px solid var(--color-border)",
}}
>
<ActionBtn onClick={onEdit} color="#1D4ED8" bg="#EFF6FF" border="#BFDBFE">
<ActionBtn
onClick={onEdit}
color="#1D4ED8"
bg="#EFF6FF"
border="#BFDBFE"
>
تعديل
</ActionBtn>
{/*
* Set-primary button — only for non-primary addresses.
* Shows a loading indicator while the API call is in-flight.
*/}
{!address.isPrimary && (
<ActionBtn
onClick={onMakePrimary}
color="#065F46"
bg="#DCFCE7"
border="#BBF7D0"
disabled={settingPrimary}
>
تعيين كرئيسي
{settingPrimary ? "…" : "تعيين كرئيسي"}
</ActionBtn>
)}
@@ -152,6 +205,7 @@ function ActionBtn({
bg,
border,
style,
disabled,
children,
}: {
onClick: () => void;
@@ -159,12 +213,14 @@ function ActionBtn({
bg: string;
border: string;
style?: React.CSSProperties;
disabled?: boolean;
children: React.ReactNode;
}) {
return (
<button
type="button"
onClick={onClick}
disabled={disabled}
style={{
height: 30,
padding: "0 0.75rem",
@@ -174,8 +230,10 @@ function ActionBtn({
fontSize: 12,
fontWeight: 600,
color,
cursor: "pointer",
cursor: disabled ? "not-allowed" : "pointer",
opacity: disabled ? 0.6 : 1,
fontFamily: "var(--font-sans)",
transition: "opacity 150ms",
...style,
}}
>
@@ -184,25 +242,303 @@ function ActionBtn({
);
}
// ── Page ───────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────
// AddressMiniList — used inside the client edit modal
// ─────────────────────────────────────────────────────────────────────────────
/**
* AddressMiniList — A compact read-only list of addresses shown inside
* the ClientFormModal when editing an existing client.
*
* Purpose:
* Allows the user to see all addresses and designate one as primary
* without navigating away from the edit modal.
*
* Props:
* addresses — current list from useClientAddresses
* onMakePrimary — handler that calls clientAddressService.setPrimary
* settingId — id of the address currently being promoted (for loading state)
*/
function AddressMiniList({
addresses,
onMakePrimary,
settingId,
}: {
addresses: ClientAddress[];
onMakePrimary: (id: string) => void;
settingId: string | null;
}) {
if (addresses.length === 0) {
return (
<div
style={{
borderRadius: "var(--radius-lg)",
border: "1px dashed var(--color-border)",
padding: "1rem",
textAlign: "center",
fontSize: 12,
color: "var(--color-text-muted)",
}}
>
لا توجد عناوين مضافة بعد.
</div>
);
}
return (
<ul
dir="rtl"
style={{ listStyle: "none", margin: 0, padding: 0, display: "flex", flexDirection: "column", gap: 8 }}
>
{addresses.map((addr) => (
<li
key={addr.id}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
padding: "0.625rem 0.875rem",
borderRadius: "var(--radius-md)",
border: addr.isPrimary ? "1px solid #93C5FD" : "1px solid var(--color-border)",
background: addr.isPrimary ? "#EFF6FF" : "var(--color-surface-muted)",
fontSize: 12,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span aria-hidden="true">{labelIcon(addr.label)}</span>
<div>
<span style={{ fontWeight: 600, color: "var(--color-text-primary)" }}>
{addr.label}
</span>
<span style={{ color: "var(--color-text-muted)", marginRight: 8 }}>
{addr.city}، {addr.state}
</span>
</div>
</div>
{addr.isPrimary ? (
<span
style={{
fontSize: 10,
fontWeight: 700,
color: "#1D4ED8",
background: "#DBEAFE",
border: "1px solid #BFDBFE",
borderRadius: "var(--radius-full)",
padding: "0.1rem 0.45rem",
flexShrink: 0,
}}
>
رئيسي
</span>
) : (
<button
type="button"
onClick={() => onMakePrimary(addr.id)}
disabled={settingId === addr.id}
style={{
height: 24,
padding: "0 0.5rem",
borderRadius: "var(--radius-md)",
border: "1px solid #BBF7D0",
background: "#DCFCE7",
fontSize: 11,
fontWeight: 600,
color: "#065F46",
cursor: settingId === addr.id ? "not-allowed" : "pointer",
opacity: settingId === addr.id ? 0.6 : 1,
fontFamily: "var(--font-sans)",
flexShrink: 0,
}}
>
{settingId === addr.id ? "…" : "تعيين كرئيسي"}
</button>
)}
</li>
))}
</ul>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// ClientEditModalWithAddresses — wraps ClientFormModal + AddressMiniList
// ─────────────────────────────────────────────────────────────────────────────
/**
* ClientEditModalWithAddresses
*/
interface ClientEditModalWithAddressesProps {
client: Client;
addresses: ClientAddress[];
onClose: () => void;
onSubmit: (data: ClientFormData, isNew: boolean) => Promise<boolean>;
onMakePrimary: (id: string) => Promise<boolean>;
}
function ClientEditModalWithAddresses({
client,
addresses,
onClose,
onSubmit,
onMakePrimary,
}: ClientEditModalWithAddressesProps) {
const [settingId, setSettingId] = useState<string | null>(null);
const handleSetPrimary = async (id: string) => {
setSettingId(id);
await onMakePrimary(id);
setSettingId(null);
};
return (
// We reuse the backdrop and center the compound modal manually
<div
role="dialog"
aria-modal="true"
aria-label="تعديل بيانات العميل"
onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}
style={{
position: "fixed",
inset: 0,
zIndex: 50,
background: "rgba(15,23,42,0.55)",
backdropFilter: "blur(4px)",
display: "flex",
alignItems: "flex-start",
justifyContent: "center",
padding: "2rem 1rem",
overflowY: "auto",
gap: "1rem",
}}
>
{/* Left panel: client form */}
<div
onClick={(e) => e.stopPropagation()}
style={{
width: "100%",
maxWidth: 520,
display: "flex",
flexDirection: "column",
gap: "1rem",
}}
>
{/* Render ClientFormModal in a "portal-less" way by embedding its JSX inline */}
<div
style={{
background: "var(--color-surface)",
borderRadius: "var(--radius-2xl)",
border: "1px solid var(--color-border)",
boxShadow: "0 24px 64px rgba(0,0,0,.18)",
overflow: "hidden",
}}
>
{/* We mount ClientFormModal as a standalone dialog — it handles its own
form logic. We intercept onClose to close the whole compound modal. */}
<ClientFormModal
editClient={client}
onClose={onClose}
onSubmit={onSubmit}
/>
</div>
{/*
* Address section — appended below the form card.
* Satisfies: "create a section that displays all available addresses."
*/}
<div
style={{
background: "var(--color-surface)",
borderRadius: "var(--radius-xl)",
border: "1px solid var(--color-border)",
boxShadow: "var(--shadow-card)",
overflow: "hidden",
}}
>
{/* Section header */}
<div
style={{
padding: "0.875rem 1.25rem",
borderBottom: "1px solid var(--color-border)",
background: "var(--color-surface-muted)",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div>
<p
style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: "0.25em",
textTransform: "uppercase",
color: "#2563EB",
margin: 0,
}}
>
عناوين العميل
</p>
<p
style={{
margin: "2px 0 0",
fontSize: 12,
color: "var(--color-text-muted)",
}}
>
{addresses.length} عنوان اضغط "تعيين كرئيسي" لتغيير العنوان الرئيسي
</p>
</div>
</div>
{/* Address mini-list */}
<div style={{ padding: "1rem" }}>
<AddressMiniList
addresses={addresses}
onMakePrimary={handleSetPrimary}
settingId={settingId}
/>
</div>
</div>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────────────────────
// Page component
// ─────────────────────────────────────────────────────────────────────────────
export default function ClientAddressesPage() {
const params = useParams();
const router = useRouter();
const clientId = params?.clientId as string;
console.log('clientId:', params.clientId); // شوف إيه اللي بيجي
// FIX: "id is undefined" 404 bug.
// params.clientId only works if your route folder is named [clientId].
// If your folder is actually [id], params.clientId is always undefined,
// which becomes the string "undefined" in API URLs like /client/undefined/addresses.
// CHECK YOUR FOLDER NAME and keep only the line that matches it:
const clientId = (params?.clientId ?? params?.id) as string | undefined;
// FIX: warn loudly in dev instead of silently calling the API with no id
useEffect(() => {
if (!clientId) {
console.warn("ClientAddressesPage: clientId is missing from route params. Check your folder name (must match the key used in useParams()).");
}
}, [clientId]);
// ── Parent client meta ───────────────────────────────────────────────────
const [client, setClient] = useState<Client | null>(null);
const [client, setClient] = useState<Client | null>(null);
const [clientLoading, setClientLoading] = useState(true);
// Fetch the parent client so we can show their name / details in the header
useEffect(() => {
if (!clientId) return;
setClientLoading(true);
clientService
.getById(clientId, getStoredToken())
.then((res) => setClient(res.data))
.catch(() => router.replace("/clients"))
.catch(() => router.replace("/dashboard/clients"))
.finally(() => setClientLoading(false));
}, [clientId, router]);
@@ -211,29 +547,55 @@ export default function ClientAddressesPage() {
addresses,
loading: addrLoading,
error,
notification,
notification, // { type, message } — matches ToastNotification
clearError,
createAddress,
updateAddress,
deleteAddress,
makePrimary,
reload,
} = useClientAddresses(clientId ?? "");
// NOTE: hook itself guards "if (!clientId) return" before calling the API,
// so passing "" here is safe — it just won't fetch until clientId is real.
// ── Modal state (mirrors UsersPage pattern) ──────────────────────────────
// false = closed | null = create mode | ClientAddress = edit mode
const [formTarget, setFormTarget] = useState<ClientAddress | null | false>(false);
const [deleteTarget, setDeleteTarget] = useState<ClientAddress | null>(null);
const [deleting, setDeleting] = useState(false);
// ── Modal state ──────────────────────────────────────────────────────────
// Address form: false = closed | null = create | ClientAddress = edit
const [addrFormTarget, setAddrFormTarget] = useState<ClientAddress | null | false>(false);
const [deleteTarget, setDeleteTarget] = useState<ClientAddress | null>(null);
const [deleting, setDeleting] = useState(false);
// ── Handlers ─────────────────────────────────────────────────────────────
const handleFormSubmit = async (
data: ClientAddressFormData,
isNew: boolean
// Client edit modal (with address section)
const [editingClient, setEditingClient] = useState(false);
// Per-card primary-setting loading state (card id)
const [settingPrimaryId, setSettingPrimaryId] = useState<string | null>(null);
// ── Address form handler ─────────────────────────────────────────────────
// FIX: "addrFormTarget!.id" used "!" which only blocks null/undefined,
// NOT "false" — but addrFormTarget's type is "ClientAddress | null | false".
// If addrFormTarget was ever false here, ".id" would be undefined at runtime,
// causing the "id is undefined" 404 bug from the API call.
// We now check addrFormTarget directly instead of trusting the isNew param,
// so create vs update can never disagree with each other.
const handleAddressSubmit = async (
data: CreateAddressFormValues | UpdateAddressFormValues
): Promise<boolean> => {
if (isNew) return createAddress(data);
return updateAddress((formTarget as ClientAddress).id, data);
if (addrFormTarget === null) {
// create mode
return createAddress(data as CreateAddressFormValues);
}
if (addrFormTarget === false) {
// modal isn't even open — this should never happen, but guard anyway
console.error("handleAddressSubmit called while addrFormTarget is false (modal closed)");
return false;
}
// update mode — addrFormTarget is a real ClientAddress here, .id is safe
return updateAddress(addrFormTarget.id, data as UpdateAddressFormValues);
};
// ── Delete handler ───────────────────────────────────────────────────────
const handleDeleteConfirm = async () => {
if (!deleteTarget || deleting) return;
setDeleting(true);
@@ -242,7 +604,35 @@ export default function ClientAddressesPage() {
if (ok) setDeleteTarget(null);
};
// ── Guard: wait for router + client ─────────────────────────────────────
// ── Set-primary handler ──────────────────────────────────────────────────
/**
* makePrimaryForCard — wraps makePrimary from the hook with per-card
* loading state so each card can show its own spinner independently.
*/
const makePrimaryForCard = async (id: string): Promise<boolean> => {
setSettingPrimaryId(id);
const ok = await makePrimary(id);
setSettingPrimaryId(null);
return ok;
};
// ── Client edit submit ───────────────────────────────────────────────────
const handleClientEditSubmit = async (
data: ClientFormData,
_isNew: boolean,
): Promise<boolean> => {
if (!client) return false;
try {
const token = getStoredToken();
const res = await clientService.update(client.id, data, token);
setClient(res.data); // optimistically update local state
return true;
} catch {
return false;
}
};
// ── Guard: wait for client ───────────────────────────────────────────────
if (!clientId || clientLoading) {
return (
<div
@@ -270,22 +660,29 @@ export default function ClientAddressesPage() {
// ── Render ────────────────────────────────────────────────────────────────
return (
<>
{/* Global success / error toast */}
{/*
* Global toast — sourced from UI barrel (not the old Client/Toast).
* The notification object from useClientAddresses has the same shape
* as ToastNotification: { type: "success" | "error"; message: string }.
*/}
<Toast notification={notification} />
{/* Create / Edit address modal */}
{formTarget !== false && (
{/* ── Address create / edit modal ── */}
{addrFormTarget !== false && (
<AddressFormModal
editAddress={formTarget}
onClose={() => setFormTarget(false)}
onSubmit={handleFormSubmit}
editAddress={addrFormTarget}
onClose={() => setAddrFormTarget(false)}
onSubmit={handleAddressSubmit}
/>
)}
{/* Delete confirmation — reuses the same DeleteConfirmModal shape */}
{/*
* ── Address delete confirmation ──
* We cast the address to the Client shape that DeleteConfirmModal expects
* (it only uses id + name fields from Client).
*/}
{deleteTarget && (
<DeleteConfirmModal
// Cast address as Client for the modal (same shape: id + name)
client={
{
...deleteTarget,
@@ -302,6 +699,21 @@ export default function ClientAddressesPage() {
/>
)}
{/*
* ── Client edit modal with embedded address section ──
* TEST: Click "تعديل بيانات العميل" in the header.
* → Modal shows client form + address list with "تعيين كرئيسي" buttons.
*/}
{editingClient && client && (
<ClientEditModalWithAddresses
client={client}
addresses={addresses}
onClose={() => setEditingClient(false)}
onSubmit={handleClientEditSubmit}
onMakePrimary={makePrimaryForCard}
/>
)}
<section style={{ display: "flex", flexDirection: "column", gap: "1.5rem" }}>
{/* ── Page header ── */}
@@ -317,7 +729,7 @@ export default function ClientAddressesPage() {
{/* Back link */}
<button
type="button"
onClick={() => router.push("/clients")}
onClick={() => router.push("/dashboard/clients")}
style={{
display: "inline-flex",
alignItems: "center",
@@ -332,7 +744,14 @@ export default function ClientAddressesPage() {
fontFamily: "var(--font-sans)",
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M15 18l-6-6 6-6" />
</svg>
جميع العملاء
@@ -349,6 +768,7 @@ export default function ClientAddressesPage() {
>
إدارة العناوين
</p>
<div className="mt-3 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1
@@ -376,34 +796,80 @@ export default function ClientAddressesPage() {
</p>
</div>
{/* Add address button */}
<button
type="button"
onClick={() => setFormTarget(null)}
style={{
height: 40,
padding: "0 1.125rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
fontSize: 13,
fontWeight: 700,
color: "#FFF",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 7,
fontFamily: "var(--font-sans)",
boxShadow: "0 1px 4px rgba(37,99,235,.35)",
whiteSpace: "nowrap",
}}
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
إضافة عنوان
</button>
{/* Header action buttons */}
<div style={{ display: "flex", gap: "0.5rem", flexWrap: "wrap" }}>
{/* Edit client (opens compound modal with address section) */}
{client && (
<button
type="button"
onClick={() => setEditingClient(true)}
style={{
height: 40,
padding: "0 1rem",
borderRadius: "var(--radius-lg)",
border: "1px solid var(--color-border)",
background: "var(--color-surface)",
fontSize: 13,
fontWeight: 600,
color: "var(--color-text-secondary)",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 7,
fontFamily: "var(--font-sans)",
}}
>
<svg
width="13"
height="13"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
</svg>
تعديل بيانات العميل
</button>
)}
{/* Add address button */}
<button
type="button"
onClick={() => setAddrFormTarget(null)}
style={{
height: 40,
padding: "0 1.125rem",
borderRadius: "var(--radius-lg)",
border: "none",
background: "var(--color-brand-600)",
fontSize: 13,
fontWeight: 700,
color: "#FFF",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 7,
fontFamily: "var(--font-sans)",
boxShadow: "0 1px 4px rgba(37,99,235,.35)",
whiteSpace: "nowrap",
}}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
إضافة عنوان
</button>
</div>
</div>
{/* Client info strip */}
@@ -421,7 +887,12 @@ export default function ClientAddressesPage() {
borderTop: "1px solid var(--color-border)",
}}
>
<span style={{ fontWeight: 600, color: "var(--color-text-primary)" }}>
<span
style={{
fontWeight: 600,
color: "var(--color-text-primary)",
}}
>
{client.name}
</span>
<a
@@ -448,11 +919,18 @@ export default function ClientAddressesPage() {
)}
</header>
{/* General load error */}
{error && <Alert type="error" message={error} onClose={clearError} />}
{/*
* Error alert — shown when the address load fails.
* Uses the UI/Alert component with an onClose dismiss handler.
* TEST: Simulate a network error → alert banner appears above the grid.
*/}
{error && (
<Alert type="error" message={error} onClose={clearError} />
)}
{/* Address grid */}
{/* ── Address grid ── */}
{addrLoading ? (
/* Loading skeleton-ish state */
<div
style={{
display: "flex",
@@ -460,12 +938,25 @@ export default function ClientAddressesPage() {
justifyContent: "center",
padding: "4rem 0",
color: "var(--color-text-muted)",
gap: 12,
fontSize: 13,
}}
>
جارٍ التحميل
<div
style={{
width: 20,
height: 20,
borderRadius: "50%",
border: "2px solid var(--color-border)",
borderTopColor: "var(--color-brand-600)",
animation: "spin 0.7s linear infinite",
flexShrink: 0,
}}
/>
جارٍ تحميل العناوين
</div>
) : addresses.length === 0 ? (
/* Empty state */
<div
style={{
borderRadius: "var(--radius-xl)",
@@ -498,7 +989,7 @@ export default function ClientAddressesPage() {
</p>
<button
type="button"
onClick={() => setFormTarget(null)}
onClick={() => setAddrFormTarget(null)}
style={{
marginTop: 16,
fontSize: 13,
@@ -514,6 +1005,14 @@ export default function ClientAddressesPage() {
</button>
</div>
) : (
/*
* Address cards grid.
* TEST:
* - Click "تعديل" → AddressFormModal opens pre-filled.
* - Click "تعيين كرئيسي" → button shows "…" loading, then card re-renders
* with "★ رئيسي" badge and blue ring; previously primary card loses the badge.
* - Click "حذف" → DeleteConfirmModal opens → confirm → card removed, toast shown.
*/
<div
style={{
display: "grid",
@@ -525,9 +1024,10 @@ export default function ClientAddressesPage() {
<AddressCard
key={addr.id}
address={addr}
onEdit={() => setFormTarget(addr)}
onEdit={() => setAddrFormTarget(addr)}
onDelete={() => setDeleteTarget(addr)}
onMakePrimary={() => makePrimary(addr.id)}
onMakePrimary={() => makePrimaryForCard(addr.id)}
settingPrimary={settingPrimaryId === addr.id}
/>
))}
</div>