diff --git a/Components/Client/Addressformmodal.tsx b/Components/Client/Addressformmodal.tsx new file mode 100644 index 0000000..2bae2a8 --- /dev/null +++ b/Components/Client/Addressformmodal.tsx @@ -0,0 +1,392 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { Alert, Spinner } from "../UI"; +import type { + ClientAddress, + ClientAddressFormData, + ClientAddressFormErrors, +} from "../../types/client"; + +// ── Fixed styles ─────────────────────────────────────────────────────────── +const S = { + input: { + width: "100%", + height: 40, + padding: "0 0.75rem", + borderRadius: "var(--radius-md)", + border: "1px solid var(--color-border)", + background: "var(--color-surface)", + fontSize: 13, + color: "var(--color-text-primary)", + outline: "none", + fontFamily: "var(--font-sans)", + } as React.CSSProperties, + label: { + display: "flex", + flexDirection: "column" as const, + gap: 6, + fontSize: 12, + fontWeight: 600, + color: "var(--color-text-secondary)", + } as React.CSSProperties, + errorText: { + fontSize: 11, + color: "var(--color-danger)", + fontWeight: 500, + } as React.CSSProperties, +}; + +// ── Validation ───────────────────────────────────────────────────────────── +function validate(data: ClientAddressFormData): ClientAddressFormErrors { + const e: ClientAddressFormErrors = {}; + if (!data.label.trim()) e.label = "النوع مطلوب"; + if (!data.street.trim()) e.street = "العنوان مطلوب"; + if (!data.city.trim()) e.city = "المدينة مطلوبة"; + if (!data.state.trim()) e.state = "المنطقة مطلوبة"; + if (!data.postalCode.trim()) e.postalCode = "الرمز البريدي مطلوب"; + if (!data.country.trim()) e.country = "الدولة مطلوبة"; + return e; +} + +const LABEL_PRESETS = ["فوترة", "شحن", "المقر الرئيسي", "فرع", "مستودع", "أخرى"]; + +// ── Props ────────────────────────────────────────────────────────────────── +interface AddressFormModalProps { + editAddress: ClientAddress | null; // null = create mode + onClose: () => void; + onSubmit: (data: ClientAddressFormData, isNew: boolean) => Promise; +} + +// ── Component ────────────────────────────────────────────────────────────── +export function AddressFormModal({ + editAddress, + onClose, + onSubmit, +}: AddressFormModalProps) { + const isNew = editAddress === null; + + const [form, setForm] = useState({ + label: editAddress?.label ?? "", + street: editAddress?.street ?? "", + city: editAddress?.city ?? "", + state: editAddress?.state ?? "", + postalCode: editAddress?.postalCode ?? "", + country: editAddress?.country ?? "المملكة العربية السعودية", + isPrimary: editAddress?.isPrimary ?? false, + }); + const [errors, setErrors] = useState({}); + const [saving, setSaving] = useState(false); + const [apiError, setApiError] = useState(""); + const firstRef = useRef(null); + + useEffect(() => { firstRef.current?.focus(); }, []); + useEffect(() => { + const handler = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; + window.addEventListener("keydown", handler); + return () => window.removeEventListener("keydown", handler); + }, [onClose]); + + const set = (field: keyof ClientAddressFormData) => + (e: React.ChangeEvent) => { + setForm((p) => ({ ...p, [field]: e.target.value })); + if (errors[field as keyof ClientAddressFormErrors]) + setErrors((p) => ({ ...p, [field]: undefined })); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + const errs = validate(form); + if (Object.keys(errs).length) { setErrors(errs); return; } + setSaving(true); + setApiError(""); + const ok = await onSubmit(form, isNew); + setSaving(false); + if (ok) onClose(); + else setApiError("حدث خطأ غير متوقع. يرجى المحاولة لاحقاً."); + }; + + const inputStyle = (field: keyof ClientAddressFormErrors): React.CSSProperties => ({ + ...S.input, + ...(errors[field] ? { borderColor: "var(--color-danger)", background: "#FEF2F2" } : {}), + }); + + return ( +
{ 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: "center", + justifyContent: "center", + padding: "1rem", + }} + > +
e.stopPropagation()} + style={{ + width: "100%", + maxWidth: 520, + 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", + }} + > + {/* Header */} +
+
+

+ {isNew ? "إضافة عنوان" : "تعديل عنوان"} +

+

+ {isNew ? "عنوان جديد" : editAddress?.label} +

+
+ +
+ + {/* Body */} +
+ {apiError && ( + setApiError("")} /> + )} + + {/* Label + isPrimary */} +
+ + + +
+ + {/* Street */} + + + {/* City + State */} +
+ + +
+ + {/* Postal code + Country */} +
+ + +
+ + {/* Actions */} +
+ + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/Components/Client/ClientAddressForm.tsx b/Components/Client/ClientAddressForm.tsx new file mode 100644 index 0000000..4700977 --- /dev/null +++ b/Components/Client/ClientAddressForm.tsx @@ -0,0 +1,175 @@ +import React, { useState, FormEvent } from "react"; +import { ClientAddress, CreateClientAddressPayload } from "../../types/client"; +import { Input, Select, Button } from "../UI"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface ClientAddressFormProps { + clientId: string; + initialValues?: Partial; + onSubmit: (data: CreateClientAddressPayload) => Promise; + onCancel: () => void; + submitLabel?: string; +} + +type FormErrors = Partial>; + +// ─── Validation ─────────────────────────────────────────────────────────────── + +function validate(v: CreateClientAddressPayload): FormErrors { + const e: FormErrors = {}; + if (!v.label.trim()) e.label = "Label is required."; + if (!v.street.trim()) e.street = "Street is required."; + if (!v.city.trim()) e.city = "City is required."; + if (!v.state.trim()) e.state = "State / Province is required."; + if (!v.postalCode.trim()) e.postalCode = "Postal code is required."; + if (!v.country.trim()) e.country = "Country is required."; + return e; +} + +// Common address labels – quick-pick for the user +const LABEL_PRESETS = ["Billing", "Shipping", "Head Office", "Branch", "Warehouse", "Other"]; + +// ─── Component ──────────────────────────────────────────────────────────────── + +export default function ClientAddressForm({ + clientId, + initialValues = {}, + onSubmit, + onCancel, + submitLabel = "Save address", +}: ClientAddressFormProps) { + const [values, setValues] = useState({ + clientId, + label: initialValues.label ?? "", + street: initialValues.street ?? "", + city: initialValues.city ?? "", + state: initialValues.state ?? "", + postalCode: initialValues.postalCode ?? "", + country: initialValues.country ?? "", + isPrimary: initialValues.isPrimary ?? false, + }); + const [errors, setErrors] = useState({}); + const [submitting, setSubmitting] = useState(false); + + const set = + (field: keyof Omit) => + (e: React.ChangeEvent) => { + setValues((v) => ({ ...v, [field]: e.target.value })); + if (errors[field]) setErrors((er) => ({ ...er, [field]: undefined })); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + const errs = validate(values); + if (Object.keys(errs).length) { + setErrors(errs); + return; + } + setSubmitting(true); + try { + await onSubmit(values); + } catch { + /* parent surfaces the error */ + } finally { + setSubmitting(false); + } + }; + + return ( +
+ {/* Label + Primary toggle */} +
+ + + +
+ + {/* Street */} + + + {/* City + State */} +
+ + +
+ + {/* Postal code + Country */} +
+ + +
+ + {/* Actions */} +
+ + +
+
+ ); +} \ No newline at end of file diff --git a/Components/Client/ClientForm.tsx b/Components/Client/ClientForm.tsx new file mode 100644 index 0000000..feae04e --- /dev/null +++ b/Components/Client/ClientForm.tsx @@ -0,0 +1,135 @@ +import React, { useState, FormEvent } from "react"; +import { Client, CreateClientPayload } from "../../types/client"; +import { Input, Textarea, Button } from "../UI"; + +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface ClientFormProps { + /** Pre-populated when editing; omit for create */ + initialValues?: Partial; + onSubmit: (data: CreateClientPayload) => Promise; + onCancel: () => void; + submitLabel?: string; +} + +type FormErrors = Partial>; + +// ─── Validation ─────────────────────────────────────────────────────────────── + +function validate(values: CreateClientPayload): FormErrors { + const errors: FormErrors = {}; + if (!values.name.trim()) errors.name = "Name is required."; + if (!values.email.trim()) { + errors.email = "Email is required."; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) { + errors.email = "Enter a valid email address."; + } + if (!values.phone.trim()) errors.phone = "Phone is required."; + return errors; +} + +// ─── Component ──────────────────────────────────────────────────────────────── + +export default function ClientForm({ + initialValues = {}, + onSubmit, + onCancel, + submitLabel = "Save client", +}: ClientFormProps) { + const [values, setValues] = useState({ + name: initialValues.name ?? "", + email: initialValues.email ?? "", + phone: initialValues.phone ?? "", + taxId: initialValues.taxId ?? "", + notes: initialValues.notes ?? "", + }); + const [errors, setErrors] = useState({}); + const [submitting, setSubmitting] = useState(false); + + // Generic field updater + const set = (field: keyof CreateClientPayload) => + (e: React.ChangeEvent) => { + setValues((v) => ({ ...v, [field]: e.target.value })); + if (errors[field]) setErrors((er) => ({ ...er, [field]: undefined })); + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + const errs = validate(values); + if (Object.keys(errs).length > 0) { + setErrors(errs); + return; + } + setSubmitting(true); + try { + await onSubmit(values); + } catch { + // Error surfaced via parent hook's alert; keep form open + } finally { + setSubmitting(false); + } + }; + + return ( +
+ {/* Row 1: Name + Email */} +
+ + +
+ + {/* Row 2: Phone + Tax ID */} +
+ + +
+ + {/* Notes */} +