@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.
Files changed (115) hide show
  1. package/cli/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/src/components/advance-invoices/advance-invoices.hooks.ts +2 -2
  4. package/src/components/advance-invoices/create/create-advance-invoice-form.tsx +91 -35
  5. package/src/components/advance-invoices/create/locales/de.ts +5 -0
  6. package/src/components/advance-invoices/create/locales/es.ts +5 -0
  7. package/src/components/advance-invoices/create/locales/fr.ts +5 -0
  8. package/src/components/advance-invoices/create/locales/hr.ts +5 -0
  9. package/src/components/advance-invoices/create/locales/it.ts +5 -0
  10. package/src/components/advance-invoices/create/locales/nl.ts +5 -0
  11. package/src/components/advance-invoices/create/locales/pl.ts +5 -0
  12. package/src/components/advance-invoices/create/locales/pt.ts +5 -0
  13. package/src/components/advance-invoices/create/locales/sl.ts +5 -0
  14. package/src/components/advance-invoices/create/prepare-advance-invoice-submission.ts +5 -5
  15. package/src/components/credit-notes/create/create-credit-note-form.tsx +91 -35
  16. package/src/components/credit-notes/create/locales/de.ts +5 -0
  17. package/src/components/credit-notes/create/locales/es.ts +5 -0
  18. package/src/components/credit-notes/create/locales/fr.ts +5 -0
  19. package/src/components/credit-notes/create/locales/hr.ts +5 -0
  20. package/src/components/credit-notes/create/locales/it.ts +5 -0
  21. package/src/components/credit-notes/create/locales/nl.ts +5 -0
  22. package/src/components/credit-notes/create/locales/pl.ts +5 -0
  23. package/src/components/credit-notes/create/locales/pt.ts +5 -0
  24. package/src/components/credit-notes/create/locales/sl.ts +5 -0
  25. package/src/components/credit-notes/credit-notes.hooks.ts +2 -2
  26. package/src/components/delivery-notes/create/create-delivery-note-form.tsx +47 -0
  27. package/src/components/delivery-notes/create/locales/de.ts +5 -0
  28. package/src/components/delivery-notes/create/locales/es.ts +5 -0
  29. package/src/components/delivery-notes/create/locales/fr.ts +5 -0
  30. package/src/components/delivery-notes/create/locales/hr.ts +5 -0
  31. package/src/components/delivery-notes/create/locales/it.ts +5 -0
  32. package/src/components/delivery-notes/create/locales/nl.ts +5 -0
  33. package/src/components/delivery-notes/create/locales/pl.ts +5 -0
  34. package/src/components/delivery-notes/create/locales/pt.ts +5 -0
  35. package/src/components/delivery-notes/create/locales/sl.ts +5 -0
  36. package/src/components/documents/create/document-details-section.tsx +472 -346
  37. package/src/components/documents/create/prepare-document-submission.ts +3 -1
  38. package/src/components/documents/create/smart-code-insert-button.tsx +6 -0
  39. package/src/components/documents/view/document-details-card.tsx +6 -0
  40. package/src/components/documents/view/locales/de.ts +1 -0
  41. package/src/components/documents/view/locales/es.ts +1 -0
  42. package/src/components/documents/view/locales/fr.ts +1 -0
  43. package/src/components/documents/view/locales/hr.ts +1 -0
  44. package/src/components/documents/view/locales/it.ts +1 -0
  45. package/src/components/documents/view/locales/nl.ts +1 -0
  46. package/src/components/documents/view/locales/pl.ts +1 -0
  47. package/src/components/documents/view/locales/pt.ts +1 -0
  48. package/src/components/documents/view/locales/sl.ts +1 -0
  49. package/src/components/entities/entity-settings-form/email-template-variables-info.tsx +6 -0
  50. package/src/components/entities/entity-settings-form/input-with-preview.tsx +2 -145
  51. package/src/components/entities/entity-settings-form/locales/de.ts +4 -0
  52. package/src/components/entities/entity-settings-form/locales/es.ts +4 -0
  53. package/src/components/entities/entity-settings-form/locales/fr.ts +4 -0
  54. package/src/components/entities/entity-settings-form/locales/hr.ts +4 -0
  55. package/src/components/entities/entity-settings-form/locales/it.ts +4 -0
  56. package/src/components/entities/entity-settings-form/locales/nl.ts +4 -0
  57. package/src/components/entities/entity-settings-form/locales/pl.ts +4 -0
  58. package/src/components/entities/entity-settings-form/locales/pt.ts +4 -0
  59. package/src/components/entities/entity-settings-form/locales/sl.ts +4 -0
  60. package/src/components/entities/fina-settings-form/fina-settings-form.tsx +15 -0
  61. package/src/components/entities/fina-settings-form/fina-settings.hooks.ts +5 -1
  62. package/src/components/entities/fina-settings-form/locales/de.ts +3 -0
  63. package/src/components/entities/fina-settings-form/locales/en.ts +3 -0
  64. package/src/components/entities/fina-settings-form/locales/es.ts +3 -0
  65. package/src/components/entities/fina-settings-form/locales/fr.ts +3 -0
  66. package/src/components/entities/fina-settings-form/locales/hr.ts +3 -0
  67. package/src/components/entities/fina-settings-form/locales/it.ts +3 -0
  68. package/src/components/entities/fina-settings-form/locales/nl.ts +3 -0
  69. package/src/components/entities/fina-settings-form/locales/pl.ts +3 -0
  70. package/src/components/entities/fina-settings-form/locales/pt.ts +3 -0
  71. package/src/components/entities/fina-settings-form/locales/sl.ts +3 -0
  72. package/src/components/entities/fina-settings-form/sections/premises-management-section.tsx +4 -4
  73. package/src/components/entities/fina-settings-form/sections/register-premise-dialog.tsx +3 -3
  74. package/src/components/entities/settings/defaults-settings-form.tsx +38 -1
  75. package/src/components/entities/settings/tax-rules-settings-form.tsx +1 -2
  76. package/src/components/estimates/create/create-estimate-form.tsx +43 -2
  77. package/src/components/estimates/create/locales/de.ts +5 -0
  78. package/src/components/estimates/create/locales/es.ts +5 -0
  79. package/src/components/estimates/create/locales/fr.ts +5 -0
  80. package/src/components/estimates/create/locales/hr.ts +5 -0
  81. package/src/components/estimates/create/locales/it.ts +5 -0
  82. package/src/components/estimates/create/locales/nl.ts +5 -0
  83. package/src/components/estimates/create/locales/pl.ts +5 -0
  84. package/src/components/estimates/create/locales/pt.ts +5 -0
  85. package/src/components/estimates/create/locales/sl.ts +5 -0
  86. package/src/components/invoices/create/create-invoice-form.tsx +130 -40
  87. package/src/components/invoices/create/locales/de.ts +13 -0
  88. package/src/components/invoices/create/locales/es.ts +13 -0
  89. package/src/components/invoices/create/locales/fr.ts +13 -0
  90. package/src/components/invoices/create/locales/hr.ts +13 -0
  91. package/src/components/invoices/create/locales/it.ts +13 -0
  92. package/src/components/invoices/create/locales/nl.ts +13 -0
  93. package/src/components/invoices/create/locales/pl.ts +13 -0
  94. package/src/components/invoices/create/locales/pt.ts +13 -0
  95. package/src/components/invoices/create/locales/sl.ts +13 -0
  96. package/src/components/invoices/create/prepare-invoice-submission.ts +5 -5
  97. package/src/components/invoices/invoices.hooks.ts +2 -2
  98. package/src/components/table/table-pagination.tsx +1 -1
  99. package/src/generated/schemas/advanceinvoice.ts +2 -0
  100. package/src/generated/schemas/creditnote.ts +1 -0
  101. package/src/generated/schemas/deliverynote.ts +1 -0
  102. package/src/generated/schemas/entity.ts +4 -4
  103. package/src/generated/schemas/entityapikey.ts +19 -0
  104. package/src/generated/schemas/estimate.ts +2 -0
  105. package/src/generated/schemas/index.ts +1 -0
  106. package/src/generated/schemas/invoice.ts +2 -0
  107. package/src/generated/schemas/renderadvanceinvoicepreview_body.ts +1 -1
  108. package/src/generated/schemas/rendercreditnotepreview_body.ts +1 -1
  109. package/src/generated/schemas/renderdeliverynotepreview_body.ts +1 -1
  110. package/src/generated/schemas/renderestimatepreview_body.ts +1 -1
  111. package/src/generated/schemas/renderinvoicepreview_body.ts +1 -1
  112. package/src/generated/schemas/startpdfexport_body.ts +14 -2
  113. package/src/generated/schemas/webhook.ts +4 -0
  114. package/src/lib/template-variables.tsx +167 -0
  115. 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
