1import React from "react";
2import { Formik, Form, Field, FieldArray, ErrorMessage } from "formik";
3import * as Yup from "yup";
4
5interface LineItem {
6 description: string;
7 quantity: number;
8 unitPrice: number;
9}
10
11interface InvoiceFormValues {
12 clientName: string;
13 invoiceDate: string;
14 items: LineItem[];
15}
16
17const InvoiceSchema = Yup.object().shape({
18 clientName: Yup.string().required("Client name is required"),
19 invoiceDate: Yup.date().required("Invoice date is required"),
20 items: Yup.array()
21 .of(
22 Yup.object().shape({
23 description: Yup.string().required("Description required"),
24 quantity: Yup.number().min(1, "Min qty is 1").required("Required"),
25 unitPrice: Yup.number().min(0.01, "Min price is $0.01").required("Required"),
26 })
27 )
28 .min(1, "You must specify at least one line item"),
29});
30
31const initialValues: InvoiceFormValues = {
32 clientName: "",
33 invoiceDate: new Date().toISOString().split("T")[0],
34 items: [{ description: "Cloud Infrastructure Setup", quantity: 1, unitPrice: 1200 }],
35};
36
37export function InvoiceBuilder() {
38 return (
39 <div className="max-w-3xl p-8 bg-slate-900 text-white rounded-xl">
40 <h2 className="text-xl font-bold mb-6">Create Enterprise Invoice</h2>
41
42 <Formik
43 initialValues={initialValues}
44 validationSchema={InvoiceSchema}
45 onSubmit={async (values, { setSubmitting }) => {
46 console.log("Submitting Invoice:", values);
47 await new Promise((r) => setTimeout(r, 1000));
48 setSubmitting(false);
49 }}
50 >
51 {({ values, isSubmitting }) => (
52 <Form className="space-y-6">
53 <div className="grid grid-cols-2 gap-4">
54 <div>
55 <label className="block text-xs text-slate-400 mb-1">Client Name</label>
56 <Field
57 name="clientName"
58 className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-sm text-white"
59 />
60 <ErrorMessage name="clientName" component="div" className="text-rose-400 text-xs mt-1" />
61 </div>
62
63 <div>
64 <label className="block text-xs text-slate-400 mb-1">Date</label>
65 <Field
66 name="invoiceDate"
67 type="date"
68 className="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded text-sm text-white"
69 />
70 <ErrorMessage name="invoiceDate" component="div" className="text-rose-400 text-xs mt-1" />
71 </div>
72 </div>
73
74 {/* Dynamic Repeatable Field Array */}
75 <div className="border-t border-slate-800 pt-4">
76 <h3 className="text-sm font-semibold text-cyan-400 mb-3">Invoice Line Items</h3>
77
78 <FieldArray name="items">
79 {({ push, remove }) => (
80 <div className="space-y-3">
81 {values.items.map((item, index) => (
82 <div key={index} className="flex gap-3 items-start bg-slate-950 p-3 rounded-lg border border-slate-800">
83 <div className="flex-1">
84 <Field
85 name={`items.${index}.description`}
86 placeholder="Item description"
87 className="w-full px-3 py-1.5 bg-slate-800 border border-slate-700 rounded text-xs text-white"
88 />
89 <ErrorMessage
90 name={`items.${index}.description`}
91 component="div"
92 className="text-rose-400 text-xs mt-0.5"
93 />
94 </div>
95
96 <div className="w-24">
97 <Field
98 name={`items.${index}.quantity`}
99 type="number"
100 placeholder="Qty"
101 className="w-full px-3 py-1.5 bg-slate-800 border border-slate-700 rounded text-xs text-white"
102 />
103 <ErrorMessage
104 name={`items.${index}.quantity`}
105 component="div"
106 className="text-rose-400 text-xs mt-0.5"
107 />
108 </div>
109
110 <div className="w-28">
111 <Field
112 name={`items.${index}.unitPrice`}
113 type="number"
114 placeholder="Price"
115 className="w-full px-3 py-1.5 bg-slate-800 border border-slate-700 rounded text-xs text-white"
116 />
117 <ErrorMessage
118 name={`items.${index}.unitPrice`}
119 component="div"
120 className="text-rose-400 text-xs mt-0.5"
121 />
122 </div>
123
124 {values.items.length > 1 && (
125 <button
126 type="button"
127 onClick={() => remove(index)}
128 className="px-2 py-1.5 bg-rose-900/40 text-rose-300 hover:bg-rose-900 rounded text-xs"
129 >
130 Remove
131 </button>
132 )}
133 </div>
134 ))}
135
136 <button
137 type="button"
138 onClick={() => push({ description: "", quantity: 1, unitPrice: 0 })}
139 className="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-cyan-400 text-xs font-semibold rounded border border-slate-700"
140 >
141 + Add Item Row
142 </button>
143 </div>
144 )}
145 </FieldArray>
146 </div>
147
148 <button
149 type="submit"
150 disabled={isSubmitting}
151 className="w-full py-2.5 bg-cyan-600 hover:bg-cyan-500 font-semibold rounded text-sm disabled:opacity-50"
152 >
153 {isSubmitting ? "Generating Invoice..." : "Submit Invoice"}
154 </button>
155 </Form>
156 )}
157 </Formik>
158 </div>
159 );
160}