@spaceinvoices/react-ui 0.3.0 → 0.4.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 (50) hide show
  1. package/cli/dist/index.js +1 -1
  2. package/package.json +1 -1
  3. package/registry.json +24 -225
  4. package/src/components/advance-invoices/advance-invoices.hooks.ts +2 -2
  5. package/src/components/documents/documents.hooks.ts +5 -48
  6. package/src/components/documents/shared/document-preview-display.tsx +12 -1
  7. package/src/components/documents/view/document-actions-bar.tsx +20 -12
  8. package/src/components/documents/view/document-activities-list.tsx +166 -0
  9. package/src/components/documents/view/document-details-card.tsx +6 -6
  10. package/src/components/documents/view/index.ts +1 -0
  11. package/src/components/documents/view/locales/de.ts +32 -0
  12. package/src/components/documents/view/locales/es.ts +32 -0
  13. package/src/components/documents/view/locales/fr.ts +32 -0
  14. package/src/components/documents/view/locales/hr.ts +32 -0
  15. package/src/components/documents/view/locales/it.ts +32 -0
  16. package/src/components/documents/view/locales/nl.ts +32 -0
  17. package/src/components/documents/view/locales/pl.ts +32 -0
  18. package/src/components/documents/view/locales/pt.ts +32 -0
  19. package/src/components/documents/view/locales/sl.ts +32 -0
  20. package/src/components/entities/fina-settings-form/locales/de.ts +3 -0
  21. package/src/components/entities/fina-settings-form/locales/en.ts +3 -0
  22. package/src/components/entities/fina-settings-form/locales/es.ts +3 -0
  23. package/src/components/entities/fina-settings-form/locales/fr.ts +3 -0
  24. package/src/components/entities/fina-settings-form/locales/hr.ts +3 -0
  25. package/src/components/entities/fina-settings-form/locales/it.ts +3 -0
  26. package/src/components/entities/fina-settings-form/locales/nl.ts +3 -0
  27. package/src/components/entities/fina-settings-form/locales/pl.ts +3 -0
  28. package/src/components/entities/fina-settings-form/locales/pt.ts +3 -0
  29. package/src/components/entities/fina-settings-form/locales/sl.ts +3 -0
  30. package/src/components/entities/furs-settings-form/furs-settings-form.tsx +15 -7
  31. package/src/components/entities/furs-settings-form/furs-settings.hooks.ts +1 -1
  32. package/src/components/entities/furs-settings-form/locales/de.ts +2 -0
  33. package/src/components/entities/furs-settings-form/locales/en.ts +12 -0
  34. package/src/components/entities/furs-settings-form/locales/es.ts +2 -0
  35. package/src/components/entities/furs-settings-form/locales/fr.ts +2 -0
  36. package/src/components/entities/furs-settings-form/locales/hr.ts +2 -0
  37. package/src/components/entities/furs-settings-form/locales/it.ts +2 -0
  38. package/src/components/entities/furs-settings-form/locales/nl.ts +2 -0
  39. package/src/components/entities/furs-settings-form/locales/pl.ts +2 -0
  40. package/src/components/entities/furs-settings-form/locales/pt.ts +2 -0
  41. package/src/components/entities/furs-settings-form/locales/sl.ts +14 -0
  42. package/src/components/entities/furs-settings-form/sections/general-settings-section.tsx +121 -1
  43. package/src/components/invoices/index.ts +1 -1
  44. package/src/components/invoices/send-email-dialog/send-email-dialog.tsx +2 -2
  45. package/src/components/invoices/view/fiscalization-status-card.tsx +121 -0
  46. package/src/generated/schemas/creditnote.ts +3 -0
  47. package/src/hooks/use-duplicate-document.ts +19 -11
  48. package/src/providers/entities-provider.tsx +21 -0
  49. package/src/components/invoices/view/fina-info-display.tsx +0 -196
  50. package/src/components/invoices/view/furs-info-display.tsx +0 -213
