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,16 +1,32 @@
"use client";
/**
* ClientTable — Enhanced version
*
* Changes from original:
* 1. Clicking a row now opens an inline ClientDetailPanel (slide-down) showing full client info.
* 2. "Addresses" link inside the detail panel navigates to /dashboard/clients/[clientId]/addresses.
* 3. The row-level onClick still calls onManageAddresses (kept for backwards-compatibility),
* but the primary UX is now the expandable detail panel.
* 4. Active row is highlighted with a brand-tinted left border.
*/
import { useState } from "react";
import { Spinner } from "../UI";
import type { Client } from "@/src/types/client";
// ── Address count badge ────────────────────────────────────────────────────
function AddressBadge({ count }: { count: number }) {
function AddressBadge({ count, onClick }: { count: number; onClick: () => void }) {
const hasAddr = count > 0;
return (
<span
<button
type="button"
onClick={(e) => { e.stopPropagation(); onClick(); }}
title="إدارة العناوين"
style={{
display: "inline-flex",
alignItems: "center",
gap: 4,
borderRadius: "var(--radius-full)",
border: hasAddr ? "1px solid #BFDBFE" : "1px solid var(--color-border)",
background: hasAddr ? "#EFF6FF" : "var(--color-surface-muted)",
@@ -18,10 +34,17 @@ function AddressBadge({ count }: { count: number }) {
fontSize: 11,
fontWeight: 600,
color: hasAddr ? "#1D4ED8" : "var(--color-text-muted)",
cursor: "pointer",
fontFamily: "var(--font-sans)",
transition: "opacity 150ms",
}}
>
{count} {count === 1 ? "عنوان" : "عناوين"}
</span>
{/* Small arrow icon to hint navigation */}
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5">
<path d="M9 18l6-6-6-6" />
</svg>
</button>
);
}
@@ -56,7 +79,7 @@ function StatusBadge({ active }: { active?: boolean }) {
);
}
// ── Icon button (mirrors UserTable's IconBtn) ──────────────────────────────
// ── Icon button ────────────────────────────────────────────────────────────
function IconBtn({
onClick,
title,
@@ -97,6 +120,233 @@ function IconBtn({
);
}
// ── Client detail panel (inline expandable) ────────────────────────────────
/**
* ClientDetailPanel — shown below the clicked row.
* Displays all client fields (name, email, phone, taxId, notes, createdAt)
* and provides a direct link to the addresses sub-page.
*
* @param client The client whose details are rendered.
* @param onGoAddresses Callback that navigates to /dashboard/clients/[id]/addresses.
* @param onEdit Opens the edit modal for this client.
*/
function ClientDetailPanel({
client,
onGoAddresses,
onEdit,
}: {
client: Client;
onGoAddresses: () => void;
onEdit: () => void;
}) {
const infoItem = (label: string, value: React.ReactNode) => (
<div style={{ display: "flex", flexDirection: "column", gap: 2 }}>
<span
style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: "0.15em",
textTransform: "uppercase",
color: "var(--color-text-muted)",
}}
>
{label}
</span>
<span
style={{
fontSize: 13,
color: "var(--color-text-primary)",
fontWeight: 500,
wordBreak: "break-word",
}}
>
{value || <span style={{ color: "var(--color-text-hint)" }}></span>}
</span>
</div>
);
return (
<div
dir="rtl"
style={{
/* Slide-down visual treatment: left accent border in brand blue */
borderRight: "3px solid var(--color-brand-600)",
background: "linear-gradient(135deg, #EFF6FF 0%, #F0F9FF 100%)",
borderBottom: "1px solid #BFDBFE",
padding: "1.25rem 1.5rem",
display: "flex",
flexDirection: "column",
gap: "1rem",
animation: "detailFadeIn 180ms ease",
}}
>
{/* ── Top row: name + quick actions ── */}
<div
style={{
display: "flex",
alignItems: "flex-start",
justifyContent: "space-between",
gap: "1rem",
flexWrap: "wrap",
}}
>
<div>
<h3
style={{
margin: 0,
fontSize: 16,
fontWeight: 700,
color: "var(--color-text-primary)",
}}
>
{client.name}
</h3>
{client.taxId && (
<code
style={{
marginTop: 2,
display: "block",
fontSize: 11,
background: "#DBEAFE",
border: "1px solid #BFDBFE",
borderRadius: "var(--radius-sm)",
padding: "0.1rem 0.4rem",
color: "#1E40AF",
width: "fit-content",
}}
>
{client.taxId}
</code>
)}
</div>
{/* Action buttons */}
<div style={{ display: "flex", gap: "0.5rem", flexShrink: 0 }}>
{/* Edit client */}
<button
type="button"
onClick={(e) => { e.stopPropagation(); onEdit(); }}
style={{
height: 32,
padding: "0 0.875rem",
borderRadius: "var(--radius-md)",
border: "1px solid #BFDBFE",
background: "#EFF6FF",
fontSize: 12,
fontWeight: 600,
color: "#1D4ED8",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 6,
fontFamily: "var(--font-sans)",
}}
>
<svg width="12" height="12" 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>
{/*
* "Addresses" button — KEY FEATURE:
* Navigates to /dashboard/clients/[clientId]/addresses
*/}
<button
type="button"
onClick={(e) => { e.stopPropagation(); onGoAddresses(); }}
style={{
height: 32,
padding: "0 0.875rem",
borderRadius: "var(--radius-md)",
border: "none",
background: "var(--color-brand-600)",
fontSize: 12,
fontWeight: 700,
color: "#FFF",
cursor: "pointer",
display: "inline-flex",
alignItems: "center",
gap: 6,
fontFamily: "var(--font-sans)",
boxShadow: "0 1px 4px rgba(37,99,235,.3)",
}}
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z" />
<circle cx="12" cy="10" r="3" />
</svg>
العناوين
{client.addresses?.length !== undefined && (
<span
style={{
background: "rgba(255,255,255,0.25)",
borderRadius: "var(--radius-full)",
padding: "0 5px",
fontSize: 10,
minWidth: 16,
textAlign: "center",
}}
>
{client.addresses.length}
</span>
)}
</button>
</div>
</div>
{/* ── Info grid ── */}
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))",
gap: "1rem",
paddingTop: "0.75rem",
borderTop: "1px solid #BFDBFE",
}}
>
{infoItem(
"البريد الإلكتروني",
<a
href={`mailto:${client.email}`}
onClick={(e) => e.stopPropagation()}
style={{ color: "#2563EB", textDecoration: "none" }}
>
{client.email}
</a>,
)}
{infoItem("الهاتف", client.phone)}
{infoItem(
"تاريخ الإضافة",
new Date(client.createdAt).toLocaleDateString("ar-SA", {
year: "numeric",
month: "long",
day: "numeric",
}),
)}
{infoItem(
"آخر تحديث",
new Date(client.updatedAt).toLocaleDateString("ar-SA", {
year: "numeric",
month: "long",
day: "numeric",
}),
)}
{client.notes && infoItem("ملاحظات", client.notes)}
</div>
{/* Keyframe animation injected via a style tag (scoped) */}
<style>{`
@keyframes detailFadeIn {
from { opacity: 0; transform: translateY(-6px); }
to { opacity: 1; transform: translateY(0); }
}
`}</style>
</div>
);
}
// ── Shared card & header styles ────────────────────────────────────────────
const cardStyle: React.CSSProperties = {
borderRadius: "var(--radius-xl)",
@@ -119,14 +369,14 @@ const thStyle: React.CSSProperties = {
// ── Props ──────────────────────────────────────────────────────────────────
interface ClientTableProps {
clients: Client[];
loading: boolean;
search: string;
page: number;
pages: number;
onEdit: (client: Client) => void;
onDelete: (client: Client) => void;
onAddFirst: () => void;
clients: Client[];
loading: boolean;
search: string;
page: number;
pages: number;
onEdit: (client: Client) => void;
onDelete: (client: Client) => void;
onAddFirst: () => void;
onPageChange: (p: number) => void;
/** Navigate to address management for a client */
onManageAddresses: (client: Client) => void;
@@ -145,6 +395,17 @@ export function ClientTable({
onPageChange,
onManageAddresses,
}: ClientTableProps) {
/**
* expandedId — tracks which client row is currently expanded.
* Clicking the same row again collapses it (toggle behaviour).
*/
const [expandedId, setExpandedId] = useState<string | null>(null);
const handleRowClick = (client: Client) => {
// Toggle: collapse if already open, expand otherwise
setExpandedId((prev) => (prev === client.id ? null : client.id));
};
return (
<div style={cardStyle}>
{/* Column headers */}
@@ -209,138 +470,202 @@ export function ClientTable({
) : (
/* Data rows */
<ul dir="rtl" style={{ listStyle: "none", margin: 0, padding: 0 }}>
{clients.map((c, i) => (
<li
key={c.id}
style={{
display: "grid",
gridTemplateColumns: "2fr 1.5fr 1.5fr 1fr 1fr 80px",
alignItems: "center",
gap: "0.5rem",
padding: "0.875rem 1.5rem",
borderBottom: "1px solid var(--color-border)",
background:
i % 2 !== 0
? "var(--color-surface-muted)"
: "transparent",
fontSize: 13,
cursor: "pointer",
}}
onClick={() => onManageAddresses(c)}
title={`إدارة عناوين ${c.name}`}
>
{/* Name */}
<div>
<p
{clients.map((c, i) => {
const isExpanded = expandedId === c.id;
return (
<li key={c.id}>
{/* ── Main row ── */}
<div
role="button"
tabIndex={0}
aria-expanded={isExpanded}
aria-label={`${c.name} — انقر لعرض التفاصيل`}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") handleRowClick(c);
}}
onClick={() => handleRowClick(c)}
style={{
fontWeight: 600,
color: "var(--color-text-primary)",
margin: 0,
display: "grid",
gridTemplateColumns: "2fr 1.5fr 1.5fr 1fr 1fr 80px",
alignItems: "center",
gap: "0.5rem",
padding: "0.875rem 1.5rem",
borderBottom: isExpanded
? "none"
: "1px solid var(--color-border)",
/* Highlight the expanded row with a brand-tinted background */
background: isExpanded
? "#EFF6FF"
: i % 2 !== 0
? "var(--color-surface-muted)"
: "transparent",
/* Left accent border when expanded (RTL: border-right in visual) */
borderRight: isExpanded
? "3px solid var(--color-brand-600)"
: "3px solid transparent",
fontSize: 13,
cursor: "pointer",
transition: "background 120ms, border-color 120ms",
outline: "none",
}}
>
{c.name}
</p>
{c.taxId && (
<p
{/* Name */}
<div>
<p
style={{
fontWeight: 600,
color: "var(--color-text-primary)",
margin: 0,
display: "flex",
alignItems: "center",
gap: 6,
}}
>
{c.name}
{/* Chevron indicator — rotates when expanded */}
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
style={{
flexShrink: 0,
color: "var(--color-text-muted)",
transform: isExpanded ? "rotate(90deg)" : "rotate(0deg)",
transition: "transform 200ms",
}}
>
<path d="M9 18l6-6-6-6" />
</svg>
</p>
{c.taxId && (
<p
style={{
marginTop: 2,
fontFamily: "var(--font-mono)",
fontSize: 11,
color: "var(--color-text-muted)",
}}
>
{c.taxId}
</p>
)}
</div>
{/* Email */}
<span
style={{
marginTop: 2,
fontFamily: "var(--font-mono)",
fontSize: 12,
color: "#2563EB",
fontWeight: 600,
}}
>
{c.email}
</span>
{/* Phone */}
<span style={{ color: "var(--color-text-secondary)" }}>
{c.phone}
</span>
{/*
* Address count badge — clicking it navigates directly to
* /dashboard/clients/[clientId]/addresses (bypasses the detail panel).
*/}
<AddressBadge
count={c.addresses?.length ?? 0}
onClick={() => onManageAddresses(c)}
/>
{/* Created at */}
<span
style={{
textAlign: "center",
fontSize: 11,
color: "var(--color-text-muted)",
}}
>
{c.taxId}
</p>
{new Date(c.createdAt).toLocaleDateString("ar-SA", {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{/* Row action buttons */}
<div
style={{
display: "flex",
justifyContent: "center",
gap: 6,
}}
onClick={(e) => e.stopPropagation()}
>
<IconBtn
title={`تعديل ${c.name}`}
color="#1D4ED8"
bg="#EFF6FF"
borderColor="#BFDBFE"
onClick={() => onEdit(c)}
>
<svg
width="14"
height="14"
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>
</IconBtn>
<IconBtn
title={`حذف ${c.name}`}
color="#DC2626"
bg="#FEF2F2"
borderColor="#FECACA"
onClick={() => onDelete(c)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6M14 11v6" />
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
</IconBtn>
</div>
</div>
{/*
* ── Expandable detail panel ──
* Rendered inline below the row when isExpanded is true.
* Contains full client metadata + "العناوين" navigation button.
*
* TEST: Click any row → panel slides in with client details.
* Click "العناوين" → router.push to /dashboard/clients/[id]/addresses.
* Click same row again → panel collapses.
*/}
{isExpanded && (
<ClientDetailPanel
client={c}
onGoAddresses={() => onManageAddresses(c)}
onEdit={() => onEdit(c)}
/>
)}
</div>
{/* Email */}
<span
style={{
fontFamily: "var(--font-mono)",
fontSize: 12,
color: "#2563EB",
fontWeight: 600,
}}
>
{c.email}
</span>
{/* Phone */}
<span style={{ color: "var(--color-text-secondary)" }}>
{c.phone}
</span>
{/* Address count */}
<AddressBadge count={c.addresses?.length ?? 0} />
{/* Created at */}
<span
style={{
textAlign: "center",
fontSize: 11,
color: "var(--color-text-muted)",
}}
>
{new Date(c.createdAt).toLocaleDateString("ar-SA", {
year: "numeric",
month: "short",
day: "numeric",
})}
</span>
{/* Actions */}
<div
style={{
display: "flex",
justifyContent: "center",
gap: 6,
}}
onClick={(e) => e.stopPropagation()} // prevent row navigation
>
<IconBtn
title={`تعديل ${c.name}`}
color="#1D4ED8"
bg="#EFF6FF"
borderColor="#BFDBFE"
onClick={() => onEdit(c)}
>
<svg
width="14"
height="14"
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>
</IconBtn>
<IconBtn
title={`حذف ${c.name}`}
color="#DC2626"
bg="#FEF2F2"
borderColor="#FECACA"
onClick={() => onDelete(c)}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6M14 11v6" />
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
</IconBtn>
</div>
</li>
))}
</li>
);
})}
</ul>
)}