@voyantjs/finance-ui 0.30.7 → 0.31.0

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 (39) hide show
  1. package/README.md +18 -0
  2. package/dist/components/invoice-detail-page.d.ts +113 -0
  3. package/dist/components/invoice-detail-page.d.ts.map +1 -0
  4. package/dist/components/invoice-detail-page.js +142 -0
  5. package/dist/components/invoices-page-skeleton.d.ts +5 -0
  6. package/dist/components/invoices-page-skeleton.d.ts.map +1 -0
  7. package/dist/components/invoices-page-skeleton.js +13 -0
  8. package/dist/components/invoices-page.d.ts +6 -0
  9. package/dist/components/invoices-page.d.ts.map +1 -0
  10. package/dist/components/invoices-page.js +109 -0
  11. package/dist/components/payment-detail-page.d.ts +41 -0
  12. package/dist/components/payment-detail-page.d.ts.map +1 -0
  13. package/dist/components/payment-detail-page.js +79 -0
  14. package/dist/components/payments-page-skeleton.d.ts +5 -0
  15. package/dist/components/payments-page-skeleton.d.ts.map +1 -0
  16. package/dist/components/payments-page-skeleton.js +13 -0
  17. package/dist/components/payments-page.d.ts +20 -0
  18. package/dist/components/payments-page.d.ts.map +1 -0
  19. package/dist/components/payments-page.js +151 -0
  20. package/dist/components/taxes-page.d.ts +11 -0
  21. package/dist/components/taxes-page.d.ts.map +1 -0
  22. package/dist/components/taxes-page.js +718 -0
  23. package/dist/i18n/en.d.ts +308 -0
  24. package/dist/i18n/en.d.ts.map +1 -1
  25. package/dist/i18n/en.js +308 -0
  26. package/dist/i18n/index.d.ts +1 -1
  27. package/dist/i18n/index.d.ts.map +1 -1
  28. package/dist/i18n/messages.d.ts +193 -1
  29. package/dist/i18n/messages.d.ts.map +1 -1
  30. package/dist/i18n/messages.js +11 -0
  31. package/dist/i18n/provider.d.ts +616 -0
  32. package/dist/i18n/provider.d.ts.map +1 -1
  33. package/dist/i18n/ro.d.ts +308 -0
  34. package/dist/i18n/ro.d.ts.map +1 -1
  35. package/dist/i18n/ro.js +308 -0
  36. package/dist/index.d.ts +7 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +7 -0
  39. package/package.json +9 -9