@@ -148,6 +148,8 @@ export default {
148
148
  "Invoice Number": "Número da fatura",
149
149
  Iteration: "Iteração",
150
150
  "QR Code": "Código QR",
151
+ "Issuer Tax Number": "Número fiscal do emissor",
152
+ Operator: "Operador",
151
153
  "Fiscalized at": "Fiscalizado em",
152
154
  "Enable Fiscalization": "Ativar fiscalização",
153
155
  "Turn on FURS fiscalization for invoices": "Ative a fiscalização FURS para faturas",
@@ -148,6 +148,8 @@ export default {
148
148
  "Invoice Number": "Številka računa",
149
149
  Iteration: "Iteracija",
150
150
  "QR Code": "QR koda",
151
+ "Issuer Tax Number": "Davčna številka izdajatelja",
152
+ Operator: "Operater",
151
153
  "Fiscalized at": "Fiskalizirano dne",
152
154
  // New flow translations
153
155
  "Enable Fiscalization": "Omogoči fiskalizacijo",
@@ -195,4 +197,16 @@ export default {
195
197
  "Privzeti podatki operaterja, ki se uporabljajo, ko so računi ustvarjeni preko API-ja brez prijavljenega uporabnika.",
196
198
  "Certificate uploaded successfully": "Certifikat uspešno naložen",
197
199
  "Loading certificate details...": "Nalaganje podrobnosti certifikata...",
200
+ // Entity info section
201
+ "Entity Information": "Podatki podjetja",
202
+ "Required company details for FURS fiscalization": "Obvezni podatki podjetja za fiskalizacijo FURS",
203
+ "Entity Tax Number": "Davčna številka podjetja",
204
+ "Your company's tax number (must match FURS certificate)":
205
+ "Davčna številka vašega podjetja (mora se ujemati s certifikatom FURS)",
206
+ Address: "Naslov",
207
+ "Post Code": "Poštna številka",
208
+ "Save Entity Info": "Shrani podatke podjetja",
209
+ "Tax number is required for FURS fiscalization": "Davčna številka je obvezna za fiskalizacijo FURS",
210
+ "Set entity tax number in General Settings first":
211
+ "Najprej nastavite davčno številko podjetja v splošnih nastavitvah",
198
212
  } as const;
@@ -1,7 +1,9 @@
1
1
  import type { Entity } from "@spaceinvoices/js-sdk";
2
- import { ChevronRight, Settings, User } from "lucide-react";
2
+ import { AlertTriangle, Building2, ChevronRight, Settings, User } from "lucide-react";
3
3
  import { type FC, type ReactNode, useEffect, useState } from "react";
4
4
  import type { UseFormReturn } from "react-hook-form";
5
+ import { useUpdateEntity } from "@/ui/components/entities/entities.hooks";
6
+ import { Alert, AlertDescription } from "@/ui/components/ui/alert";
5
7
  import { Button } from "@/ui/components/ui/button";
6
8
  import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/ui/components/ui/form";
7
9
  import { Input } from "@/ui/components/ui/input";
@@ -30,6 +32,37 @@ export const GeneralSettingsSection: FC<GeneralSettingsSectionProps> = ({
30
32
  const wrap = (section: SectionType, content: ReactNode) => (wrapSection ? wrapSection(section, content) : content);
31
33
  const [isAdvancedOpen, setIsAdvancedOpen] = useState(false);
32
34
 
35
+ // Entity info (local state for form)
36
+ const [entityTaxNumber, setEntityTaxNumber] = useState("");
37
+ const [entityAddress, setEntityAddress] = useState("");
38
+ const [entityCity, setEntityCity] = useState("");
39
+ const [entityPostCode, setEntityPostCode] = useState("");
40
+
41
+ // Initialize entity fields from entity prop
42
+ useEffect(() => {
43
+ setEntityTaxNumber(entity.tax_number || "");
44
+ setEntityAddress(entity.address || "");
45
+ setEntityCity(entity.city || "");
46
+ setEntityPostCode(entity.post_code || "");
47
+ }, [entity.tax_number, entity.address, entity.city, entity.post_code]);
48
+
49
+ const { mutate: updateEntity, isPending: isEntityUpdatePending } = useUpdateEntity({
50
+ onSuccess: () => onSuccess?.(),
51
+ onError: (error) => onError?.(error),
52
+ });
53
+
54
+ const handleSaveEntityInfo = () => {
55
+ updateEntity({
56
+ id: entity.id,
57
+ data: {
58
+ tax_number: entityTaxNumber || null,
59
+ address: entityAddress || null,
60
+ city: entityCity || null,
61
+ post_code: entityPostCode || null,
62
+ },
63
+ });
64
+ };
65
+
33
66
  // User operator settings (local state for form)
34
67
  const { data: userFursSettings, isLoading: userSettingsLoading } = useUserFursSettings(entity.id);
35
68
  const [operatorTaxNumber, setOperatorTaxNumber] = useState("");
@@ -58,6 +91,92 @@ export const GeneralSettingsSection: FC<GeneralSettingsSectionProps> = ({
58
91
  });
59
92
  };
60
93
 
94
+ // Entity Information content
95
+ const entityInfoContent = (
96
+ <div className="space-y-4">
97
+ <div className="flex items-center gap-3">
98
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-500/10">
99
+ <Building2 className="h-5 w-5 text-orange-600 dark:text-orange-400" />
100
+ </div>
101
+ <div>
102
+ <h3 className="font-semibold text-lg">{t("Entity Information")}</h3>
103
+ <p className="text-muted-foreground text-sm">{t("Required company details for FURS fiscalization")}</p>
104
+ </div>
105
+ </div>
106
+
107
+ {!entity.tax_number && (
108
+ <Alert variant="destructive">
109
+ <AlertTriangle className="h-4 w-4" />
110
+ <AlertDescription>{t("Tax number is required for FURS fiscalization")}</AlertDescription>
111
+ </Alert>
112
+ )}
113
+
114
+ <div className="space-y-4">
115
+ <div>
116
+ <label htmlFor="entity-tax-number" className="font-medium text-sm">
117
+ {t("Entity Tax Number")}
118
+ </label>
119
+ <Input
120
+ id="entity-tax-number"
121
+ placeholder="12345678"
122
+ value={entityTaxNumber}
123
+ onChange={(e) => setEntityTaxNumber(e.target.value)}
124
+ className={cn("mt-2 h-10", !entity.tax_number && "border-destructive")}
125
+ />
126
+ <p className="mt-1 text-muted-foreground text-xs">
127
+ {t("Your company's tax number (must match FURS certificate)")}
128
+ </p>
129
+ </div>
130
+
131
+ <div>
132
+ <label htmlFor="entity-address" className="font-medium text-sm">
133
+ {t("Address")}
134
+ </label>
135
+ <Input
136
+ id="entity-address"
137
+ value={entityAddress}
138
+ onChange={(e) => setEntityAddress(e.target.value)}
139
+ className="mt-2 h-10"
140
+ />
141
+ </div>
142
+
143
+ <div className="grid grid-cols-2 gap-4">
144
+ <div>
145
+ <label htmlFor="entity-post-code" className="font-medium text-sm">
146
+ {t("Post Code")}
147
+ </label>
148
+ <Input
149
+ id="entity-post-code"
150
+ value={entityPostCode}
151
+ onChange={(e) => setEntityPostCode(e.target.value)}
152
+ className="mt-2 h-10"
153
+ />
154
+ </div>
155
+ <div>
156
+ <label htmlFor="entity-city" className="font-medium text-sm">
157
+ {t("City")}
158
+ </label>
159
+ <Input
160
+ id="entity-city"
161
+ value={entityCity}
162
+ onChange={(e) => setEntityCity(e.target.value)}
163
+ className="mt-2 h-10"
164
+ />
165
+ </div>
166
+ </div>
167
+
168
+ <Button
169
+ type="button"
170
+ onClick={handleSaveEntityInfo}
171
+ disabled={isEntityUpdatePending}
172
+ className="cursor-pointer"
173
+ >
174
+ {isEntityUpdatePending ? t("Saving...") : t("Save Entity Info")}
175
+ </Button>
176
+ </div>
177
+ </div>
178
+ );
179
+
61
180
  // Operator Settings content
62
181
  const operatorContent = (
63
182
  <div className="space-y-4">
@@ -244,6 +363,7 @@ export const GeneralSettingsSection: FC<GeneralSettingsSectionProps> = ({
244
363
 
245
364
  return (
246
365
  <div className="space-y-6">
366
+ {wrap("entity-info", entityInfoContent)}
247
367
  {wrap("operator", operatorContent)}
248
368
  {wrap("fiscalization", fiscalizationContent)}
249
369
  {wrap("advanced", advancedContent)}
@@ -5,5 +5,5 @@ export * from "./list";
5
5
  export { SendEmailDialog } from "./send-email-dialog";
6
6
  export * from "./shared";
7
7
  export { EslogInfoDisplay } from "./view/eslog-info-display";
8
- export { FursInfoDisplay } from "./view/furs-info-display";
8
+ export { FiscalizationStatusCard } from "./view/fiscalization-status-card";
9
9
  // Note: InvoicePreviewDisplay is now DocumentPreviewDisplay in documents/shared
@@ -195,7 +195,7 @@ export function SendEmailDialog({
195
195
  </Button>
196
196
  </DialogTrigger>
197
197
  )}
198
- <DialogContent className="sm:max-w-md">
198
+ <DialogContent className="sm:max-w-lg">
199
199
  <DialogHeader>
200
200
  <DialogTitle>{t("Send Invoice by Email")}</DialogTitle>
201
201
  <DialogDescription>
@@ -266,7 +266,7 @@ export function SendEmailDialog({
266
266
  document={invoice}
267
267
  disabled={isLoading}
268
268
  multiline
269
- rows={4}
269
+ rows={8}
270
270
  />
271
271
  </FormControl>
272
272
  <FormDescription>{t("Leave empty to use default")}</FormDescription>
@@ -0,0 +1,121 @@
1
+ import type { FinaFiscalizationResponse, FursFiscalizationResponse } from "@spaceinvoices/js-sdk";
2
+ import { AlertCircle, CheckCircle2, Clock, Loader2, MinusCircle, RefreshCw, XCircle } from "lucide-react";
3
+ import { Alert, AlertDescription } from "@/ui/components/ui/alert";
4
+ import { Badge } from "@/ui/components/ui/badge";
5
+ import { Button } from "@/ui/components/ui/button";
6
+ import { Card, CardContent, CardHeader, CardTitle } from "@/ui/components/ui/card";
7
+ import type { ComponentTranslationProps } from "@/ui/lib/translation";
8
+ import { createTranslation } from "@/ui/lib/translation";
9
+ import de from "../../documents/view/locales/de";
10
+ import es from "../../documents/view/locales/es";
11
+ import fr from "../../documents/view/locales/fr";
12
+ import hr from "../../documents/view/locales/hr";
13
+ import it from "../../documents/view/locales/it";
14
+ import nl from "../../documents/view/locales/nl";
15
+ import pl from "../../documents/view/locales/pl";
16
+ import pt from "../../documents/view/locales/pt";
17
+ import sl from "../../documents/view/locales/sl";
18
+
19
+ const translations = { de, es, fr, hr, it, nl, pl, pt, sl } as const;
20
+
21
+ type FiscalizationData = FursFiscalizationResponse | FinaFiscalizationResponse;
22
+
23
+ interface FiscalizationStatusCardProps extends ComponentTranslationProps {
24
+ fiscalizationType: "furs" | "fina";
25
+ fiscalizationData: FiscalizationData | null | undefined;
26
+ onRetry?: () => void;
27
+ isRetrying?: boolean;
28
+ }
29
+
30
+ export function FiscalizationStatusCard({
31
+ fiscalizationType,
32
+ fiscalizationData,
33
+ onRetry,
34
+ isRetrying,
35
+ t: translateFn,
36
+ namespace,
37
+ locale,
38
+ }: FiscalizationStatusCardProps) {
39
+ const t = createTranslation({
40
+ t: translateFn,
41
+ namespace,
42
+ locale,
43
+ translations,
44
+ });
45
+
46
+ if (!fiscalizationData) {
47
+ return null;
48
+ }
49
+
50
+ const label = fiscalizationType === "furs" ? "FURS" : "FINA";
51
+
52
+ const getStatusBadge = () => {
53
+ switch (fiscalizationData.status) {
54
+ case "success":
55
+ return (
56
+ <Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100">
57
+ <CheckCircle2 className="mr-1 h-3 w-3" />
58
+ {t("Fiscalized")}
59
+ </Badge>
60
+ );
61
+ case "pending":
62
+ return (
63
+ <Badge className="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100">
64
+ <Clock className="mr-1 h-3 w-3" />
65
+ {t("Pending")}
66
+ </Badge>
67
+ );
68
+ case "failed":
69
+ return (
70
+ <Badge className="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100">
71
+ <XCircle className="mr-1 h-3 w-3" />
72
+ {t("Failed")}
73
+ </Badge>
74
+ );
75
+ case "skipped":
76
+ return (
77
+ <Badge className="bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-100">
78
+ <MinusCircle className="mr-1 h-3 w-3" />
79
+ {t("Skipped")}
80
+ </Badge>
81
+ );
82
+ default:
83
+ return null;
84
+ }
85
+ };
86
+
87
+ return (
88
+ <Card>
89
+ <CardHeader className="pb-3">
90
+ <CardTitle className="flex items-center justify-between text-lg">
91
+ <span>{t("Fiscalization")}</span>
92
+ {getStatusBadge()}
93
+ </CardTitle>
94
+ </CardHeader>
95
+ <CardContent className="space-y-3">
96
+ <div className="text-muted-foreground text-sm">
97
+ {label} &middot;{" "}
98
+ {fiscalizationData.fiscalized_at && new Date(fiscalizationData.fiscalized_at).toLocaleString(locale)}
99
+ </div>
100
+
101
+ {fiscalizationData.status === "failed" && fiscalizationData.error && (
102
+ <Alert variant="destructive">
103
+ <AlertCircle className="h-4 w-4" />
104
+ <AlertDescription className="text-sm">{fiscalizationData.error}</AlertDescription>
105
+ </Alert>
106
+ )}
107
+
108
+ {fiscalizationData.status === "skipped" && (fiscalizationData as any).data?.reason && (
109
+ <div className="text-muted-foreground text-sm">{(fiscalizationData as any).data.reason}</div>
110
+ )}
111
+
112
+ {fiscalizationData.status === "failed" && onRetry && (
113
+ <Button variant="outline" size="sm" onClick={onRetry} disabled={isRetrying}>
114
+ {isRetrying ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <RefreshCw className="mr-2 h-4 w-4" />}
115
+ {t("Retry fiscalization")}
116
+ </Button>
117
+ )}
118
+ </CardContent>
119
+ </Card>
120
+ );
121
+ }
@@ -154,3 +154,6 @@ const updateCreditNoteSchemaDefinition = z
154
154
  export type UpdateCreditNoteSchema = z.infer<typeof updateCreditNoteSchemaDefinition>;
155
155
 
156
156
  export const updateCreditNoteSchema = updateCreditNoteSchemaDefinition;
157
+
158
+ // Re-export invoice create schema as credit note create schema (same body structure)
159
+ export { createInvoiceSchema as createCreditNoteSchema, type CreateInvoiceSchema as CreateCreditNoteSchema } from './invoice';
@@ -1,12 +1,12 @@
1
1
  import type {
2
- AdvanceInvoiceWithItems,
2
+ AdvanceInvoice,
3
3
  CreateAdvanceInvoiceRequest,
4
4
  CreateCreditNoteRequest,
5
5
  CreateEstimateRequest,
6
6
  CreateInvoiceRequest,
7
- CreditNoteWithItems,
8
- EstimateWithItems,
9
- InvoiceWithItems,
7
+ CreditNote,
8
+ Estimate,
9
+ Invoice,
10
10
  } from "@spaceinvoices/js-sdk";
11
11
  import { useQuery } from "@tanstack/react-query";
12
12
 
@@ -14,7 +14,7 @@ import { useEntities } from "@/ui/providers/entities-context";
14
14
  import { useSDK } from "@/ui/providers/sdk-provider";
15
15
 
16
16
  export type DocumentType = "invoice" | "estimate" | "credit_note" | "advance_invoice";
17
- type Document = InvoiceWithItems | EstimateWithItems | CreditNoteWithItems | AdvanceInvoiceWithItems;
17
+ type Document = Invoice | Estimate | CreditNote | AdvanceInvoice;
18
18
  type CreateRequest =
19
19
  | CreateInvoiceRequest
20
20
  | CreateEstimateRequest
@@ -56,16 +56,25 @@ export function getAllowedDuplicateTargets(sourceType: DocumentType): DocumentTy
56
56
  */
57
57
  function transformDocumentForDuplication(source: Document, _targetType: DocumentType): Partial<CreateRequest> {
58
58
  // Transform items - copy only the fields needed for creation
59
- const items = source.items?.map((item: (typeof source.items)[number]) => ({
59
+ // Use type assertion for items since all document item types share the same shape
60
+ const sourceItems = source.items as Array<{
61
+ name: string;
62
+ description: string | null;
63
+ quantity: number;
64
+ price: number;
65
+ gross_price?: number | null;
66
+ taxes: Array<{ tax_id?: string }>;
67
+ }>;
68
+ const items = sourceItems?.map((item) => ({
60
69
  name: item.name,
61
70
  description: item.description,
62
71
  quantity: item.quantity,
63
72
  // Use gross_price if set, otherwise use price. The form uses is_gross_price as a UI toggle.
64
- price: (item as { gross_price?: number }).gross_price ?? item.price,
73
+ price: item.gross_price ?? item.price,
65
74
  // Copy tax references (tax_id), not computed tax data
66
- taxes: item.taxes?.map((tax: { tax_id?: string }) => ({ tax_id: tax.tax_id })),
75
+ taxes: item.taxes?.map((tax) => ({ tax_id: tax.tax_id })),
67
76
  // Derive is_gross_price from whether gross_price is set
68
- gross_price: (item as { gross_price?: number }).gross_price,
77
+ gross_price: item.gross_price ?? undefined,
69
78
  }));
70
79
 
71
80
  // Build customer data - always copy if available (form needs this for display)
@@ -158,8 +167,7 @@ export function useDuplicateDocument({
158
167
  if (sourceType === "invoice") {
159
168
  source = await sdk.invoices.get(sourceId, undefined, { entity_id: activeEntity.id });
160
169
  } else if (sourceType === "estimate") {
161
- // estimates.get only takes 2 args (no params)
162
- source = await sdk.estimates.get(sourceId, { entity_id: activeEntity.id });
170
+ source = await sdk.estimates.get(sourceId, undefined, { entity_id: activeEntity.id });
163
171
  } else if (sourceType === "advance_invoice") {
164
172
  source = await sdk.advanceInvoices.get(sourceId, undefined, { entity_id: activeEntity.id });
165
173
  } else {
@@ -180,6 +180,27 @@ export function EntitiesProvider({
180
180
  }
181
181
  }, [memoizedEntities, urlEntityId]); // Re-run when URL entity changes
182
182
 
183
+ // When urlEntityId is provided but not found in current environment, auto-switch
184
+ const urlEntityFallbackAttempted = useRef<string | null>(null);
185
+ useEffect(() => {
186
+ if (!urlEntityId || isLoading) return;
187
+ if (memoizedEntities.length === 0) return;
188
+
189
+ const found = memoizedEntities.some((e) => e.id === urlEntityId);
190
+ if (found) {
191
+ urlEntityFallbackAttempted.current = null;
192
+ return;
193
+ }
194
+
195
+ // Prevent infinite switching between environments
196
+ if (urlEntityFallbackAttempted.current === urlEntityId) return;
197
+ urlEntityFallbackAttempted.current = urlEntityId;
198
+
199
+ // URL entity not in current environment — try the other
200
+ const altEnv: EntityEnvironment = environment === "live" ? "sandbox" : "live";
201
+ setEnvironmentState(altEnv);
202
+ }, [urlEntityId, memoizedEntities, isLoading, environment]);
203
+
183
204
  const cookieOpts = useMemo(
184
205
  () => ({
185
206
  path: "/",
@@ -1,196 +0,0 @@
1
- import type { Invoice } from "@spaceinvoices/js-sdk";
2
- import { AlertCircle, Check, CheckCircle2, Clock, Copy, ExternalLink, XCircle } from "lucide-react";
3
- import { useState } from "react";
4
- import { Alert, AlertDescription, AlertTitle } from "@/ui/components/ui/alert";
5
- import { Badge } from "@/ui/components/ui/badge";
6
- import { Button } from "@/ui/components/ui/button";
7
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/ui/components/ui/card";
8
- import type { ComponentTranslationProps } from "@/ui/lib/translation";
9
- import { createTranslation } from "@/ui/lib/translation";
10
- import de from "../../entities/fina-settings-form/locales/de";
11
- import en from "../../entities/fina-settings-form/locales/en";
12
- import sl from "../../entities/fina-settings-form/locales/sl";
13
-
14
- const translations = { de, sl, en } as const;
15
-
16
- type FinaData = {
17
- status?: "success" | "pending" | "failed";
18
- error?: string;
19
- fiscalized_at?: string;
20
- data?: {
21
- zki?: string;
22
- jir?: string;
23
- premise_id?: string;
24
- device_id?: string;
25
- invoice_number?: string;
26
- qr_code_url?: string;
27
- };
28
- };
29
-
30
- interface FinaInfoDisplayProps extends ComponentTranslationProps {
31
- invoice: Invoice;
32
- }
33
-
34
- export function FinaInfoDisplay({ invoice, t: translateFn, namespace, locale }: FinaInfoDisplayProps) {
35
- const [copiedField, setCopiedField] = useState<string | null>(null);
36
-
37
- const t = createTranslation({
38
- t: translateFn,
39
- namespace,
40
- locale,
41
- translations,
42
- });
43
-
44
- const fina = (invoice as any).fina as FinaData | undefined;
45
-
46
- if (!fina) {
47
- return null;
48
- }
49
-
50
- const copyToClipboard = async (text: string, fieldName: string) => {
51
- try {
52
- await navigator.clipboard.writeText(text);
53
- setCopiedField(fieldName);
54
- setTimeout(() => setCopiedField(null), 2000);
55
- } catch (err) {
56
- console.error("Failed to copy:", err);
57
- }
58
- };
59
-
60
- const getStatusBadge = () => {
61
- switch (fina.status) {
62
- case "success":
63
- return (
64
- <Badge className="bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-100">
65
- <CheckCircle2 className="mr-1 h-3 w-3" />
66
- {t("Fiscalized")}
67
- </Badge>
68
- );
69
- case "pending":
70
- return (
71
- <Badge className="bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100">
72
- <Clock className="mr-1 h-3 w-3" />
73
- {t("Pending")}
74
- </Badge>
75
- );
76
- case "failed":
77
- return (
78
- <Badge className="bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-100">
79
- <XCircle className="mr-1 h-3 w-3" />
80
- {t("Failed")}
81
- </Badge>
82
- );
83
- default:
84
- return null;
85
- }
86
- };
87
-
88
- return (
89
- <Card>
90
- <CardHeader>
91
- <CardTitle className="flex items-center gap-2">
92
- {t("FINA Fiscalization")}
93
- {getStatusBadge()}
94
- </CardTitle>
95
- <CardDescription>{t("Croatian tax authority fiscalization details")}</CardDescription>
96
- </CardHeader>
97
- <CardContent className="space-y-4">
98
- {/* Error Message */}
99
- {fina.status === "failed" && fina.error && (
100
- <Alert variant="destructive">
101
- <AlertCircle className="h-4 w-4" />
102
- <AlertTitle>{t("Fiscalization Error")}</AlertTitle>
103
- <AlertDescription>{fina.error}</AlertDescription>
104
- </Alert>
105
- )}
106
-
107
- {/* FINA Data */}
108
- {fina.data && (
109
- <div className="space-y-3">
110
- {/* ZKI Code */}
111
- {fina.data.zki && (
112
- <div className="flex items-center justify-between rounded-lg border p-3">
113
- <div className="space-y-1">
114
- <p className="font-medium text-sm">{t("ZKI")}</p>
115
- <p className="font-mono text-muted-foreground text-xs">{fina.data.zki}</p>
116
- </div>
117
- <Button variant="ghost" size="sm" onClick={() => copyToClipboard(fina.data!.zki!, "zki")}>
118
- {copiedField === "zki" ? <Check className="h-4 w-4 text-green-600" /> : <Copy className="h-4 w-4" />}
119
- </Button>
120
- </div>
121
- )}
122
-
123
- {/* JIR Code */}
124
- {fina.data.jir && (
125
- <div className="flex items-center justify-between rounded-lg border p-3">
126
- <div className="space-y-1">
127
- <p className="font-medium text-sm">{t("JIR")}</p>
128
- <p className="font-mono text-muted-foreground text-xs">{fina.data.jir}</p>
129
- </div>
130
- <Button variant="ghost" size="sm" onClick={() => copyToClipboard(fina.data!.jir!, "jir")}>
131
- {copiedField === "jir" ? <Check className="h-4 w-4 text-green-600" /> : <Copy className="h-4 w-4" />}
132
- </Button>
133
- </div>
134
- )}
135
-
136
- {/* Business Premise & Device */}
137
- <div className="grid grid-cols-2 gap-3">
138
- {fina.data.premise_id && (
139
- <div className="space-y-1">
140
- <p className="font-medium text-sm">{t("Business Premise")}</p>
141
- <p className="text-muted-foreground text-sm">{fina.data.premise_id}</p>
142
- </div>
143
- )}
144
- {fina.data.device_id && (
145
- <div className="space-y-1">
146
- <p className="font-medium text-sm">{t("Electronic Device")}</p>
147
- <p className="text-muted-foreground text-sm">{fina.data.device_id}</p>
148
- </div>
149
- )}
150
- </div>
151
-
152
- {/* Invoice Number */}
153
- {fina.data.invoice_number && (
154
- <div className="space-y-1">
155
- <p className="font-medium text-sm">{t("Invoice Number")}</p>
156
- <p className="text-muted-foreground text-sm">{fina.data.invoice_number}</p>
157
- </div>
158
- )}
159
-
160
- {/* QR Code URL */}
161
- {fina.data.qr_code_url && (
162
- <div className="flex items-center justify-between rounded-lg border p-3">
163
- <div className="space-y-1">
164
- <p className="font-medium text-sm">{t("QR Code")}</p>
165
- <a
166
- href={fina.data.qr_code_url}
167
- target="_blank"
168
- rel="noopener noreferrer"
169
- className="flex items-center gap-1 text-primary text-xs hover:underline"
170
- >
171
- {t("QR Code")}
172
- <ExternalLink className="h-3 w-3" />
173
- </a>
174
- </div>
175
- <Button variant="ghost" size="sm" onClick={() => copyToClipboard(fina.data!.qr_code_url!, "qr_url")}>
176
- {copiedField === "qr_url" ? (
177
- <Check className="h-4 w-4 text-green-600" />
178
- ) : (
179
- <Copy className="h-4 w-4" />
180
- )}
181
- </Button>
182
- </div>
183
- )}
184
- </div>
185
- )}
186
-
187
- {/* Fiscalized Timestamp */}
188
- {fina.fiscalized_at && (
189
- <div className="pt-2 text-muted-foreground text-sm">
190
- {t("Fiscalized at")}: {new Date(fina.fiscalized_at).toLocaleString()}
191
- </div>
192
- )}
193
- </CardContent>
194
- </Card>
195
- );
196
- }