CustomerForm.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. "use client";
  2. import { useState } from "react";
  3. import { useRouter } from "next/navigation";
  4. import { useTranslations } from "next-intl";
  5. import { Button } from "@/components/ui/button";
  6. import { Input } from "@/components/ui/input";
  7. import { Label } from "@/components/ui/label";
  8. import { Switch } from "@/components/ui/switch";
  9. import { Textarea } from "@/components/ui/textarea";
  10. import {
  11. Dialog,
  12. DialogContent,
  13. DialogDescription,
  14. DialogHeader,
  15. DialogTitle,
  16. } from "@/components/ui/dialog";
  17. import { useGlassModal } from "@/components/glass-modal";
  18. import { toast } from "sonner";
  19. import { createCustomer, updateCustomer } from "../Actions/customerActions";
  20. import { Loader2 } from "lucide-react";
  21. interface CustomerFormProps {
  22. open: boolean;
  23. onOpenChange: (open: boolean) => void;
  24. customer?: {
  25. id: string;
  26. customerNumber?: string | null;
  27. name: string;
  28. email?: string | null;
  29. phone?: string | null;
  30. address?: string | null;
  31. company?: string | null;
  32. taxId?: string | null;
  33. taxExempt?: boolean;
  34. notes?: string | null;
  35. };
  36. onCreated?: (customer: { id: string; name: string; company: string | null }) => void;
  37. }
  38. export function CustomerForm({ open, onOpenChange, customer, onCreated }: CustomerFormProps) {
  39. const t = useTranslations("customers.form");
  40. const tc = useTranslations("common");
  41. const router = useRouter();
  42. const modal = useGlassModal();
  43. const [loading, setLoading] = useState(false);
  44. const [taxExempt, setTaxExempt] = useState(customer?.taxExempt ?? false);
  45. const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
  46. e.preventDefault();
  47. setLoading(true);
  48. const formData = new FormData(e.currentTarget);
  49. const data = {
  50. name: formData.get("name") as string,
  51. customerNumber: (formData.get("customerNumber") as string) || undefined,
  52. email: (formData.get("email") as string) || undefined,
  53. phone: (formData.get("phone") as string) || undefined,
  54. address: (formData.get("address") as string) || undefined,
  55. company: (formData.get("company") as string) || undefined,
  56. taxId: (formData.get("taxId") as string) || undefined,
  57. taxExempt,
  58. notes: (formData.get("notes") as string) || undefined,
  59. };
  60. const result = customer
  61. ? await updateCustomer({ ...data, id: customer.id })
  62. : await createCustomer(data);
  63. if (result.success) {
  64. toast.success(customer ? t("customerUpdated") : t("customerCreated"));
  65. onOpenChange(false);
  66. if (!customer && result.data && onCreated) {
  67. const created = result.data as { id: string; name: string; company: string | null };
  68. onCreated({ id: created.id, name: created.name, company: created.company ?? null });
  69. }
  70. router.refresh();
  71. } else {
  72. modal.open("error", tc("errors.error"), result.error || t("saveError"));
  73. }
  74. setLoading(false);
  75. };
  76. return (
  77. <Dialog open={open} onOpenChange={onOpenChange}>
  78. <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-lg">
  79. <DialogHeader>
  80. <DialogTitle>
  81. {customer ? t("editTitle") : t("addTitle")}
  82. </DialogTitle>
  83. <DialogDescription className="sr-only">
  84. {customer ? t("editTitle") : t("addTitle")}
  85. </DialogDescription>
  86. </DialogHeader>
  87. <form onSubmit={handleSubmit} className="space-y-4">
  88. <div className="grid grid-cols-[1fr_120px] gap-4">
  89. <div className="space-y-2">
  90. <Label htmlFor="name">{t("nameRequired")}</Label>
  91. <Input
  92. id="name"
  93. name="name"
  94. placeholder={t("namePlaceholder")}
  95. defaultValue={customer?.name}
  96. required
  97. />
  98. </div>
  99. <div className="space-y-2">
  100. <Label htmlFor="customerNumber">{t("customerNumber")}</Label>
  101. <Input
  102. id="customerNumber"
  103. name="customerNumber"
  104. placeholder={t("customerNumberAuto")}
  105. defaultValue={customer?.customerNumber ?? ""}
  106. maxLength={20}
  107. />
  108. </div>
  109. </div>
  110. <div className="grid grid-cols-2 gap-4">
  111. <div className="space-y-2">
  112. <Label htmlFor="email">{tc("form.email")}</Label>
  113. <Input
  114. id="email"
  115. name="email"
  116. type="email"
  117. placeholder={t("emailPlaceholder")}
  118. defaultValue={customer?.email ?? ""}
  119. />
  120. </div>
  121. <div className="space-y-2">
  122. <Label htmlFor="phone">{tc("form.phone")}</Label>
  123. <Input
  124. id="phone"
  125. name="phone"
  126. placeholder={t("phonePlaceholder")}
  127. defaultValue={customer?.phone ?? ""}
  128. />
  129. </div>
  130. </div>
  131. <div className="space-y-2">
  132. <Label htmlFor="company">{tc("form.company")}</Label>
  133. <Input
  134. id="company"
  135. name="company"
  136. placeholder={t("companyPlaceholder")}
  137. defaultValue={customer?.company ?? ""}
  138. />
  139. </div>
  140. <div className="space-y-2">
  141. <Label htmlFor="address">{tc("form.address")}</Label>
  142. <Input
  143. id="address"
  144. name="address"
  145. placeholder={t("addressPlaceholder")}
  146. defaultValue={customer?.address ?? ""}
  147. />
  148. </div>
  149. <div className="space-y-2">
  150. <Label htmlFor="taxId">{t("taxId")}</Label>
  151. <Input
  152. id="taxId"
  153. name="taxId"
  154. placeholder={t("taxIdPlaceholder")}
  155. defaultValue={customer?.taxId ?? ""}
  156. />
  157. <p className="text-xs text-muted-foreground">{t("taxIdHint")}</p>
  158. </div>
  159. <div className="flex items-center justify-between gap-4 rounded-lg border p-3">
  160. <div className="space-y-0.5">
  161. <Label>{t("taxExempt")}</Label>
  162. <p className="text-xs text-muted-foreground">{t("taxExemptHint")}</p>
  163. </div>
  164. <Switch checked={taxExempt} onCheckedChange={setTaxExempt} />
  165. </div>
  166. <div className="space-y-2">
  167. <Label htmlFor="notes">{tc("form.notes")}</Label>
  168. <Textarea
  169. id="notes"
  170. name="notes"
  171. placeholder={t("notesPlaceholder")}
  172. rows={3}
  173. defaultValue={customer?.notes ?? ""}
  174. />
  175. </div>
  176. <div className="flex justify-end gap-3 pt-4">
  177. <Button
  178. type="button"
  179. variant="outline"
  180. onClick={() => onOpenChange(false)}
  181. >
  182. {tc("buttons.cancel")}
  183. </Button>
  184. <Button type="submit" disabled={loading}>
  185. {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
  186. {customer ? tc("buttons.saveChanges") : t("addTitle")}
  187. </Button>
  188. </div>
  189. </form>
  190. </DialogContent>
  191. </Dialog>
  192. );
  193. }