@@ -0,0 +1,718 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
4
+ import { useVoyantFinanceContext } from "@voyantjs/finance-react";
5
+ import { Badge, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Sheet, SheetBody, SheetContent, SheetFooter, SheetHeader, SheetTitle, Switch, Textarea, } from "@voyantjs/ui/components";
6
+ import { Skeleton } from "@voyantjs/ui/components/skeleton";
7
+ import { Loader2, MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react";
8
+ import { createContext, useContext, useEffect, useMemo, useState } from "react";
9
+ import { useFinanceUiMessagesOrDefault } from "../i18n/index.js";
10
+ const TAX_CODE_OPTIONS = [
11
+ "standard",
12
+ "reduced",
13
+ "exempt",
14
+ "reverse_charge",
15
+ "margin_scheme_art311",
16
+ "zero_rated",
17
+ "out_of_scope",
18
+ "other",
19
+ ];
20
+ const TaxesPageApiContext = createContext(null);
21
+ function joinUrl(baseUrl, path) {
22
+ const trimmedBase = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
23
+ const trimmedPath = path.startsWith("/") ? path : `/${path}`;
24
+ return `${trimmedBase}${trimmedPath}`;
25
+ }
26
+ async function readJson(response) {
27
+ if (!response.ok) {
28
+ let body;
29
+ try {
30
+ body = await response.json();
31
+ }
32
+ catch {
33
+ body = await response.text().catch(() => undefined);
34
+ }
35
+ const message = typeof body === "object" && body !== null && "error" in body
36
+ ? String(body.error)
37
+ : `API error: ${response.status} ${response.statusText}`;
38
+ throw new Error(message);
39
+ }
40
+ if (response.status === 204)
41
+ return undefined;
42
+ return response.json();
43
+ }
44
+ function createTaxesPageApi(baseUrl, fetcher) {
45
+ const request = async (path, init = {}) => {
46
+ const headers = new Headers(init.headers);
47
+ if (init.body !== undefined && !headers.has("Content-Type")) {
48
+ headers.set("Content-Type", "application/json");
49
+ }
50
+ return readJson(await fetcher(joinUrl(baseUrl, path), { ...init, headers }));
51
+ };
52
+ const api = {
53
+ get: (path) => request(path, { method: "GET" }),
54
+ post: (path, body) => request(path, {
55
+ method: "POST",
56
+ body: body !== undefined ? JSON.stringify(body) : undefined,
57
+ }),
58
+ patch: (path, body) => request(path, {
59
+ method: "PATCH",
60
+ body: body !== undefined ? JSON.stringify(body) : undefined,
61
+ }),
62
+ delete: (path) => request(path, { method: "DELETE" }),
63
+ };
64
+ return api;
65
+ }
66
+ function useTaxesPageApi() {
67
+ const api = useContext(TaxesPageApiContext);
68
+ if (!api)
69
+ throw new Error("TaxesPage requires a TaxesPageApiContext provider");
70
+ return api;
71
+ }
72
+ const TAX_CLASS_APPLIES_TO_OPTIONS = ["base", "addon", "accommodation", "all"];
73
+ const TAX_POLICY_CONDITION_FACT_OPTIONS = [
74
+ "hasAccommodation",
75
+ "accommodationCountries",
76
+ ];
77
+ const EMPTY_FORM = {
78
+ taxClassLabel: "",
79
+ taxClassCode: "",
80
+ taxClassDescription: "",
81
+ regimeName: "",
82
+ regimeCode: "standard",
83
+ jurisdiction: "RO",
84
+ ratePercent: "0",
85
+ regimeDescription: "",
86
+ legalReference: "",
87
+ lines: [],
88
+ active: true,
89
+ };
90
+ const EMPTY_POLICY_PROFILE_FORM = {
91
+ name: "",
92
+ code: "",
93
+ jurisdiction: "RO",
94
+ description: "",
95
+ active: true,
96
+ };
97
+ const EMPTY_POLICY_RULE_FORM = {
98
+ profileId: "",
99
+ side: "sell",
100
+ priority: "100",
101
+ name: "",
102
+ appliesTo: "all",
103
+ conditionMode: "always",
104
+ conditions: [],
105
+ taxRegimeId: "",
106
+ active: true,
107
+ };
108
+ let taxClassLineKey = 0;
109
+ let taxPolicyConditionKey = 0;
110
+ function nextTaxClassLineKey(seed = "line") {
111
+ taxClassLineKey += 1;
112
+ return `${seed}-${taxClassLineKey}`;
113
+ }
114
+ function nextTaxPolicyConditionKey(seed = "condition") {
115
+ taxPolicyConditionKey += 1;
116
+ return `${seed}-${taxPolicyConditionKey}`;
117
+ }
118
+ function initialForm(row) {
119
+ if (!row)
120
+ return EMPTY_FORM;
121
+ return {
122
+ taxClassLabel: row.taxClass.label,
123
+ taxClassCode: row.taxClass.code,
124
+ taxClassDescription: row.taxClass.description ?? "",
125
+ regimeName: row.regime?.name ?? row.taxClass.label,
126
+ regimeCode: row.regime?.code ?? "other",
127
+ jurisdiction: row.regime?.jurisdiction ?? "RO",
128
+ ratePercent: row.regime?.ratePercent != null ? String(row.regime.ratePercent) : "0",
129
+ regimeDescription: row.regime?.description ?? "",
130
+ legalReference: row.regime?.legalReference ?? "",
131
+ lines: (row.taxClass.lines ?? []).map((line) => ({
132
+ key: nextTaxClassLineKey(`${line.applies_to}-${line.regime_id}`),
133
+ appliesTo: line.applies_to,
134
+ regimeId: line.regime_id,
135
+ })),
136
+ active: row.taxClass.active,
137
+ };
138
+ }
139
+ function initialPolicyProfileForm(profile) {
140
+ if (!profile)
141
+ return EMPTY_POLICY_PROFILE_FORM;
142
+ return {
143
+ name: profile.name,
144
+ code: profile.code,
145
+ jurisdiction: profile.jurisdiction ?? "",
146
+ description: profile.description ?? "",
147
+ active: profile.active,
148
+ };
149
+ }
150
+ function initialPolicyRuleForm(rule, profileId, taxRegimeId) {
151
+ if (!rule) {
152
+ return {
153
+ ...EMPTY_POLICY_RULE_FORM,
154
+ profileId,
155
+ taxRegimeId,
156
+ };
157
+ }
158
+ return {
159
+ profileId: rule.profileId,
160
+ side: rule.side,
161
+ priority: String(rule.priority),
162
+ name: rule.name,
163
+ appliesTo: rule.appliesTo,
164
+ ...parsePolicyCondition(rule.condition),
165
+ taxRegimeId: rule.taxRegimeId,
166
+ active: rule.active,
167
+ };
168
+ }
169
+ function parsePolicyCondition(condition) {
170
+ if (!condition || condition.always === true) {
171
+ return { conditionMode: "always", conditions: [] };
172
+ }
173
+ const group = Array.isArray(condition.all)
174
+ ? { mode: "all", expressions: condition.all }
175
+ : Array.isArray(condition.any)
176
+ ? { mode: "any", expressions: condition.any }
177
+ : { mode: "all", expressions: [condition] };
178
+ const conditions = group.expressions
179
+ .map(parsePolicyConditionExpression)
180
+ .filter((row) => Boolean(row));
181
+ return {
182
+ conditionMode: conditions.length ? group.mode : "always",
183
+ conditions,
184
+ };
185
+ }
186
+ function parsePolicyConditionExpression(expression) {
187
+ if (typeof expression !== "object" || expression === null || Array.isArray(expression)) {
188
+ return null;
189
+ }
190
+ const record = expression;
191
+ if (record.fact === "hasAccommodation") {
192
+ return {
193
+ key: nextTaxPolicyConditionKey("has-accommodation"),
194
+ fact: "hasAccommodation",
195
+ operator: "eq",
196
+ value: record.eq === false ? "false" : "true",
197
+ };
198
+ }
199
+ if (record.fact === "accommodationCountries") {
200
+ return {
201
+ key: nextTaxPolicyConditionKey("accommodation-countries"),
202
+ fact: "accommodationCountries",
203
+ operator: "contains",
204
+ value: typeof record.contains === "string" ? record.contains : "RO",
205
+ };
206
+ }
207
+ return null;
208
+ }
209
+ function toSlug(value) {
210
+ return value
211
+ .trim()
212
+ .toLowerCase()
213
+ .normalize("NFKD")
214
+ .replace(/[\u0300-\u036f]/g, "")
215
+ .replace(/[^a-z0-9]+/g, "-")
216
+ .replace(/^-+|-+$/g, "");
217
+ }
218
+ function formatRate(value) {
219
+ return value == null ? "-" : `${value}%`;
220
+ }
221
+ function appliesToLabel(messages, appliesTo) {
222
+ const taxMessages = messages.taxesPage;
223
+ switch (appliesTo) {
224
+ case "base":
225
+ return taxMessages.appliesToBase;
226
+ case "addon":
227
+ return taxMessages.appliesToAddon;
228
+ case "accommodation":
229
+ return taxMessages.appliesToAccommodation;
230
+ case "all":
231
+ return taxMessages.appliesToAll;
232
+ }
233
+ }
234
+ function summarizeCondition(messages, condition) {
235
+ const taxMessages = messages.taxesPage;
236
+ if (!condition)
237
+ return "-";
238
+ if (condition.always === true)
239
+ return taxMessages.policyConditionAlways;
240
+ const parsed = parsePolicyCondition(condition);
241
+ if (parsed.conditionMode === "always")
242
+ return taxMessages.policyConditionAlways;
243
+ const prefix = parsed.conditionMode === "all"
244
+ ? taxMessages.policyConditionModeAll
245
+ : taxMessages.policyConditionModeAny;
246
+ const labels = parsed.conditions.map((row) => summarizeConditionRow(messages, row));
247
+ return `${prefix}: ${labels.join("; ")}`;
248
+ }
249
+ function summarizeConditionRow(messages, condition) {
250
+ const taxMessages = messages.taxesPage;
251
+ if (condition.fact === "hasAccommodation") {
252
+ return `${taxMessages.policyFactHasAccommodation} ${condition.value === "false" ? taxMessages.policyValueNo : taxMessages.policyValueYes}`;
253
+ }
254
+ if (condition.fact === "accommodationCountries") {
255
+ return `${taxMessages.policyFactAccommodationCountries} ${taxMessages.policyOperatorContains} ${condition.value}`;
256
+ }
257
+ return "custom";
258
+ }
259
+ function normalizeCondition(condition) {
260
+ if (condition.fact === "hasAccommodation") {
261
+ return {
262
+ ...condition,
263
+ operator: "eq",
264
+ value: condition.value === "false" ? "false" : "true",
265
+ };
266
+ }
267
+ return {
268
+ ...condition,
269
+ operator: "contains",
270
+ value: condition.value.trim().toUpperCase() || "RO",
271
+ };
272
+ }
273
+ function buildPolicyCondition(form, taxMessages) {
274
+ if (form.conditionMode === "always") {
275
+ return { always: true };
276
+ }
277
+ const expressions = form.conditions.map((row) => {
278
+ const condition = normalizeCondition(row);
279
+ if (condition.fact === "hasAccommodation") {
280
+ return { fact: "hasAccommodation", eq: condition.value === "true" };
281
+ }
282
+ if (!/^[A-Z]{2}$/.test(condition.value)) {
283
+ throw new Error(taxMessages.validationPolicyRuleConditionInvalid);
284
+ }
285
+ return { fact: "accommodationCountries", contains: condition.value };
286
+ });
287
+ if (!expressions.length) {
288
+ throw new Error(taxMessages.validationPolicyRuleConditionInvalid);
289
+ }
290
+ return form.conditionMode === "all" ? { all: expressions } : { any: expressions };
291
+ }
292
+ export function TaxesPage({ api: apiProp } = {}) {
293
+ if (apiProp)
294
+ return _jsx(TaxesPageContent, { api: apiProp });
295
+ return _jsx(TaxesPageWithDefaultApi, {});
296
+ }
297
+ function TaxesPageWithDefaultApi() {
298
+ const { baseUrl, fetcher } = useVoyantFinanceContext();
299
+ const api = useMemo(() => createTaxesPageApi(baseUrl, fetcher), [baseUrl, fetcher]);
300
+ return _jsx(TaxesPageContent, { api: api });
301
+ }
302
+ function TaxesPageContent({ api }) {
303
+ const messages = useFinanceUiMessagesOrDefault();
304
+ const taxMessages = messages.taxesPage;
305
+ const queryClient = useQueryClient();
306
+ const [sheetOpen, setSheetOpen] = useState(false);
307
+ const [editing, setEditing] = useState();
308
+ const [profileSheetOpen, setProfileSheetOpen] = useState(false);
309
+ const [editingProfile, setEditingProfile] = useState();
310
+ const [ruleSheetOpen, setRuleSheetOpen] = useState(false);
311
+ const [editingRule, setEditingRule] = useState();
312
+ const [ruleProfileId, setRuleProfileId] = useState("");
313
+ const taxClassesQuery = useQuery({
314
+ queryKey: ["tax-classes"],
315
+ queryFn: () => api.get("/v1/finance/tax-classes?limit=100"),
316
+ });
317
+ const taxRegimesQuery = useQuery({
318
+ queryKey: ["tax-regimes"],
319
+ queryFn: () => api.get("/v1/finance/tax-regimes?limit=100"),
320
+ });
321
+ const policyProfilesQuery = useQuery({
322
+ queryKey: ["tax-policy-profiles"],
323
+ queryFn: () => api.get("/v1/finance/tax-policy-profiles?limit=100"),
324
+ });
325
+ const policyRulesQuery = useQuery({
326
+ queryKey: ["tax-policy-rules"],
327
+ queryFn: () => api.get("/v1/finance/tax-policy-rules?limit=100"),
328
+ });
329
+ const regimesById = useMemo(() => new Map((taxRegimesQuery.data?.data ?? []).map((regime) => [regime.id, regime])), [taxRegimesQuery.data]);
330
+ const rows = useMemo(() => (taxClassesQuery.data?.data ?? []).map((taxClass) => ({
331
+ taxClass,
332
+ regime: taxClass.defaultRegimeId
333
+ ? (regimesById.get(taxClass.defaultRegimeId) ?? null)
334
+ : null,
335
+ })), [regimesById, taxClassesQuery.data]);
336
+ const policyRulesByProfileId = useMemo(() => {
337
+ const grouped = new Map();
338
+ for (const rule of policyRulesQuery.data?.data ?? []) {
339
+ const existing = grouped.get(rule.profileId) ?? [];
340
+ existing.push(rule);
341
+ grouped.set(rule.profileId, existing);
342
+ }
343
+ for (const rules of grouped.values()) {
344
+ rules.sort((a, b) => a.priority - b.priority || a.name.localeCompare(b.name));
345
+ }
346
+ return grouped;
347
+ }, [policyRulesQuery.data]);
348
+ const isPending = taxClassesQuery.isPending ||
349
+ taxRegimesQuery.isPending ||
350
+ policyProfilesQuery.isPending ||
351
+ policyRulesQuery.isPending;
352
+ const deleteMutation = useMutation({
353
+ mutationFn: async (row) => {
354
+ await api.delete(`/v1/finance/tax-classes/${row.taxClass.id}`);
355
+ },
356
+ onSuccess: () => {
357
+ void queryClient.invalidateQueries({ queryKey: ["tax-classes"] });
358
+ },
359
+ });
360
+ const deleteProfileMutation = useMutation({
361
+ mutationFn: async (profile) => {
362
+ const rules = policyRulesByProfileId.get(profile.id) ?? [];
363
+ await Promise.all(rules.map((rule) => api.delete(`/v1/finance/tax-policy-rules/${rule.id}`)));
364
+ await api.delete(`/v1/finance/tax-policy-profiles/${profile.id}`);
365
+ },
366
+ onSuccess: () => {
367
+ void queryClient.invalidateQueries({ queryKey: ["tax-policy-profiles"] });
368
+ void queryClient.invalidateQueries({ queryKey: ["tax-policy-rules"] });
369
+ },
370
+ });
371
+ const deleteRuleMutation = useMutation({
372
+ mutationFn: async (rule) => {
373
+ await api.delete(`/v1/finance/tax-policy-rules/${rule.id}`);
374
+ },
375
+ onSuccess: () => {
376
+ void queryClient.invalidateQueries({ queryKey: ["tax-policy-rules"] });
377
+ },
378
+ });
379
+ return (_jsx(TaxesPageApiContext.Provider, { value: api, children: _jsxs("div", { className: "flex flex-col gap-6 p-6", children: [_jsxs("div", { className: "flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold tracking-tight", children: taxMessages.title }), _jsx("p", { className: "text-sm text-muted-foreground", children: taxMessages.description })] }), _jsxs(Button, { size: "sm", onClick: () => {
380
+ setEditing(undefined);
381
+ setSheetOpen(true);
382
+ }, children: [_jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }), taxMessages.addTax] })] }), isPending ? (_jsx(TaxesPageSkeleton, { rows: 5 })) : (_jsx("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: rows.length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-muted-foreground", children: taxMessages.empty })) : (_jsx("div", { className: "flex flex-col divide-y", children: rows.map((row) => {
383
+ const overrideLines = row.taxClass.lines ?? [];
384
+ return (_jsxs("div", { className: "flex items-center justify-between px-6 py-3", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: row.taxClass.label }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: row.taxClass.code }), _jsx(Badge, { variant: "secondary", className: "text-xs", children: taxMessages.taxClassBadge }), overrideLines.length ? (_jsx(Badge, { variant: "outline", className: "text-xs", children: taxMessages.regimeOverrideCount.replace("{count}", String(overrideLines.length)) })) : null] }), _jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-2 text-xs text-muted-foreground", children: [_jsx("span", { children: taxMessages.defaultRegimeLabel }), _jsx("span", { children: row.regime?.name ?? "-" }), _jsx("span", { className: "font-mono", children: row.regime?.code ?? "-" }), _jsx(Badge, { variant: "outline", className: "text-xs", children: formatRate(row.regime?.ratePercent ?? null) }), !row.taxClass.active ? (_jsx(Badge, { variant: "secondary", className: "text-xs", children: taxMessages.inactive })) : null] }), overrideLines.length ? (_jsxs("div", { className: "mt-1 flex flex-wrap items-center gap-1.5 text-xs text-muted-foreground", children: [_jsx("span", { children: taxMessages.regimeOverridesLabel }), overrideLines.map((line) => {
385
+ const regime = regimesById.get(line.regime_id);
386
+ return (_jsxs(Badge, { variant: "outline", className: "text-xs", children: [appliesToLabel(messages, line.applies_to), ":", " ", regime?.name ?? line.regime_id] }, `${line.applies_to}-${line.regime_id}`));
387
+ })] })) : null, row.taxClass.description || row.regime?.description ? (_jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: row.taxClass.description ?? row.regime?.description })) : null] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-muted-foreground", children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => {
388
+ setEditing(row);
389
+ setSheetOpen(true);
390
+ }, children: [_jsx(Pencil, { className: "h-4 w-4" }), taxMessages.edit] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => {
391
+ if (confirm(taxMessages.deleteConfirm)) {
392
+ deleteMutation.mutate(row);
393
+ }
394
+ }, children: [_jsx(Trash2, { className: "h-4 w-4" }), taxMessages.delete] })] })] })] }, row.taxClass.id));
395
+ }) })) })), _jsx(TaxSheet, { open: sheetOpen, onOpenChange: setSheetOpen, row: editing, onSuccess: () => {
396
+ setSheetOpen(false);
397
+ setEditing(undefined);
398
+ void queryClient.invalidateQueries({ queryKey: ["tax-classes"] });
399
+ void queryClient.invalidateQueries({ queryKey: ["tax-regimes"] });
400
+ }, taxRegimes: taxRegimesQuery.data?.data ?? [] }), _jsxs("div", { className: "mt-2 flex items-center justify-between", children: [_jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold tracking-tight", children: taxMessages.policyTitle }), _jsx("p", { className: "text-sm text-muted-foreground", children: taxMessages.policyDescription })] }), _jsxs(Button, { size: "sm", onClick: () => {
401
+ setEditingProfile(undefined);
402
+ setProfileSheetOpen(true);
403
+ }, children: [_jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }), taxMessages.addPolicyProfile] })] }), isPending ? null : (_jsx("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: (policyProfilesQuery.data?.data ?? []).length === 0 ? (_jsx("p", { className: "py-12 text-center text-sm text-muted-foreground", children: taxMessages.policyEmpty })) : (_jsx("div", { className: "flex flex-col divide-y", children: (policyProfilesQuery.data?.data ?? []).map((profile) => {
404
+ const profileRules = policyRulesByProfileId.get(profile.id) ?? [];
405
+ return (_jsxs("div", { className: "flex flex-col gap-3 px-6 py-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { className: "min-w-0", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsx("span", { className: "text-sm font-medium", children: profile.name }), _jsx("span", { className: "font-mono text-xs text-muted-foreground", children: profile.code }), profile.jurisdiction ? (_jsx(Badge, { variant: "outline", className: "text-xs", children: profile.jurisdiction })) : null, !profile.active ? (_jsx(Badge, { variant: "secondary", className: "text-xs", children: taxMessages.inactive })) : null] }), profile.description ? (_jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: profile.description })) : null] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: () => {
406
+ setEditingRule(undefined);
407
+ setRuleProfileId(profile.id);
408
+ setRuleSheetOpen(true);
409
+ }, children: [_jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }), taxMessages.addPolicyRule] }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-muted-foreground", children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => {
410
+ setEditingProfile(profile);
411
+ setProfileSheetOpen(true);
412
+ }, children: [_jsx(Pencil, { className: "h-4 w-4" }), taxMessages.edit] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => {
413
+ if (confirm(taxMessages.deletePolicyProfileConfirm)) {
414
+ deleteProfileMutation.mutate(profile);
415
+ }
416
+ }, children: [_jsx(Trash2, { className: "h-4 w-4" }), taxMessages.delete] })] })] })] })] }), profileRules.length ? (_jsxs("div", { className: "overflow-hidden rounded-md border", children: [_jsxs("div", { className: "grid grid-cols-[5rem_5rem_minmax(0,1.3fr)_8rem_minmax(0,1.2fr)_minmax(0,1fr)_auto] gap-3 border-b bg-muted/40 px-3 py-2 text-xs font-medium text-muted-foreground", children: [_jsx("span", { children: taxMessages.policyPriorityLabel }), _jsx("span", { children: taxMessages.policySideLabel }), _jsx("span", { children: taxMessages.policyRuleNameLabel }), _jsx("span", { children: taxMessages.appliesToLabel }), _jsx("span", { children: taxMessages.policyConditionLabel }), _jsx("span", { children: taxMessages.taxRegimeLabel }), _jsx("span", { children: taxMessages.policyActionsLabel })] }), profileRules.map((rule) => {
417
+ const regime = regimesById.get(rule.taxRegimeId);
418
+ return (_jsxs("div", { className: "grid grid-cols-[5rem_5rem_minmax(0,1.3fr)_8rem_minmax(0,1.2fr)_minmax(0,1fr)_auto] items-center gap-3 border-b px-3 py-2 text-sm last:border-b-0", children: [_jsx("span", { className: "font-mono text-xs", children: rule.priority }), _jsx("span", { className: "text-xs", children: rule.side === "sell"
419
+ ? taxMessages.policySideSell
420
+ : taxMessages.policySideBuy }), _jsxs("div", { className: "min-w-0", children: [_jsx("div", { className: "truncate font-medium", children: rule.name }), !rule.active ? (_jsx(Badge, { variant: "secondary", className: "mt-1 text-xs", children: taxMessages.inactive })) : null] }), _jsx("span", { className: "text-xs", children: appliesToLabel(messages, rule.appliesTo) }), _jsx("code", { className: "truncate text-xs text-muted-foreground", children: summarizeCondition(messages, rule.condition) }), _jsx("span", { className: "truncate text-xs", children: regime
421
+ ? `${regime.name} (${formatRate(regime.ratePercent)})`
422
+ : rule.taxRegimeId }), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "h-8 w-8 text-muted-foreground", children: _jsx(MoreHorizontal, { className: "h-4 w-4" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => {
423
+ setEditingRule(rule);
424
+ setRuleProfileId(rule.profileId);
425
+ setRuleSheetOpen(true);
426
+ }, children: [_jsx(Pencil, { className: "h-4 w-4" }), taxMessages.edit] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onClick: () => {
427
+ if (confirm(taxMessages.deletePolicyRuleConfirm)) {
428
+ deleteRuleMutation.mutate(rule);
429
+ }
430
+ }, children: [_jsx(Trash2, { className: "h-4 w-4" }), taxMessages.delete] })] })] })] }, rule.id));
431
+ })] })) : (_jsx("p", { className: "rounded-md border border-dashed px-3 py-4 text-sm text-muted-foreground", children: taxMessages.policyRulesEmpty }))] }, profile.id));
432
+ }) })) })), _jsx(PolicyProfileSheet, { open: profileSheetOpen, onOpenChange: setProfileSheetOpen, profile: editingProfile, onSuccess: () => {
433
+ setProfileSheetOpen(false);
434
+ setEditingProfile(undefined);
435
+ void queryClient.invalidateQueries({ queryKey: ["tax-policy-profiles"] });
436
+ } }), _jsx(PolicyRuleSheet, { open: ruleSheetOpen, onOpenChange: setRuleSheetOpen, rule: editingRule, profileId: ruleProfileId, taxRegimes: taxRegimesQuery.data?.data ?? [], onSuccess: () => {
437
+ setRuleSheetOpen(false);
438
+ setEditingRule(undefined);
439
+ setRuleProfileId("");
440
+ void queryClient.invalidateQueries({ queryKey: ["tax-policy-rules"] });
441
+ } })] }) }));
442
+ }
443
+ function TaxesPageSkeleton({ rows }) {
444
+ return (_jsx("div", { className: "rounded-lg border bg-card text-card-foreground shadow-sm", children: _jsx("div", { className: "flex flex-col divide-y", children: Array.from({ length: rows }).map((_, index) => (_jsxs("div", { className: "flex items-center justify-between px-6 py-3", children: [_jsxs("div", { className: "min-w-0 flex-1 space-y-2", children: [_jsx(Skeleton, { className: "h-4 w-48" }), _jsx(Skeleton, { className: "h-3 w-72 max-w-full" })] }), _jsx(Skeleton, { className: "h-8 w-8 rounded-md" })] }, index))) }) }));
445
+ }
446
+ function TaxSheet({ open, onOpenChange, row, onSuccess, taxRegimes, }) {
447
+ const messages = useFinanceUiMessagesOrDefault();
448
+ const taxMessages = messages.taxesPage;
449
+ const api = useTaxesPageApi();
450
+ const [form, setForm] = useState(() => initialForm(row));
451
+ const [error, setError] = useState(null);
452
+ const isEditing = !!row;
453
+ useEffect(() => {
454
+ setForm(initialForm(row));
455
+ setError(null);
456
+ }, [row]);
457
+ const mutation = useMutation({
458
+ mutationFn: async () => {
459
+ if (!form.taxClassLabel.trim())
460
+ throw new Error(taxMessages.validationNameRequired);
461
+ const ratePercent = Number(form.ratePercent);
462
+ if (!Number.isFinite(ratePercent) || ratePercent < 0) {
463
+ throw new Error(taxMessages.validationRateInvalid);
464
+ }
465
+ const regimeInput = {
466
+ code: form.regimeCode,
467
+ name: form.regimeName.trim() || form.taxClassLabel.trim(),
468
+ jurisdiction: form.jurisdiction.trim() || null,
469
+ ratePercent: Math.round(ratePercent),
470
+ description: form.regimeDescription.trim() || null,
471
+ legalReference: form.legalReference.trim() || null,
472
+ active: form.active,
473
+ };
474
+ const taxClassInput = {
475
+ code: form.taxClassCode.trim() || toSlug(form.taxClassLabel),
476
+ label: form.taxClassLabel.trim(),
477
+ description: form.taxClassDescription.trim() || null,
478
+ lines: form.lines.length
479
+ ? form.lines
480
+ .filter((line) => line.regimeId.trim())
481
+ .map((line) => ({
482
+ regime_id: line.regimeId,
483
+ applies_to: line.appliesTo,
484
+ }))
485
+ : null,
486
+ active: form.active,
487
+ };
488
+ const regimeEnvelope = row?.regime
489
+ ? await api.patch(`/v1/finance/tax-regimes/${row.regime.id}`, regimeInput)
490
+ : await api.post("/v1/finance/tax-regimes", regimeInput);
491
+ const regime = regimeEnvelope.data;
492
+ if (row) {
493
+ await api.patch(`/v1/finance/tax-classes/${row.taxClass.id}`, {
494
+ ...taxClassInput,
495
+ defaultRegimeId: regime.id,
496
+ });
497
+ }
498
+ else {
499
+ await api.post("/v1/finance/tax-classes", {
500
+ ...taxClassInput,
501
+ defaultRegimeId: regime.id,
502
+ });
503
+ }
504
+ },
505
+ onSuccess,
506
+ onError: (err) => setError(err instanceof Error ? err.message : taxMessages.saveFailed),
507
+ });
508
+ const setField = (key) => (value) => setForm((current) => ({ ...current, [key]: value }));
509
+ const updateLine = (index, patch) => {
510
+ setForm((current) => ({
511
+ ...current,
512
+ lines: current.lines.map((line, lineIndex) => lineIndex === index ? { ...line, ...patch } : line),
513
+ }));
514
+ };
515
+ const addLine = () => {
516
+ const regimeId = taxRegimes[0]?.id;
517
+ if (!regimeId)
518
+ return;
519
+ setForm((current) => ({
520
+ ...current,
521
+ lines: [...current.lines, { key: nextTaxClassLineKey(), appliesTo: "all", regimeId }],
522
+ }));
523
+ };
524
+ const removeLine = (index) => {
525
+ setForm((current) => ({
526
+ ...current,
527
+ lines: current.lines.filter((_, lineIndex) => lineIndex !== index),
528
+ }));
529
+ };
530
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? taxMessages.editSheetTitle : taxMessages.newSheetTitle }) }), _jsx(SheetBody, { children: _jsxs("form", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "flex flex-col gap-3 rounded-lg border p-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-medium", children: taxMessages.taxClassSectionTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: taxMessages.taxClassSectionDescription })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.taxClassLabelLabel }), _jsx(Input, { value: form.taxClassLabel, onChange: (event) => {
531
+ const next = event.target.value;
532
+ setForm((current) => ({
533
+ ...current,
534
+ taxClassLabel: next,
535
+ taxClassCode: current.taxClassCode || toSlug(next),
536
+ regimeName: current.regimeName || next,
537
+ }));
538
+ }, placeholder: taxMessages.taxClassLabelPlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.taxClassCodeLabel }), _jsx(Input, { value: form.taxClassCode, onChange: (event) => setField("taxClassCode")(event.target.value), placeholder: taxMessages.taxClassCodePlaceholder })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.taxClassDescriptionLabel }), _jsx(Textarea, { value: form.taxClassDescription, onChange: (event) => setField("taxClassDescription")(event.target.value), placeholder: taxMessages.taxClassDescriptionPlaceholder })] })] }), _jsxs("div", { className: "flex flex-col gap-3 rounded-lg border p-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-medium", children: taxMessages.defaultRegimeSectionTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: taxMessages.defaultRegimeSectionDescription })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.regimeNameLabel }), _jsx(Input, { value: form.regimeName, onChange: (event) => setField("regimeName")(event.target.value), placeholder: taxMessages.regimeNamePlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.regimeCodeLabel }), _jsxs(Select, { value: form.regimeCode, onValueChange: (value) => setField("regimeCode")(value), items: TAX_CODE_OPTIONS.map((code) => ({ value: code, label: code })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: TAX_CODE_OPTIONS.map((code) => (_jsx(SelectItem, { value: code, children: code }, code))) })] })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.rateLabel }), _jsx(Input, { type: "number", min: "0", step: "1", value: form.ratePercent, onChange: (event) => setField("ratePercent")(event.target.value) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.jurisdictionLabel }), _jsx(Input, { value: form.jurisdiction, onChange: (event) => setField("jurisdiction")(event.target.value), placeholder: "RO" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.legalReferenceLabel }), _jsx(Input, { value: form.legalReference, onChange: (event) => setField("legalReference")(event.target.value), placeholder: taxMessages.legalReferencePlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.regimeDescriptionLabel }), _jsx(Textarea, { value: form.regimeDescription, onChange: (event) => setField("regimeDescription")(event.target.value), placeholder: taxMessages.regimeDescriptionPlaceholder })] })] }), _jsxs("div", { className: "flex flex-col gap-3 rounded-lg border p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-medium", children: taxMessages.regimeOverridesSectionTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: taxMessages.regimeOverridesSectionDescription })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addLine, disabled: !taxRegimes.length, children: [_jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }), taxMessages.addRegimeOverride] })] }), form.lines.length ? (_jsx("div", { className: "flex flex-col gap-2", children: form.lines.map((line, index) => (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)_auto] items-end gap-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.appliesToLabel }), _jsxs(Select, { value: line.appliesTo, onValueChange: (value) => updateLine(index, { appliesTo: value }), items: TAX_CLASS_APPLIES_TO_OPTIONS.map((appliesTo) => ({
539
+ value: appliesTo,
540
+ label: appliesToLabel(messages, appliesTo),
541
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: TAX_CLASS_APPLIES_TO_OPTIONS.map((appliesTo) => (_jsx(SelectItem, { value: appliesTo, children: appliesToLabel(messages, appliesTo) }, appliesTo))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.taxRegimeLabel }), _jsxs(Select, { value: line.regimeId, onValueChange: (value) => updateLine(index, { regimeId: value ?? "" }), items: taxRegimes.map((regime) => ({
542
+ value: regime.id,
543
+ label: `${regime.name} (${formatRate(regime.ratePercent)})`,
544
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: taxRegimes.map((regime) => (_jsxs(SelectItem, { value: regime.id, children: [regime.name, " (", formatRate(regime.ratePercent), ")"] }, regime.id))) })] })] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeLine(index), "aria-label": taxMessages.removeRegimeOverride, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, line.key))) })) : (_jsx("p", { className: "text-xs text-muted-foreground", children: taxMessages.noRegimeOverrides }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.active, onCheckedChange: setField("active") }), _jsx(Label, { children: taxMessages.activeLabel })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null] }) }), _jsxs(SheetFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: taxMessages.cancel }), _jsxs(Button, { onClick: () => mutation.mutate(), disabled: mutation.isPending, children: [mutation.isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? taxMessages.saveChanges : taxMessages.createTax] })] })] }) }));
545
+ }
546
+ function PolicyProfileSheet({ open, onOpenChange, profile, onSuccess, }) {
547
+ const messages = useFinanceUiMessagesOrDefault();
548
+ const taxMessages = messages.taxesPage;
549
+ const api = useTaxesPageApi();
550
+ const [form, setForm] = useState(() => initialPolicyProfileForm(profile));
551
+ const [error, setError] = useState(null);
552
+ const isEditing = !!profile;
553
+ useEffect(() => {
554
+ setForm(initialPolicyProfileForm(profile));
555
+ setError(null);
556
+ }, [profile]);
557
+ const mutation = useMutation({
558
+ mutationFn: async () => {
559
+ if (!form.name.trim())
560
+ throw new Error(taxMessages.validationPolicyProfileNameRequired);
561
+ const input = {
562
+ name: form.name.trim(),
563
+ code: form.code.trim() || toSlug(form.name),
564
+ jurisdiction: form.jurisdiction.trim() || null,
565
+ description: form.description.trim() || null,
566
+ active: form.active,
567
+ };
568
+ if (profile) {
569
+ await api.patch(`/v1/finance/tax-policy-profiles/${profile.id}`, input);
570
+ }
571
+ else {
572
+ await api.post("/v1/finance/tax-policy-profiles", input);
573
+ }
574
+ },
575
+ onSuccess,
576
+ onError: (err) => setError(err instanceof Error ? err.message : taxMessages.savePolicyProfileFailed),
577
+ });
578
+ const setField = (key) => (value) => setForm((current) => ({ ...current, [key]: value }));
579
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing
580
+ ? taxMessages.editPolicyProfileSheetTitle
581
+ : taxMessages.newPolicyProfileSheetTitle }) }), _jsx(SheetBody, { children: _jsxs("form", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyProfileNameLabel }), _jsx(Input, { value: form.name, onChange: (event) => {
582
+ const next = event.target.value;
583
+ setForm((current) => ({
584
+ ...current,
585
+ name: next,
586
+ code: current.code || toSlug(next),
587
+ }));
588
+ }, placeholder: taxMessages.policyProfileNamePlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyProfileCodeLabel }), _jsx(Input, { value: form.code, onChange: (event) => setField("code")(event.target.value), placeholder: taxMessages.policyProfileCodePlaceholder })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.jurisdictionLabel }), _jsx(Input, { value: form.jurisdiction, onChange: (event) => setField("jurisdiction")(event.target.value.toUpperCase()), placeholder: "RO" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyProfileDescriptionLabel }), _jsx(Textarea, { value: form.description, onChange: (event) => setField("description")(event.target.value), placeholder: taxMessages.policyProfileDescriptionPlaceholder })] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.active, onCheckedChange: setField("active") }), _jsx(Label, { children: taxMessages.activeLabel })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null] }) }), _jsxs(SheetFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: taxMessages.cancel }), _jsxs(Button, { onClick: () => mutation.mutate(), disabled: mutation.isPending, children: [mutation.isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? taxMessages.saveChanges : taxMessages.createPolicyProfile] })] })] }) }));
589
+ }
590
+ function PolicyRuleSheet({ open, onOpenChange, rule, profileId, taxRegimes, onSuccess, }) {
591
+ const messages = useFinanceUiMessagesOrDefault();
592
+ const taxMessages = messages.taxesPage;
593
+ const api = useTaxesPageApi();
594
+ const defaultTaxRegimeId = taxRegimes[0]?.id ?? "";
595
+ const [form, setForm] = useState(() => initialPolicyRuleForm(rule, profileId, defaultTaxRegimeId));
596
+ const [error, setError] = useState(null);
597
+ const isEditing = !!rule;
598
+ useEffect(() => {
599
+ setForm(initialPolicyRuleForm(rule, profileId, defaultTaxRegimeId));
600
+ setError(null);
601
+ }, [defaultTaxRegimeId, profileId, rule]);
602
+ const mutation = useMutation({
603
+ mutationFn: async () => {
604
+ if (!form.profileId)
605
+ throw new Error(taxMessages.validationPolicyProfileRequired);
606
+ if (!form.name.trim())
607
+ throw new Error(taxMessages.validationPolicyRuleNameRequired);
608
+ if (!form.taxRegimeId)
609
+ throw new Error(taxMessages.validationPolicyRuleRegimeRequired);
610
+ const priority = Number(form.priority);
611
+ if (!Number.isInteger(priority) || priority < 0) {
612
+ throw new Error(taxMessages.validationPolicyRulePriorityInvalid);
613
+ }
614
+ const condition = buildPolicyCondition(form, taxMessages);
615
+ const input = {
616
+ profileId: form.profileId,
617
+ side: form.side,
618
+ priority,
619
+ name: form.name.trim(),
620
+ appliesTo: form.appliesTo,
621
+ condition,
622
+ taxRegimeId: form.taxRegimeId,
623
+ active: form.active,
624
+ };
625
+ if (rule) {
626
+ await api.patch(`/v1/finance/tax-policy-rules/${rule.id}`, input);
627
+ }
628
+ else {
629
+ await api.post("/v1/finance/tax-policy-rules", input);
630
+ }
631
+ },
632
+ onSuccess,
633
+ onError: (err) => setError(err instanceof Error ? err.message : taxMessages.savePolicyRuleFailed),
634
+ });
635
+ const setField = (key) => (value) => setForm((current) => ({ ...current, [key]: value }));
636
+ const updateCondition = (index, patch) => {
637
+ setForm((current) => ({
638
+ ...current,
639
+ conditions: current.conditions.map((condition, conditionIndex) => conditionIndex === index ? normalizeCondition({ ...condition, ...patch }) : condition),
640
+ }));
641
+ };
642
+ const addCondition = () => {
643
+ setForm((current) => ({
644
+ ...current,
645
+ conditionMode: current.conditionMode === "always" ? "all" : current.conditionMode,
646
+ conditions: [
647
+ ...current.conditions,
648
+ {
649
+ key: nextTaxPolicyConditionKey(),
650
+ fact: "hasAccommodation",
651
+ operator: "eq",
652
+ value: "true",
653
+ },
654
+ ],
655
+ }));
656
+ };
657
+ const removeCondition = (index) => {
658
+ setForm((current) => {
659
+ const conditions = current.conditions.filter((_, conditionIndex) => conditionIndex !== index);
660
+ return {
661
+ ...current,
662
+ conditions,
663
+ conditionMode: conditions.length ? current.conditionMode : "always",
664
+ };
665
+ });
666
+ };
667
+ return (_jsx(Sheet, { open: open, onOpenChange: onOpenChange, children: _jsxs(SheetContent, { side: "right", size: "lg", children: [_jsx(SheetHeader, { children: _jsx(SheetTitle, { children: isEditing ? taxMessages.editPolicyRuleSheetTitle : taxMessages.newPolicyRuleSheetTitle }) }), _jsx(SheetBody, { children: _jsxs("form", { className: "flex flex-col gap-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyRuleNameLabel }), _jsx(Input, { value: form.name, onChange: (event) => setField("name")(event.target.value), placeholder: taxMessages.policyRuleNamePlaceholder })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyPriorityLabel }), _jsx(Input, { type: "number", min: "0", step: "1", value: form.priority, onChange: (event) => setField("priority")(event.target.value) })] })] }), _jsxs("div", { className: "grid grid-cols-3 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policySideLabel }), _jsxs(Select, { value: form.side, onValueChange: (value) => setField("side")(value), items: [
668
+ { value: "sell", label: taxMessages.policySideSell },
669
+ { value: "buy", label: taxMessages.policySideBuy },
670
+ ], children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "sell", children: taxMessages.policySideSell }), _jsx(SelectItem, { value: "buy", children: taxMessages.policySideBuy })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.appliesToLabel }), _jsxs(Select, { value: form.appliesTo, onValueChange: (value) => setField("appliesTo")(value), items: TAX_CLASS_APPLIES_TO_OPTIONS.map((appliesTo) => ({
671
+ value: appliesTo,
672
+ label: appliesToLabel(messages, appliesTo),
673
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: TAX_CLASS_APPLIES_TO_OPTIONS.map((appliesTo) => (_jsx(SelectItem, { value: appliesTo, children: appliesToLabel(messages, appliesTo) }, appliesTo))) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.taxRegimeLabel }), _jsxs(Select, { value: form.taxRegimeId, onValueChange: (value) => setField("taxRegimeId")(value ?? ""), items: taxRegimes.map((regime) => ({
674
+ value: regime.id,
675
+ label: `${regime.name} (${formatRate(regime.ratePercent)})`,
676
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: taxRegimes.map((regime) => (_jsxs(SelectItem, { value: regime.id, children: [regime.name, " (", formatRate(regime.ratePercent), ")"] }, regime.id))) })] })] })] }), _jsxs("div", { className: "flex flex-col gap-3 rounded-lg border p-4", children: [_jsxs("div", { className: "flex items-start justify-between gap-4", children: [_jsxs("div", { children: [_jsx("h3", { className: "text-sm font-medium", children: taxMessages.policyConditionSectionTitle }), _jsx("p", { className: "text-xs text-muted-foreground", children: taxMessages.policyConditionSectionDescription })] }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: addCondition, disabled: form.conditionMode === "always", children: [_jsx(Plus, { className: "mr-1.5 h-3.5 w-3.5" }), taxMessages.addPolicyCondition] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyConditionModeLabel }), _jsxs(Select, { value: form.conditionMode, onValueChange: (value) => {
677
+ const conditionMode = value;
678
+ setForm((current) => ({
679
+ ...current,
680
+ conditionMode,
681
+ conditions: conditionMode === "always"
682
+ ? []
683
+ : current.conditions.length
684
+ ? current.conditions
685
+ : [
686
+ {
687
+ key: nextTaxPolicyConditionKey(),
688
+ fact: "hasAccommodation",
689
+ operator: "eq",
690
+ value: "true",
691
+ },
692
+ ],
693
+ }));
694
+ }, items: [
695
+ { value: "always", label: taxMessages.policyConditionAlways },
696
+ { value: "all", label: taxMessages.policyConditionModeAll },
697
+ { value: "any", label: taxMessages.policyConditionModeAny },
698
+ ], children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "always", children: taxMessages.policyConditionAlways }), _jsx(SelectItem, { value: "all", children: taxMessages.policyConditionModeAll }), _jsx(SelectItem, { value: "any", children: taxMessages.policyConditionModeAny })] })] })] }), form.conditionMode === "always" ? (_jsx("p", { className: "rounded-md border border-dashed px-3 py-4 text-sm text-muted-foreground", children: taxMessages.policyConditionAlwaysDescription })) : (_jsx("div", { className: "flex flex-col gap-3", children: form.conditions.map((condition, index) => (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)_minmax(0,1fr)_minmax(0,1fr)_auto] items-end gap-2", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyFactLabel }), _jsxs(Select, { value: condition.fact, onValueChange: (value) => updateCondition(index, {
699
+ fact: value,
700
+ }), items: TAX_POLICY_CONDITION_FACT_OPTIONS.map((fact) => ({
701
+ value: fact,
702
+ label: fact === "hasAccommodation"
703
+ ? taxMessages.policyFactHasAccommodation
704
+ : taxMessages.policyFactAccommodationCountries,
705
+ })), children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "hasAccommodation", children: taxMessages.policyFactHasAccommodation }), _jsx(SelectItem, { value: "accommodationCountries", children: taxMessages.policyFactAccommodationCountries })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyOperatorLabel }), _jsxs(Select, { value: condition.operator, onValueChange: (value) => updateCondition(index, {
706
+ operator: value,
707
+ }), items: [
708
+ {
709
+ value: condition.fact === "hasAccommodation" ? "eq" : "contains",
710
+ label: condition.fact === "hasAccommodation"
711
+ ? taxMessages.policyOperatorEquals
712
+ : taxMessages.policyOperatorContains,
713
+ },
714
+ ], children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: condition.fact === "hasAccommodation" ? (_jsx(SelectItem, { value: "eq", children: taxMessages.policyOperatorEquals })) : (_jsx(SelectItem, { value: "contains", children: taxMessages.policyOperatorContains })) })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: taxMessages.policyValueLabel }), condition.fact === "hasAccommodation" ? (_jsxs(Select, { value: condition.value, onValueChange: (value) => updateCondition(index, { value: value ?? "true" }), items: [
715
+ { value: "true", label: taxMessages.policyValueYes },
716
+ { value: "false", label: taxMessages.policyValueNo },
717
+ ], children: [_jsx(SelectTrigger, { className: "w-full", children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "true", children: taxMessages.policyValueYes }), _jsx(SelectItem, { value: "false", children: taxMessages.policyValueNo })] })] })) : (_jsx(Input, { value: condition.value, maxLength: 2, onChange: (event) => updateCondition(index, { value: event.target.value.toUpperCase() }), placeholder: "RO" }))] }), _jsx(Button, { type: "button", variant: "ghost", size: "icon", onClick: () => removeCondition(index), "aria-label": taxMessages.removePolicyCondition, children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, condition.key))) }))] }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Switch, { checked: form.active, onCheckedChange: setField("active") }), _jsx(Label, { children: taxMessages.activeLabel })] }), error ? _jsx("p", { className: "text-sm text-destructive", children: error }) : null] }) }), _jsxs(SheetFooter, { children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => onOpenChange(false), children: taxMessages.cancel }), _jsxs(Button, { onClick: () => mutation.mutate(), disabled: mutation.isPending, children: [mutation.isPending ? _jsx(Loader2, { className: "mr-2 h-4 w-4 animate-spin" }) : null, isEditing ? taxMessages.saveChanges : taxMessages.createPolicyRule] })] })] }) }));
718
+ }