@voyantjs/finance-ui 0.30.7 → 0.31.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/components/invoice-detail-page.d.ts +113 -0
- package/dist/components/invoice-detail-page.d.ts.map +1 -0
- package/dist/components/invoice-detail-page.js +142 -0
- package/dist/components/invoices-page-skeleton.d.ts +5 -0
- package/dist/components/invoices-page-skeleton.d.ts.map +1 -0
- package/dist/components/invoices-page-skeleton.js +13 -0
- package/dist/components/invoices-page.d.ts +6 -0
- package/dist/components/invoices-page.d.ts.map +1 -0
- package/dist/components/invoices-page.js +109 -0
- package/dist/components/payment-detail-page.d.ts +41 -0
- package/dist/components/payment-detail-page.d.ts.map +1 -0
- package/dist/components/payment-detail-page.js +79 -0
- package/dist/components/payments-page-skeleton.d.ts +5 -0
- package/dist/components/payments-page-skeleton.d.ts.map +1 -0
- package/dist/components/payments-page-skeleton.js +13 -0
- package/dist/components/payments-page.d.ts +20 -0
- package/dist/components/payments-page.d.ts.map +1 -0
- package/dist/components/payments-page.js +151 -0
- package/dist/components/taxes-page.d.ts +11 -0
- package/dist/components/taxes-page.d.ts.map +1 -0
- package/dist/components/taxes-page.js +718 -0
- package/dist/i18n/en.d.ts +308 -0
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +308 -0
- package/dist/i18n/index.d.ts +1 -1
- package/dist/i18n/index.d.ts.map +1 -1
- package/dist/i18n/messages.d.ts +193 -1
- package/dist/i18n/messages.d.ts.map +1 -1
- package/dist/i18n/messages.js +11 -0
- package/dist/i18n/provider.d.ts +616 -0
- package/dist/i18n/provider.d.ts.map +1 -1
- package/dist/i18n/ro.d.ts +308 -0
- package/dist/i18n/ro.d.ts.map +1 -1
- package/dist/i18n/ro.js +308 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- 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
|
+
}
|