@spaceinvoices/react-ui 0.4.5 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cli/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/components/advance-invoices/advance-invoices.hooks.ts +2 -2
- package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +91 -35
- package/src/components/advance-invoices/create/locales/de.ts +5 -0
- package/src/components/advance-invoices/create/locales/es.ts +5 -0
- package/src/components/advance-invoices/create/locales/fr.ts +5 -0
- package/src/components/advance-invoices/create/locales/hr.ts +5 -0
- package/src/components/advance-invoices/create/locales/it.ts +5 -0
- package/src/components/advance-invoices/create/locales/nl.ts +5 -0
- package/src/components/advance-invoices/create/locales/pl.ts +5 -0
- package/src/components/advance-invoices/create/locales/pt.ts +5 -0
- package/src/components/advance-invoices/create/locales/sl.ts +5 -0
- package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +5 -5
- package/src/components/credit-notes/create/create-credit-note-form.tsx +91 -35
- package/src/components/credit-notes/create/locales/de.ts +5 -0
- package/src/components/credit-notes/create/locales/es.ts +5 -0
- package/src/components/credit-notes/create/locales/fr.ts +5 -0
- package/src/components/credit-notes/create/locales/hr.ts +5 -0
- package/src/components/credit-notes/create/locales/it.ts +5 -0
- package/src/components/credit-notes/create/locales/nl.ts +5 -0
- package/src/components/credit-notes/create/locales/pl.ts +5 -0
- package/src/components/credit-notes/create/locales/pt.ts +5 -0
- package/src/components/credit-notes/create/locales/sl.ts +5 -0
- package/src/components/credit-notes/credit-notes.hooks.ts +2 -2
- package/src/components/delivery-notes/create/create-delivery-note-form.tsx +47 -0
- package/src/components/delivery-notes/create/locales/de.ts +5 -0
- package/src/components/delivery-notes/create/locales/es.ts +5 -0
- package/src/components/delivery-notes/create/locales/fr.ts +5 -0
- package/src/components/delivery-notes/create/locales/hr.ts +5 -0
- package/src/components/delivery-notes/create/locales/it.ts +5 -0
- package/src/components/delivery-notes/create/locales/nl.ts +5 -0
- package/src/components/delivery-notes/create/locales/pl.ts +5 -0
- package/src/components/delivery-notes/create/locales/pt.ts +5 -0
- package/src/components/delivery-notes/create/locales/sl.ts +5 -0
- package/src/components/documents/create/document-details-section.tsx +472 -346
- package/src/components/documents/create/prepare-document-submission.ts +3 -1
- package/src/components/documents/create/smart-code-insert-button.tsx +6 -0
- package/src/components/documents/view/document-details-card.tsx +6 -0
- package/src/components/documents/view/locales/de.ts +1 -0
- package/src/components/documents/view/locales/es.ts +1 -0
- package/src/components/documents/view/locales/fr.ts +1 -0
- package/src/components/documents/view/locales/hr.ts +1 -0
- package/src/components/documents/view/locales/it.ts +1 -0
- package/src/components/documents/view/locales/nl.ts +1 -0
- package/src/components/documents/view/locales/pl.ts +1 -0
- package/src/components/documents/view/locales/pt.ts +1 -0
- package/src/components/documents/view/locales/sl.ts +1 -0
- package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +6 -0
- package/src/components/entities/entity-settings-form/input-with-preview.tsx +2 -145
- package/src/components/entities/entity-settings-form/locales/de.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/es.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/fr.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/hr.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/it.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/nl.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/pl.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/pt.ts +4 -0
- package/src/components/entities/entity-settings-form/locales/sl.ts +4 -0
- package/src/components/entities/fina-settings-form/fina-settings-form.tsx +15 -0
- package/src/components/entities/fina-settings-form/fina-settings.hooks.ts +5 -1
- package/src/components/entities/fina-settings-form/locales/de.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/en.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/es.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/fr.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/hr.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/it.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/nl.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/pl.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/pt.ts +3 -0
- package/src/components/entities/fina-settings-form/locales/sl.ts +3 -0
- package/src/components/entities/fina-settings-form/sections/premises-management-section.tsx +4 -4
- package/src/components/entities/fina-settings-form/sections/register-premise-dialog.tsx +3 -3
- package/src/components/entities/settings/defaults-settings-form.tsx +38 -1
- package/src/components/entities/settings/tax-rules-settings-form.tsx +1 -2
- package/src/components/estimates/create/create-estimate-form.tsx +43 -2
- package/src/components/estimates/create/locales/de.ts +5 -0
- package/src/components/estimates/create/locales/es.ts +5 -0
- package/src/components/estimates/create/locales/fr.ts +5 -0
- package/src/components/estimates/create/locales/hr.ts +5 -0
- package/src/components/estimates/create/locales/it.ts +5 -0
- package/src/components/estimates/create/locales/nl.ts +5 -0
- package/src/components/estimates/create/locales/pl.ts +5 -0
- package/src/components/estimates/create/locales/pt.ts +5 -0
- package/src/components/estimates/create/locales/sl.ts +5 -0
- package/src/components/invoices/create/create-invoice-form.tsx +130 -40
- package/src/components/invoices/create/locales/de.ts +13 -0
- package/src/components/invoices/create/locales/es.ts +13 -0
- package/src/components/invoices/create/locales/fr.ts +13 -0
- package/src/components/invoices/create/locales/hr.ts +13 -0
- package/src/components/invoices/create/locales/it.ts +13 -0
- package/src/components/invoices/create/locales/nl.ts +13 -0
- package/src/components/invoices/create/locales/pl.ts +13 -0
- package/src/components/invoices/create/locales/pt.ts +13 -0
- package/src/components/invoices/create/locales/sl.ts +13 -0
- package/src/components/invoices/create/prepare-invoice-submission.ts +5 -5
- package/src/components/invoices/invoices.hooks.ts +2 -2
- package/src/components/table/table-pagination.tsx +1 -1
- package/src/generated/schemas/advanceinvoice.ts +2 -0
- package/src/generated/schemas/creditnote.ts +1 -0
- package/src/generated/schemas/deliverynote.ts +1 -0
- package/src/generated/schemas/entity.ts +4 -4
- package/src/generated/schemas/entityapikey.ts +19 -0
- package/src/generated/schemas/estimate.ts +2 -0
- package/src/generated/schemas/index.ts +1 -0
- package/src/generated/schemas/invoice.ts +2 -0
- package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +1 -1
- package/src/generated/schemas/rendercreditnotepreview_body.ts +1 -1
- package/src/generated/schemas/renderdeliverynotepreview_body.ts +1 -1
- package/src/generated/schemas/renderestimatepreview_body.ts +1 -1
- package/src/generated/schemas/renderinvoicepreview_body.ts +1 -1
- package/src/generated/schemas/startpdfexport_body.ts +14 -2
- package/src/generated/schemas/webhook.ts +4 -0
- package/src/lib/template-variables.tsx +167 -0
- package/src/providers/entities-context.tsx +2 -2
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
* Handles: number, date, and document-type-specific date field (date_due or date_valid_till)
|
|
4
4
|
*/
|
|
5
5
|
import type { Entity, Estimate, Invoice, ViesCheckResponse } from "@spaceinvoices/js-sdk";
|
|
6
|
-
import { CalendarIcon, Globe, Info, Loader2 } from "lucide-react";
|
|
6
|
+
import { CalendarIcon, ChevronDown, Globe, Info, Loader2 } from "lucide-react";
|
|
7
7
|
import { useRef, useState } from "react";
|
|
8
8
|
import { Badge } from "@/ui/components/ui/badge";
|
|
9
9
|
import { Button } from "@/ui/components/ui/button";
|
|
10
10
|
import { Calendar } from "@/ui/components/ui/calendar";
|
|
11
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/ui/components/ui/collapsible";
|
|
11
12
|
import { FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/ui/components/ui/form";
|
|
12
13
|
import { Input } from "@/ui/components/ui/input";
|
|
13
14
|
import { Popover, PopoverContent, PopoverTrigger } from "@/ui/components/ui/popover";
|
|
@@ -15,6 +16,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|
|
15
16
|
import { Textarea } from "@/ui/components/ui/textarea";
|
|
16
17
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/ui/components/ui/tooltip";
|
|
17
18
|
import { CURRENCY_CODES } from "@/ui/lib/constants";
|
|
19
|
+
import { replaceTemplateVariablesForPreview } from "@/ui/lib/template-variables";
|
|
18
20
|
import { cn } from "@/ui/lib/utils";
|
|
19
21
|
import type { DocumentTypes } from "../types";
|
|
20
22
|
import type { AnyControl } from "./form-types";
|
|
@@ -42,12 +44,12 @@ type FursInlineProps = {
|
|
|
42
44
|
|
|
43
45
|
type FinaPremise = {
|
|
44
46
|
id: string;
|
|
45
|
-
|
|
47
|
+
business_premise_name: string;
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
type FinaDevice = {
|
|
49
51
|
id: string;
|
|
50
|
-
|
|
52
|
+
electronic_device_name: string;
|
|
51
53
|
};
|
|
52
54
|
|
|
53
55
|
type FinaInlineProps = {
|
|
@@ -64,6 +66,22 @@ type ServiceDateProps = {
|
|
|
64
66
|
onDateTypeChange: (type: "single" | "range") => void;
|
|
65
67
|
};
|
|
66
68
|
|
|
69
|
+
const DUE_DAYS_PRESETS = [0, 7, 14, 30, 60, 90] as const;
|
|
70
|
+
|
|
71
|
+
type DueDaysProps = {
|
|
72
|
+
dueDaysType: number | "custom";
|
|
73
|
+
onDueDaysTypeChange: (type: number | "custom") => void;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const LABEL_WIDTH = "w-[6.5rem] shrink-0";
|
|
77
|
+
|
|
78
|
+
function extractSequenceNumber(fullNumber: string, premise?: string, device?: string): string {
|
|
79
|
+
if (!fullNumber || (!premise && !device)) return fullNumber;
|
|
80
|
+
const parts = fullNumber.split(/[-/]/);
|
|
81
|
+
const filtered = parts.filter((part) => !(premise && part === premise) && !(device && part === device));
|
|
82
|
+
return filtered.join("") || fullNumber;
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
type DocumentDetailsSectionProps = {
|
|
68
86
|
control: AnyControl;
|
|
69
87
|
documentType: DocumentTypes;
|
|
@@ -72,6 +90,7 @@ type DocumentDetailsSectionProps = {
|
|
|
72
90
|
fursInline?: FursInlineProps; // FURS premise/device inline with number
|
|
73
91
|
finaInline?: FinaInlineProps; // FINA premise/device inline with number
|
|
74
92
|
serviceDate?: ServiceDateProps; // Service date section (invoice only)
|
|
93
|
+
dueDays?: DueDaysProps; // Due days selector (invoice only)
|
|
75
94
|
};
|
|
76
95
|
|
|
77
96
|
export function DocumentDetailsSection({
|
|
@@ -82,6 +101,7 @@ export function DocumentDetailsSection({
|
|
|
82
101
|
fursInline,
|
|
83
102
|
finaInline,
|
|
84
103
|
serviceDate,
|
|
104
|
+
dueDays,
|
|
85
105
|
}: DocumentDetailsSectionProps) {
|
|
86
106
|
// Determine the date field name based on document type
|
|
87
107
|
// Delivery notes don't have a secondary date field
|
|
@@ -94,93 +114,124 @@ export function DocumentDetailsSection({
|
|
|
94
114
|
const showFinaSelects = !!finaInline;
|
|
95
115
|
|
|
96
116
|
return (
|
|
97
|
-
<div className="flex-1 space-y-
|
|
117
|
+
<div className="flex-1 space-y-3">
|
|
98
118
|
<h2 className="font-bold text-xl">{t("Details")}</h2>
|
|
99
119
|
|
|
100
|
-
{/* Number field - with optional FURS premise/device
|
|
120
|
+
{/* Number field - inline with optional FURS/FINA premise/device + sequence number */}
|
|
101
121
|
<FormField
|
|
102
122
|
control={control}
|
|
103
123
|
name="number"
|
|
104
124
|
render={({ field }) => (
|
|
105
125
|
<FormItem>
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{premise.business_premise_name}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
{device.electronic_device_name}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<
|
|
142
|
-
<
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
</
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
))}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
126
|
+
<div className="flex items-center gap-3">
|
|
127
|
+
<FormLabel className={LABEL_WIDTH}>{t("Number")} *</FormLabel>
|
|
128
|
+
{showFursSelects ? (
|
|
129
|
+
<div className="flex flex-1 items-center gap-2">
|
|
130
|
+
<Select
|
|
131
|
+
value={fursInline.selectedPremise || ""}
|
|
132
|
+
onValueChange={(v) => fursInline.onPremiseChange(v ?? undefined)}
|
|
133
|
+
>
|
|
134
|
+
<SelectTrigger className="w-24">
|
|
135
|
+
<SelectValue placeholder={t("Premise")} />
|
|
136
|
+
</SelectTrigger>
|
|
137
|
+
<SelectContent>
|
|
138
|
+
{fursInline.premises.map((premise) => (
|
|
139
|
+
<SelectItem key={premise.id} value={premise.business_premise_name}>
|
|
140
|
+
{premise.business_premise_name}
|
|
141
|
+
</SelectItem>
|
|
142
|
+
))}
|
|
143
|
+
</SelectContent>
|
|
144
|
+
</Select>
|
|
145
|
+
<Select
|
|
146
|
+
value={fursInline.selectedDevice || ""}
|
|
147
|
+
onValueChange={(v) => fursInline.onDeviceChange(v ?? undefined)}
|
|
148
|
+
disabled={!fursInline.selectedPremise || fursInline.devices.length === 0}
|
|
149
|
+
>
|
|
150
|
+
<SelectTrigger className="w-24">
|
|
151
|
+
<SelectValue placeholder={t("Device")} />
|
|
152
|
+
</SelectTrigger>
|
|
153
|
+
<SelectContent>
|
|
154
|
+
{fursInline.devices.map((device) => (
|
|
155
|
+
<SelectItem key={device.id} value={device.electronic_device_name}>
|
|
156
|
+
{device.electronic_device_name}
|
|
157
|
+
</SelectItem>
|
|
158
|
+
))}
|
|
159
|
+
</SelectContent>
|
|
160
|
+
</Select>
|
|
161
|
+
<Tooltip>
|
|
162
|
+
<TooltipTrigger asChild>
|
|
163
|
+
<FormControl>
|
|
164
|
+
<Input
|
|
165
|
+
disabled
|
|
166
|
+
readOnly
|
|
167
|
+
className="flex-1 text-right"
|
|
168
|
+
value={extractSequenceNumber(
|
|
169
|
+
field.value || "",
|
|
170
|
+
fursInline.selectedPremise,
|
|
171
|
+
fursInline.selectedDevice,
|
|
172
|
+
)}
|
|
173
|
+
/>
|
|
174
|
+
</FormControl>
|
|
175
|
+
</TooltipTrigger>
|
|
176
|
+
<TooltipContent>
|
|
177
|
+
<p>{field.value || t("Number format can be changed in settings")}</p>
|
|
178
|
+
</TooltipContent>
|
|
179
|
+
</Tooltip>
|
|
180
|
+
</div>
|
|
181
|
+
) : showFinaSelects ? (
|
|
182
|
+
<div className="flex flex-1 items-center gap-2">
|
|
183
|
+
<Select
|
|
184
|
+
value={finaInline.selectedPremise || ""}
|
|
185
|
+
onValueChange={(v) => finaInline.onPremiseChange(v ?? undefined)}
|
|
186
|
+
>
|
|
187
|
+
<SelectTrigger className="w-24">
|
|
188
|
+
<SelectValue placeholder={t("Premise")} />
|
|
189
|
+
</SelectTrigger>
|
|
190
|
+
<SelectContent>
|
|
191
|
+
{finaInline.premises.map((premise) => (
|
|
192
|
+
<SelectItem key={premise.id} value={premise.business_premise_name}>
|
|
193
|
+
{premise.business_premise_name}
|
|
194
|
+
</SelectItem>
|
|
195
|
+
))}
|
|
196
|
+
</SelectContent>
|
|
197
|
+
</Select>
|
|
198
|
+
<Select
|
|
199
|
+
value={finaInline.selectedDevice || ""}
|
|
200
|
+
onValueChange={(v) => finaInline.onDeviceChange(v ?? undefined)}
|
|
201
|
+
disabled={!finaInline.selectedPremise || finaInline.devices.length === 0}
|
|
202
|
+
>
|
|
203
|
+
<SelectTrigger className="w-24">
|
|
204
|
+
<SelectValue placeholder={t("Device")} />
|
|
205
|
+
</SelectTrigger>
|
|
206
|
+
<SelectContent>
|
|
207
|
+
{finaInline.devices.map((device) => (
|
|
208
|
+
<SelectItem key={device.id} value={device.electronic_device_name}>
|
|
209
|
+
{device.electronic_device_name}
|
|
210
|
+
</SelectItem>
|
|
211
|
+
))}
|
|
212
|
+
</SelectContent>
|
|
213
|
+
</Select>
|
|
214
|
+
<Tooltip>
|
|
215
|
+
<TooltipTrigger asChild>
|
|
216
|
+
<FormControl>
|
|
217
|
+
<Input
|
|
218
|
+
disabled
|
|
219
|
+
readOnly
|
|
220
|
+
className="flex-1 text-right"
|
|
221
|
+
value={extractSequenceNumber(
|
|
222
|
+
field.value || "",
|
|
223
|
+
finaInline.selectedPremise,
|
|
224
|
+
finaInline.selectedDevice,
|
|
225
|
+
)}
|
|
226
|
+
/>
|
|
227
|
+
</FormControl>
|
|
228
|
+
</TooltipTrigger>
|
|
229
|
+
<TooltipContent>
|
|
230
|
+
<p>{field.value || t("Number format can be changed in settings")}</p>
|
|
231
|
+
</TooltipContent>
|
|
232
|
+
</Tooltip>
|
|
233
|
+
</div>
|
|
234
|
+
) : (
|
|
184
235
|
<Tooltip>
|
|
185
236
|
<TooltipTrigger asChild>
|
|
186
237
|
<FormControl>
|
|
@@ -191,19 +242,9 @@ export function DocumentDetailsSection({
|
|
|
191
242
|
<p>{t("Number format can be changed in settings")}</p>
|
|
192
243
|
</TooltipContent>
|
|
193
244
|
</Tooltip>
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
<TooltipTrigger asChild>
|
|
198
|
-
<FormControl>
|
|
199
|
-
<Input {...field} disabled />
|
|
200
|
-
</FormControl>
|
|
201
|
-
</TooltipTrigger>
|
|
202
|
-
<TooltipContent>
|
|
203
|
-
<p>{t("Number format can be changed in settings")}</p>
|
|
204
|
-
</TooltipContent>
|
|
205
|
-
</Tooltip>
|
|
206
|
-
)}
|
|
245
|
+
)}
|
|
246
|
+
</div>
|
|
247
|
+
|
|
207
248
|
<FormMessage />
|
|
208
249
|
</FormItem>
|
|
209
250
|
)}
|
|
@@ -214,64 +255,70 @@ export function DocumentDetailsSection({
|
|
|
214
255
|
name="date"
|
|
215
256
|
render={({ field }) => (
|
|
216
257
|
<FormItem>
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
<
|
|
221
|
-
<
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
<
|
|
235
|
-
<
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
258
|
+
<div className="flex items-center gap-3">
|
|
259
|
+
<FormLabel className={LABEL_WIDTH}>{t("Date")} *</FormLabel>
|
|
260
|
+
{showFinaSelects ? (
|
|
261
|
+
<Tooltip>
|
|
262
|
+
<TooltipTrigger asChild>
|
|
263
|
+
<FormControl>
|
|
264
|
+
<Button variant="outline" disabled className="flex-1 pl-3 text-left font-normal">
|
|
265
|
+
{new Date().toLocaleDateString()}
|
|
266
|
+
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
267
|
+
</Button>
|
|
268
|
+
</FormControl>
|
|
269
|
+
</TooltipTrigger>
|
|
270
|
+
<TooltipContent>
|
|
271
|
+
<p>{t("FINA fiscalized invoices always use the current date")}</p>
|
|
272
|
+
</TooltipContent>
|
|
273
|
+
</Tooltip>
|
|
274
|
+
) : (
|
|
275
|
+
<Popover>
|
|
276
|
+
<PopoverTrigger asChild>
|
|
277
|
+
<FormControl>
|
|
278
|
+
<Button
|
|
279
|
+
variant="outline"
|
|
280
|
+
className={cn("flex-1 pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
|
281
|
+
>
|
|
282
|
+
{field.value ? new Date(field.value).toLocaleDateString() : <span>{t("Pick a date")}</span>}
|
|
283
|
+
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
284
|
+
</Button>
|
|
285
|
+
</FormControl>
|
|
286
|
+
</PopoverTrigger>
|
|
287
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
288
|
+
<Calendar
|
|
289
|
+
mode="single"
|
|
290
|
+
selected={field.value ? new Date(field.value) : undefined}
|
|
291
|
+
onSelect={(date) => field.onChange(date?.toISOString())}
|
|
292
|
+
disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
|
|
293
|
+
initialFocus
|
|
294
|
+
/>
|
|
295
|
+
</PopoverContent>
|
|
296
|
+
</Popover>
|
|
297
|
+
)}
|
|
298
|
+
</div>
|
|
256
299
|
<FormMessage />
|
|
257
300
|
</FormItem>
|
|
258
301
|
)}
|
|
259
302
|
/>
|
|
260
303
|
|
|
261
|
-
{/* Service Date -
|
|
304
|
+
{/* Service Date - select replaces label */}
|
|
262
305
|
{serviceDate && (
|
|
263
306
|
<FormField
|
|
264
307
|
control={control}
|
|
265
308
|
name="date_service"
|
|
266
309
|
render={({ field }) => (
|
|
267
310
|
<FormItem>
|
|
268
|
-
<div className="flex items-center
|
|
269
|
-
<FormLabel>{t("Service Date")}</FormLabel>
|
|
311
|
+
<div className="flex items-center gap-3">
|
|
270
312
|
<Select
|
|
271
313
|
value={serviceDate.dateType}
|
|
272
314
|
onValueChange={(v) => serviceDate.onDateTypeChange(v as "single" | "range")}
|
|
273
315
|
>
|
|
274
|
-
<SelectTrigger
|
|
316
|
+
<SelectTrigger
|
|
317
|
+
className={cn(
|
|
318
|
+
LABEL_WIDTH,
|
|
319
|
+
"h-auto border-none p-0 font-medium text-sm shadow-none [&>svg]:ml-1 [&>svg]:size-3.5",
|
|
320
|
+
)}
|
|
321
|
+
>
|
|
275
322
|
<SelectValue>{serviceDate.dateType === "single" ? t("Single Date") : t("Date Range")}</SelectValue>
|
|
276
323
|
</SelectTrigger>
|
|
277
324
|
<SelectContent>
|
|
@@ -279,40 +326,15 @@ export function DocumentDetailsSection({
|
|
|
279
326
|
<SelectItem value="range">{t("Date Range")}</SelectItem>
|
|
280
327
|
</SelectContent>
|
|
281
328
|
</Select>
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
{serviceDate.dateType === "single" ? (
|
|
285
|
-
<Popover>
|
|
286
|
-
<PopoverTrigger asChild>
|
|
287
|
-
<FormControl>
|
|
288
|
-
<Button
|
|
289
|
-
variant="outline"
|
|
290
|
-
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
|
291
|
-
>
|
|
292
|
-
{field.value ? new Date(field.value).toLocaleDateString() : <span>{t("Pick a date")}</span>}
|
|
293
|
-
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
294
|
-
</Button>
|
|
295
|
-
</FormControl>
|
|
296
|
-
</PopoverTrigger>
|
|
297
|
-
<PopoverContent className="w-auto p-0" align="start">
|
|
298
|
-
<Calendar
|
|
299
|
-
mode="single"
|
|
300
|
-
selected={field.value ? new Date(field.value) : undefined}
|
|
301
|
-
onSelect={(date) => field.onChange(date?.toISOString())}
|
|
302
|
-
initialFocus
|
|
303
|
-
/>
|
|
304
|
-
</PopoverContent>
|
|
305
|
-
</Popover>
|
|
306
|
-
) : (
|
|
307
|
-
<div className="grid grid-cols-2 gap-2">
|
|
329
|
+
{serviceDate.dateType === "single" ? (
|
|
308
330
|
<Popover>
|
|
309
331
|
<PopoverTrigger asChild>
|
|
310
332
|
<FormControl>
|
|
311
333
|
<Button
|
|
312
334
|
variant="outline"
|
|
313
|
-
className={cn("
|
|
335
|
+
className={cn("flex-1 pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
|
314
336
|
>
|
|
315
|
-
{field.value ? new Date(field.value).toLocaleDateString() : <span>{t("
|
|
337
|
+
{field.value ? new Date(field.value).toLocaleDateString() : <span>{t("Pick a date")}</span>}
|
|
316
338
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
317
339
|
</Button>
|
|
318
340
|
</FormControl>
|
|
@@ -326,37 +348,61 @@ export function DocumentDetailsSection({
|
|
|
326
348
|
/>
|
|
327
349
|
</PopoverContent>
|
|
328
350
|
</Popover>
|
|
329
|
-
|
|
330
|
-
<
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
<Popover>
|
|
335
|
-
<PopoverTrigger asChild>
|
|
351
|
+
) : (
|
|
352
|
+
<div className="grid flex-1 grid-cols-2 gap-2">
|
|
353
|
+
<Popover>
|
|
354
|
+
<PopoverTrigger asChild>
|
|
355
|
+
<FormControl>
|
|
336
356
|
<Button
|
|
337
357
|
variant="outline"
|
|
338
|
-
className={cn(
|
|
339
|
-
"w-full pl-3 text-left font-normal",
|
|
340
|
-
!toField.value && "text-muted-foreground",
|
|
341
|
-
)}
|
|
358
|
+
className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
|
342
359
|
>
|
|
343
|
-
{
|
|
360
|
+
{field.value ? new Date(field.value).toLocaleDateString() : <span>{t("From")}</span>}
|
|
344
361
|
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
345
362
|
</Button>
|
|
346
|
-
</
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
</
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
363
|
+
</FormControl>
|
|
364
|
+
</PopoverTrigger>
|
|
365
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
366
|
+
<Calendar
|
|
367
|
+
mode="single"
|
|
368
|
+
selected={field.value ? new Date(field.value) : undefined}
|
|
369
|
+
onSelect={(date) => field.onChange(date?.toISOString())}
|
|
370
|
+
initialFocus
|
|
371
|
+
/>
|
|
372
|
+
</PopoverContent>
|
|
373
|
+
</Popover>
|
|
374
|
+
|
|
375
|
+
<FormField
|
|
376
|
+
control={control}
|
|
377
|
+
name="date_service_to"
|
|
378
|
+
render={({ field: toField }) => (
|
|
379
|
+
<Popover>
|
|
380
|
+
<PopoverTrigger asChild>
|
|
381
|
+
<Button
|
|
382
|
+
variant="outline"
|
|
383
|
+
className={cn(
|
|
384
|
+
"w-full pl-3 text-left font-normal",
|
|
385
|
+
!toField.value && "text-muted-foreground",
|
|
386
|
+
)}
|
|
387
|
+
>
|
|
388
|
+
{toField.value ? new Date(toField.value).toLocaleDateString() : <span>{t("To")}</span>}
|
|
389
|
+
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
390
|
+
</Button>
|
|
391
|
+
</PopoverTrigger>
|
|
392
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
393
|
+
<Calendar
|
|
394
|
+
mode="single"
|
|
395
|
+
selected={toField.value ? new Date(toField.value) : undefined}
|
|
396
|
+
onSelect={(date) => toField.onChange(date?.toISOString())}
|
|
397
|
+
initialFocus
|
|
398
|
+
/>
|
|
399
|
+
</PopoverContent>
|
|
400
|
+
</Popover>
|
|
401
|
+
)}
|
|
402
|
+
/>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
</div>
|
|
360
406
|
<FormMessage />
|
|
361
407
|
</FormItem>
|
|
362
408
|
)}
|
|
@@ -369,29 +415,60 @@ export function DocumentDetailsSection({
|
|
|
369
415
|
name={dateFieldName}
|
|
370
416
|
render={({ field }) => (
|
|
371
417
|
<FormItem>
|
|
372
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
<
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
>
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
418
|
+
<div className="flex items-center gap-3">
|
|
419
|
+
<FormLabel className={LABEL_WIDTH}>{dateFieldLabel}</FormLabel>
|
|
420
|
+
{documentType === "invoice" && dueDays && (
|
|
421
|
+
<Select
|
|
422
|
+
value={String(dueDays.dueDaysType)}
|
|
423
|
+
onValueChange={(v) => dueDays.onDueDaysTypeChange(v === "custom" ? "custom" : Number(v))}
|
|
424
|
+
>
|
|
425
|
+
<SelectTrigger className="h-8 w-auto shrink-0 gap-1 border-none px-2 text-xs shadow-none">
|
|
426
|
+
<SelectValue>
|
|
427
|
+
{dueDays.dueDaysType === "custom"
|
|
428
|
+
? t("Custom")
|
|
429
|
+
: dueDays.dueDaysType === 0
|
|
430
|
+
? t("On receipt")
|
|
431
|
+
: t(`${dueDays.dueDaysType} days`)}
|
|
432
|
+
</SelectValue>
|
|
433
|
+
</SelectTrigger>
|
|
434
|
+
<SelectContent>
|
|
435
|
+
{DUE_DAYS_PRESETS.map((days) => (
|
|
436
|
+
<SelectItem key={days} value={String(days)}>
|
|
437
|
+
{days === 0 ? t("On receipt") : t(`${days} days`)}
|
|
438
|
+
</SelectItem>
|
|
439
|
+
))}
|
|
440
|
+
<SelectItem value="custom">{t("Custom")}</SelectItem>
|
|
441
|
+
</SelectContent>
|
|
442
|
+
</Select>
|
|
443
|
+
)}
|
|
444
|
+
<Popover>
|
|
445
|
+
<PopoverTrigger asChild>
|
|
446
|
+
<FormControl>
|
|
447
|
+
<Button
|
|
448
|
+
variant="outline"
|
|
449
|
+
className={cn("flex-1 pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
|
|
450
|
+
>
|
|
451
|
+
{field.value ? new Date(field.value).toLocaleDateString() : <span>{t("Pick a date")}</span>}
|
|
452
|
+
<CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
|
|
453
|
+
</Button>
|
|
454
|
+
</FormControl>
|
|
455
|
+
</PopoverTrigger>
|
|
456
|
+
<PopoverContent className="w-auto p-0" align="start">
|
|
457
|
+
<Calendar
|
|
458
|
+
mode="single"
|
|
459
|
+
selected={field.value ? new Date(field.value) : undefined}
|
|
460
|
+
onSelect={(date) => {
|
|
461
|
+
field.onChange(date?.toISOString());
|
|
462
|
+
if (dueDays && dueDays.dueDaysType !== "custom") {
|
|
463
|
+
dueDays.onDueDaysTypeChange("custom");
|
|
464
|
+
}
|
|
465
|
+
}}
|
|
466
|
+
disabled={(date) => date < new Date("1900-01-01")}
|
|
467
|
+
initialFocus
|
|
468
|
+
/>
|
|
469
|
+
</PopoverContent>
|
|
470
|
+
</Popover>
|
|
471
|
+
</div>
|
|
395
472
|
<FormMessage />
|
|
396
473
|
</FormItem>
|
|
397
474
|
)}
|
|
@@ -400,24 +477,42 @@ export function DocumentDetailsSection({
|
|
|
400
477
|
|
|
401
478
|
<FormField
|
|
402
479
|
control={control}
|
|
403
|
-
name="
|
|
480
|
+
name="reference"
|
|
404
481
|
render={({ field }) => (
|
|
405
482
|
<FormItem>
|
|
406
|
-
<
|
|
407
|
-
|
|
483
|
+
<div className="flex items-center gap-3">
|
|
484
|
+
<FormLabel className={LABEL_WIDTH}>{t("Reference")}</FormLabel>
|
|
408
485
|
<FormControl>
|
|
409
|
-
<
|
|
410
|
-
<SelectValue placeholder={t("Select currency")} />
|
|
411
|
-
</SelectTrigger>
|
|
486
|
+
<Input {...field} value={field.value || ""} placeholder={t("e.g., PO-2024-001")} />
|
|
412
487
|
</FormControl>
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
488
|
+
</div>
|
|
489
|
+
<FormMessage />
|
|
490
|
+
</FormItem>
|
|
491
|
+
)}
|
|
492
|
+
/>
|
|
493
|
+
|
|
494
|
+
<FormField
|
|
495
|
+
control={control}
|
|
496
|
+
name="currency_code"
|
|
497
|
+
render={({ field }) => (
|
|
498
|
+
<FormItem>
|
|
499
|
+
<div className="flex items-center gap-3">
|
|
500
|
+
<FormLabel className={LABEL_WIDTH}>{t("Currency")} *</FormLabel>
|
|
501
|
+
<Select onValueChange={(value) => value && field.onChange(value)} value={field.value || ""}>
|
|
502
|
+
<FormControl>
|
|
503
|
+
<SelectTrigger className="h-10 flex-1">
|
|
504
|
+
<SelectValue placeholder={t("Select currency")} />
|
|
505
|
+
</SelectTrigger>
|
|
506
|
+
</FormControl>
|
|
507
|
+
<SelectContent>
|
|
508
|
+
{CURRENCY_CODES.map((currency) => (
|
|
509
|
+
<SelectItem key={currency.value} value={currency.value}>
|
|
510
|
+
{currency.label}
|
|
511
|
+
</SelectItem>
|
|
512
|
+
))}
|
|
513
|
+
</SelectContent>
|
|
514
|
+
</Select>
|
|
515
|
+
</div>
|
|
421
516
|
<FormMessage />
|
|
422
517
|
</FormItem>
|
|
423
518
|
)}
|
|
@@ -433,122 +528,6 @@ export function DocumentDetailsSection({
|
|
|
433
528
|
* Note field component with smart code insertion button
|
|
434
529
|
* Exported for use in document forms (placed after items section)
|
|
435
530
|
*/
|
|
436
|
-
// Helper functions for template variable replacement (shared with InputWithPreview)
|
|
437
|
-
function formatVariableName(varName: string): string {
|
|
438
|
-
return varName
|
|
439
|
-
.split("_")
|
|
440
|
-
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
441
|
-
.join(" ");
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
function getVariableValue(
|
|
445
|
-
varName: string,
|
|
446
|
-
entity?: Entity | null,
|
|
447
|
-
document?: Partial<Invoice | Estimate> | null,
|
|
448
|
-
): string | null {
|
|
449
|
-
if (!entity) return null;
|
|
450
|
-
|
|
451
|
-
// Entity-related variables
|
|
452
|
-
if (varName === "entity_name") return entity.name || null;
|
|
453
|
-
if (varName === "entity_email") return (entity.settings as any)?.email || null;
|
|
454
|
-
|
|
455
|
-
// Date variables
|
|
456
|
-
if (varName === "current_date") {
|
|
457
|
-
return new Date().toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" });
|
|
458
|
-
}
|
|
459
|
-
if (varName === "current_year") return new Date().getFullYear().toString();
|
|
460
|
-
|
|
461
|
-
// Document-specific variables
|
|
462
|
-
if (document) {
|
|
463
|
-
if (varName === "document_number") return (document as any).number || null;
|
|
464
|
-
if (varName === "document_date" && (document as any).date) {
|
|
465
|
-
return new Date((document as any).date).toLocaleDateString("en-US", {
|
|
466
|
-
month: "long",
|
|
467
|
-
day: "numeric",
|
|
468
|
-
year: "numeric",
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
if (varName === "document_total" && (document as any).total_with_tax) {
|
|
472
|
-
return new Intl.NumberFormat("en-US", {
|
|
473
|
-
style: "currency",
|
|
474
|
-
currency: (document as any).currency_code || "USD",
|
|
475
|
-
}).format(Number((document as any).total_with_tax));
|
|
476
|
-
}
|
|
477
|
-
if (varName === "document_currency") return (document as any).currency_code || null;
|
|
478
|
-
|
|
479
|
-
// Invoice due date
|
|
480
|
-
if (varName === "document_due_date" && (document as any).date_due) {
|
|
481
|
-
return new Date((document as any).date_due).toLocaleDateString("en-US", {
|
|
482
|
-
month: "long",
|
|
483
|
-
day: "numeric",
|
|
484
|
-
year: "numeric",
|
|
485
|
-
});
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Estimate valid until
|
|
489
|
-
if (varName === "document_valid_until" && (document as any).date_valid_till) {
|
|
490
|
-
return new Date((document as any).date_valid_till).toLocaleDateString("en-US", {
|
|
491
|
-
month: "long",
|
|
492
|
-
day: "numeric",
|
|
493
|
-
year: "numeric",
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
// Customer variables
|
|
498
|
-
if ((document as any).customer) {
|
|
499
|
-
if (varName === "customer_name") return (document as any).customer.name || null;
|
|
500
|
-
if (varName === "customer_email") return (document as any).customer.email || null;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
function replaceTemplateVariablesForPreview(
|
|
508
|
-
template: string,
|
|
509
|
-
entity?: Entity | null,
|
|
510
|
-
document?: Partial<Invoice | Estimate> | null,
|
|
511
|
-
): React.ReactNode[] {
|
|
512
|
-
if (!template) return [];
|
|
513
|
-
|
|
514
|
-
const parts: React.ReactNode[] = [];
|
|
515
|
-
const regex = /\{([^}]+)\}/g;
|
|
516
|
-
let lastIndex = 0;
|
|
517
|
-
let match: RegExpExecArray | null = null;
|
|
518
|
-
|
|
519
|
-
match = regex.exec(template);
|
|
520
|
-
while (match !== null) {
|
|
521
|
-
if (match.index > lastIndex) {
|
|
522
|
-
parts.push(template.slice(lastIndex, match.index));
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
const varName = match[1];
|
|
526
|
-
const actualValue = getVariableValue(varName, entity, document);
|
|
527
|
-
const displayValue = actualValue || formatVariableName(varName);
|
|
528
|
-
|
|
529
|
-
parts.push(
|
|
530
|
-
<span
|
|
531
|
-
key={match.index}
|
|
532
|
-
className={cn(
|
|
533
|
-
"rounded px-1.5 py-0.5 font-medium text-xs",
|
|
534
|
-
actualValue ? "bg-secondary text-secondary-foreground" : "bg-primary/10 text-primary",
|
|
535
|
-
)}
|
|
536
|
-
>
|
|
537
|
-
{displayValue}
|
|
538
|
-
</span>,
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
lastIndex = regex.lastIndex;
|
|
542
|
-
match = regex.exec(template);
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
if (lastIndex < template.length) {
|
|
546
|
-
parts.push(template.slice(lastIndex));
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return parts;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
531
|
export function DocumentNoteField({
|
|
553
532
|
control,
|
|
554
533
|
t,
|
|
@@ -732,6 +711,74 @@ export function DocumentTaxClauseField({
|
|
|
732
711
|
);
|
|
733
712
|
}
|
|
734
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Signature field component with smart code insertion button
|
|
716
|
+
* Wrapped in a collapsible for a clean form layout
|
|
717
|
+
*/
|
|
718
|
+
export function DocumentSignatureField({
|
|
719
|
+
control,
|
|
720
|
+
t,
|
|
721
|
+
entity,
|
|
722
|
+
document,
|
|
723
|
+
}: {
|
|
724
|
+
control: AnyControl;
|
|
725
|
+
t: (key: string) => string;
|
|
726
|
+
entity?: Entity | null;
|
|
727
|
+
document?: Partial<Invoice | Estimate> | null;
|
|
728
|
+
}) {
|
|
729
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
730
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
731
|
+
|
|
732
|
+
return (
|
|
733
|
+
<FormField
|
|
734
|
+
control={control}
|
|
735
|
+
name="signature"
|
|
736
|
+
render={({ field }) => {
|
|
737
|
+
const hasContent = field.value;
|
|
738
|
+
const showPreview = !isFocused && hasContent && entity;
|
|
739
|
+
const preview = showPreview ? replaceTemplateVariablesForPreview(field.value || "", entity, document) : null;
|
|
740
|
+
|
|
741
|
+
return (
|
|
742
|
+
<FormItem>
|
|
743
|
+
<div className="flex items-center justify-between">
|
|
744
|
+
<FormLabel>{t("Signature")}</FormLabel>
|
|
745
|
+
<SmartCodeInsertButton
|
|
746
|
+
textareaRef={textareaRef}
|
|
747
|
+
value={field.value || ""}
|
|
748
|
+
onInsert={(newValue) => field.onChange(newValue)}
|
|
749
|
+
t={t}
|
|
750
|
+
/>
|
|
751
|
+
</div>
|
|
752
|
+
<FormControl>
|
|
753
|
+
<div className="relative">
|
|
754
|
+
<Textarea
|
|
755
|
+
{...field}
|
|
756
|
+
ref={(e) => {
|
|
757
|
+
field.ref(e);
|
|
758
|
+
(textareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = e;
|
|
759
|
+
}}
|
|
760
|
+
value={field.value || ""}
|
|
761
|
+
placeholder={showPreview ? "" : t("Add signature text...")}
|
|
762
|
+
rows={2}
|
|
763
|
+
className={cn("resize-y", showPreview && "text-transparent caret-transparent")}
|
|
764
|
+
onFocus={() => setIsFocused(true)}
|
|
765
|
+
onBlur={() => setIsFocused(false)}
|
|
766
|
+
/>
|
|
767
|
+
{showPreview && (
|
|
768
|
+
<div className="pointer-events-none absolute inset-0 z-10 flex items-start overflow-hidden rounded-md border border-input bg-background px-3 py-2 shadow-xs">
|
|
769
|
+
<div className="w-full whitespace-pre-wrap text-base md:text-sm">{preview}</div>
|
|
770
|
+
</div>
|
|
771
|
+
)}
|
|
772
|
+
</div>
|
|
773
|
+
</FormControl>
|
|
774
|
+
<FormMessage />
|
|
775
|
+
</FormItem>
|
|
776
|
+
);
|
|
777
|
+
}}
|
|
778
|
+
/>
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
735
782
|
/**
|
|
736
783
|
* Payment terms field component with smart code insertion button
|
|
737
784
|
* Similar to DocumentNoteField, exported for use in document forms
|
|
@@ -799,3 +846,82 @@ export function DocumentPaymentTermsField({
|
|
|
799
846
|
/>
|
|
800
847
|
);
|
|
801
848
|
}
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Footer field component with collapsible wrapper and smart code insertion button
|
|
852
|
+
* Collapsed by default, opens if content exists
|
|
853
|
+
*/
|
|
854
|
+
export function DocumentFooterField({
|
|
855
|
+
control,
|
|
856
|
+
t,
|
|
857
|
+
entity,
|
|
858
|
+
document,
|
|
859
|
+
}: {
|
|
860
|
+
control: AnyControl;
|
|
861
|
+
t: (key: string) => string;
|
|
862
|
+
entity?: Entity | null;
|
|
863
|
+
document?: Partial<Invoice | Estimate> | null;
|
|
864
|
+
}) {
|
|
865
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
866
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
867
|
+
|
|
868
|
+
return (
|
|
869
|
+
<FormField
|
|
870
|
+
control={control}
|
|
871
|
+
name="footer"
|
|
872
|
+
render={({ field }) => {
|
|
873
|
+
const hasContent = field.value;
|
|
874
|
+
const showPreview = !isFocused && hasContent && entity;
|
|
875
|
+
const preview = showPreview ? replaceTemplateVariablesForPreview(field.value || "", entity, document) : null;
|
|
876
|
+
|
|
877
|
+
return (
|
|
878
|
+
<FormItem>
|
|
879
|
+
<Collapsible defaultOpen={!!hasContent}>
|
|
880
|
+
<div className="flex items-center justify-between">
|
|
881
|
+
<CollapsibleTrigger asChild>
|
|
882
|
+
<button type="button" className="flex items-center gap-1 font-medium text-sm">
|
|
883
|
+
<ChevronDown className="size-4 transition-transform [[data-panel-hidden]_&]:-rotate-90 [[data-panel-open]_&]:rotate-0" />
|
|
884
|
+
{t("Footer")}
|
|
885
|
+
</button>
|
|
886
|
+
</CollapsibleTrigger>
|
|
887
|
+
<div className="[[data-panel-hidden]_&]:hidden">
|
|
888
|
+
<SmartCodeInsertButton
|
|
889
|
+
textareaRef={textareaRef}
|
|
890
|
+
value={field.value || ""}
|
|
891
|
+
onInsert={(newValue) => field.onChange(newValue)}
|
|
892
|
+
t={t}
|
|
893
|
+
/>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
<CollapsibleContent className="mt-2">
|
|
897
|
+
<FormControl>
|
|
898
|
+
<div className="relative">
|
|
899
|
+
<Textarea
|
|
900
|
+
{...field}
|
|
901
|
+
ref={(e) => {
|
|
902
|
+
field.ref(e);
|
|
903
|
+
(textareaRef as React.MutableRefObject<HTMLTextAreaElement | null>).current = e;
|
|
904
|
+
}}
|
|
905
|
+
value={field.value || ""}
|
|
906
|
+
placeholder={showPreview ? "" : t("Add document footer...")}
|
|
907
|
+
rows={2}
|
|
908
|
+
className={cn("resize-y", showPreview && "text-transparent caret-transparent")}
|
|
909
|
+
onFocus={() => setIsFocused(true)}
|
|
910
|
+
onBlur={() => setIsFocused(false)}
|
|
911
|
+
/>
|
|
912
|
+
{showPreview && (
|
|
913
|
+
<div className="pointer-events-none absolute inset-0 z-10 flex items-start overflow-hidden rounded-md border border-input bg-background px-3 py-2 shadow-xs">
|
|
914
|
+
<div className="w-full whitespace-pre-wrap text-base md:text-sm">{preview}</div>
|
|
915
|
+
</div>
|
|
916
|
+
)}
|
|
917
|
+
</div>
|
|
918
|
+
</FormControl>
|
|
919
|
+
<FormMessage />
|
|
920
|
+
</CollapsibleContent>
|
|
921
|
+
</Collapsible>
|
|
922
|
+
</FormItem>
|
|
923
|
+
);
|
|
924
|
+
}}
|
|
925
|
+
/>
|
|
926
|
+
);
|
|
927
|
+
}
|