update order pages and create client pages also client adresses pages
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* app/dashboard/clients/page.tsx — Enhanced
|
||||
*
|
||||
* Changes from original:
|
||||
* 1. Replaced `import { Toast } from "@/src/Components/Client/Toast"` with
|
||||
* `import { Toast } from "@/src/Components/UI"` (the canonical, feature-rich version
|
||||
* that supports an optional dismiss button and proper exit animation).
|
||||
* 2. The notification type is now `ToastNotification` from the UI barrel, matching
|
||||
* the Toast's expected prop type.
|
||||
* 3. All other logic (CRUD, routing, modal management) is unchanged.
|
||||
*
|
||||
* HOW TO TEST:
|
||||
* - Add a client → green toast "تم إضافة العميل بنجاح."
|
||||
* - Edit a client → green toast "تم تحديث بيانات العميل."
|
||||
* - Delete a client → green toast "تم حذف العميل بنجاح."
|
||||
* - Click a table row → detail panel expands inline.
|
||||
* - Click "العناوين" inside the panel → navigates to /dashboard/clients/[id]/addresses.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Alert } from "@/src/Components/UI";
|
||||
import { Toast } from "@/src/Components/Client/Toast";
|
||||
import { useClients } from "@/src/hooks/useClients";
|
||||
|
||||
// ── UI components ──────────────────────────────────────────────────────────
|
||||
// NOTE: Toast is now imported from the canonical UI barrel, NOT from Client/Toast.
|
||||
import { Alert, Toast } from "@/src/Components/UI";
|
||||
|
||||
// ── Client-specific components ─────────────────────────────────────────────
|
||||
import { useClients } from "@/src/hooks/useClients";
|
||||
import type { Client, ClientFormData } from "@/src/types/client";
|
||||
import { ClientFormModal } from "@/src/Components/Client/Clientformmodal";
|
||||
import { ClientTable } from "@/src/Components/Client/Clienttable";
|
||||
import { ClientFormModal } from "@/src/Components/Client/Clientformmodal";
|
||||
import { ClientTable } from "@/src/Components/Client/Clienttable";
|
||||
import { DeleteConfirmModal } from "@/src/Components/Client/Deleteconfirmmodal";
|
||||
|
||||
export default function ClientsPage() {
|
||||
@@ -31,7 +54,7 @@ export default function ClientsPage() {
|
||||
// ── Create / Update handler ──────────────────────────────────────────────
|
||||
const handleFormSubmit = async (
|
||||
data: ClientFormData,
|
||||
isNew: boolean
|
||||
isNew: boolean,
|
||||
): Promise<boolean> => {
|
||||
if (isNew) return createClient(data);
|
||||
return updateClient((formTarget as Client).id, data);
|
||||
@@ -47,6 +70,8 @@ export default function ClientsPage() {
|
||||
};
|
||||
|
||||
// ── Navigate to address management ──────────────────────────────────────
|
||||
// This is called both by the AddressBadge click and the "العناوين" button
|
||||
// inside the ClientDetailPanel.
|
||||
const handleManageAddresses = (client: Client) => {
|
||||
router.push(`/dashboard/clients/${client.id}/addresses`);
|
||||
};
|
||||
@@ -54,7 +79,11 @@ export default function ClientsPage() {
|
||||
// ── Render ───────────────────────────────────────────────────────────────
|
||||
return (
|
||||
<>
|
||||
{/* Global success / error toast */}
|
||||
{/*
|
||||
* Global toast notification.
|
||||
* Using the new UI/Toast which accepts ToastNotification | null.
|
||||
* The hook's `notification` shape { type, message } matches ToastNotification exactly.
|
||||
*/}
|
||||
<Toast notification={notification} />
|
||||
|
||||
{/* Create / Edit modal */}
|
||||
|
||||
Reference in New Issue
Block a user