- premise_id: string;
47
+ business_premise_name: string;
46
48
  };
47
49
 
48
50
  type FinaDevice = {
49
51
  id: string;
50
- device_id: string;
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-4">
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 inline (Premise | Device | Number) */}
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
- <FormLabel>{t("Number")} *</FormLabel>
107
- {showFursSelects ? (
108
- <div className="flex gap-2">
109
- <Select
110
- value={fursInline.selectedPremise || ""}
111
- onValueChange={(v) => fursInline.onPremiseChange(v ?? undefined)}
112
- >
113
- <SelectTrigger className="w-24">
114
- <SelectValue placeholder={t("Premise")} />
115
- </SelectTrigger>
116
- <SelectContent>
117
- {fursInline.premises.map((premise) => (
118
- <SelectItem key={premise.id} value={premise.business_premise_name}>
119
- {premise.business_premise_name}
120
- </SelectItem>
121
- ))}
122
- </SelectContent>
123
- </Select>
124
- <Select
125
- value={fursInline.selectedDevice || ""}
126
- onValueChange={(v) => fursInline.onDeviceChange(v ?? undefined)}
127
- disabled={!fursInline.selectedPremise || fursInline.devices.length === 0}
128
- >
129
- <SelectTrigger className="w-24">
130
- <SelectValue placeholder={t("Device")} />
131
- </SelectTrigger>
132
- <SelectContent>
133
- {fursInline.devices.map((device) => (
134
- <SelectItem key={device.id} value={device.electronic_device_name}>
135
- {device.electronic_device_name}
136
- </SelectItem>
137
- ))}
138
- </SelectContent>
139
- </Select>
140
- <Tooltip>
141
- <TooltipTrigger asChild>
142
- <FormControl>
143
- <Input {...field} disabled className="flex-1" />
144
- </FormControl>
145
- </TooltipTrigger>
146
- <TooltipContent>
147
- <p>{t("Number format can be changed in settings")}</p>
148
- </TooltipContent>
149
- </Tooltip>
150
- </div>
151
- ) : showFinaSelects ? (
152
- <div className="flex gap-2">
153
- <Select
154
- value={finaInline.selectedPremise || ""}
155
- onValueChange={(v) => finaInline.onPremiseChange(v ?? undefined)}
156
- >
157
- <SelectTrigger className="w-24">
158
- <SelectValue placeholder={t("Premise")} />
159
- </SelectTrigger>
160
- <SelectContent>
161
- {finaInline.premises.map((premise) => (
162
- <SelectItem key={premise.id} value={premise.premise_id}>
163
- {premise.premise_id}
164
- </SelectItem>
165
- ))}
166
- </SelectContent>
167
- </Select>
168
- <Select
169
- value={finaInline.selectedDevice || ""}
170
- onValueChange={(v) => finaInline.onDeviceChange(v ?? undefined)}
171
- disabled={!finaInline.selectedPremise || finaInline.devices.length === 0}
172
- >
173
- <SelectTrigger className="w-24">
174
- <SelectValue placeholder={t("Device")} />
175
- </SelectTrigger>
176
- <SelectContent>
177
- {finaInline.devices.map((device) => (
178
- <SelectItem key={device.id} value={device.device_id}>
179
- {device.device_id}
180
- </SelectItem>
181
- ))}
182
- </SelectContent>
183
- </Select>
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
- </div>
195
- ) : (
196
- <Tooltip>
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
- <FormLabel className="">{t("Date")} *</FormLabel>
218
- {showFinaSelects ? (
219
- <Tooltip>
220
- <TooltipTrigger asChild>
221
- <FormControl>
222
- <Button variant="outline" disabled className="w-full pl-3 text-left font-normal">
223
- {new Date().toLocaleDateString()}
224
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
225
- </Button>
226
- </FormControl>
227
- </TooltipTrigger>
228
- <TooltipContent>
229
- <p>{t("FINA fiscalized invoices always use the current date")}</p>
230
- </TooltipContent>
231
- </Tooltip>
232
- ) : (
233
- <Popover>
234
- <PopoverTrigger asChild>
235
- <FormControl>
236
- <Button
237
- variant="outline"
238
- className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
239
- >
240
- {field.value ? new Date(field.value).toLocaleDateString() : <span>{t("Pick a date")}</span>}
241
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
242
- </Button>
243
- </FormControl>
244
- </PopoverTrigger>
245
- <PopoverContent className="w-auto p-0" align="start">
246
- <Calendar
247
- mode="single"
248
- selected={field.value ? new Date(field.value) : undefined}
249
- onSelect={(date) => field.onChange(date?.toISOString())}
250
- disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
251
- initialFocus
252
- />
253
- </PopoverContent>
254
- </Popover>
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 - Invoice only */}
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 justify-between">
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 className="h-7 w-auto gap-1 border-none px-2 font-normal text-xs shadow-none">
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
- </div>
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("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
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("From")}</span>}
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
- <FormField
331
- control={control}
332
- name="date_service_to"
333
- render={({ field: toField }) => (
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
- {toField.value ? new Date(toField.value).toLocaleDateString() : <span>{t("To")}</span>}
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
- </PopoverTrigger>
347
- <PopoverContent className="w-auto p-0" align="start">
348
- <Calendar
349
- mode="single"
350
- selected={toField.value ? new Date(toField.value) : undefined}
351
- onSelect={(date) => toField.onChange(date?.toISOString())}
352
- initialFocus
353
- />
354
- </PopoverContent>
355
- </Popover>
356
- )}
357
- />
358
- </div>
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
- <FormLabel className="">{dateFieldLabel}</FormLabel>
373
- <Popover>
374
- <PopoverTrigger asChild>
375
- <FormControl>
376
- <Button
377
- variant="outline"
378
- className={cn("w-full pl-3 text-left font-normal", !field.value && "text-muted-foreground")}
379
- >
380
- {field.value ? new Date(field.value).toLocaleDateString() : <span>{t("Pick a date")}</span>}
381
- <CalendarIcon className="ml-auto h-4 w-4 opacity-50" />
382
- </Button>
383
- </FormControl>
384
- </PopoverTrigger>
385
- <PopoverContent className="w-auto p-0" align="start">
386
- <Calendar
387
- mode="single"
388
- selected={field.value ? new Date(field.value) : undefined}
389
- onSelect={(date) => field.onChange(date?.toISOString())}
390
- disabled={(date) => date < new Date("1900-01-01")}
391
- initialFocus
392
- />
393
- </PopoverContent>
394
- </Popover>
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="currency_code"
480
+ name="reference"
404
481
  render={({ field }) => (
405
482
  <FormItem>
406
- <FormLabel>{t("Currency")} *</FormLabel>
407
- <Select onValueChange={(value) => value && field.onChange(value)} value={field.value || ""}>
483
+ <div className="flex items-center gap-3">
484
+ <FormLabel className={LABEL_WIDTH}>{t("Reference")}</FormLabel>
408
485
  <FormControl>
409
- <SelectTrigger className="h-10">
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
- <SelectContent>
414
- {CURRENCY_CODES.map((currency) => (
415
- <SelectItem key={currency.value} value={currency.value}>
416
- {currency.label}
417
- </SelectItem>
418
- ))}
419
- </SelectContent>
420
- </Select>
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
+ }