Skip to content

Commit 8453e1e

Browse files
committed
Invoicing changes and corrections
1 parent 8e17422 commit 8453e1e

20 files changed

Lines changed: 1340 additions & 515 deletions

backend/src/server.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ const defaultSettings: JsonRecord = {
9797
email: 'info@mackodental.hu',
9898
website: 'www.mackodental.hu',
9999
},
100-
doctors: [{ id: 'doc-1', name: 'Dr. Dul Zoltán', stampNumber: '' }],
100+
doctors: [{ id: 'DOC0001', name: 'Dr. Dul Zoltán', stampNumber: '' }],
101101
pdf: {
102102
hu: {
103103
footerText: 'Az árajánlat tájékoztató jellegű és a fent jelölt ideig érvényes.',
@@ -387,6 +387,7 @@ async function nextInvoiceId(patientId: string): Promise<string> {
387387

388388
async function nextDoctorId(): Promise<string> {
389389
const last = await prisma.doctor.findFirst({
390+
where: { doctorId: { startsWith: 'DOC' } },
390391
orderBy: { doctorId: 'desc' },
391392
select: { doctorId: true },
392393
});
@@ -1818,8 +1819,11 @@ server.patch('/quotes/:quoteId/restore', async (request, reply) => {
18181819

18191820
// Settings
18201821
server.get('/settings', async () => {
1821-
const settings = await prisma.appSettings.findUnique({ where: { id: 'default' } });
1822-
const result = parseJsonObject(settings?.data, defaultSettings);
1822+
const [settingsRow, doctors] = await Promise.all([
1823+
prisma.appSettings.findUnique({ where: { id: 'default' } }),
1824+
prisma.doctor.findMany({ orderBy: { doctorId: 'asc' } }),
1825+
]);
1826+
const result = parseJsonObject(settingsRow?.data, defaultSettings);
18231827
// Migrate old flat pdf format { footerText, warrantyText } to per-language { hu: {...}, en: {...}, de: {...} }
18241828
const pdf = result.pdf as Record<string, unknown> | undefined;
18251829
if (pdf && (typeof pdf.footerText === 'string' || typeof pdf.warrantyText === 'string')) {
@@ -1835,6 +1839,10 @@ server.get('/settings', async () => {
18351839
if (quote && !quote.quoteLang) {
18361840
quote.quoteLang = 'hu';
18371841
}
1842+
// Override doctors from Doctor table
1843+
if (doctors.length > 0) {
1844+
result.doctors = doctors.map((d) => ({ id: d.doctorId, name: d.doctorName, stampNumber: d.doctorNum }));
1845+
}
18381846
return result;
18391847
});
18401848

@@ -2525,10 +2533,10 @@ const toDateOnly = (value: string) => {
25252533

25262534
const normalizePaymentMethod = (value: string) => {
25272535
const normalized = String(value || '').toLowerCase();
2528-
if (normalized === 'atutalas') return 'Atutalas';
2529-
if (normalized === 'keszpenz') return 'Keszpenz';
2530-
if (normalized === 'bankkartya') return 'Bankkartya';
2531-
return value || 'Atutalas';
2536+
if (normalized === 'atutalas' || normalized === 'átutalás') return 'Átutalás';
2537+
if (normalized === 'keszpenz' || normalized === 'készpénz') return 'Készpénz';
2538+
if (normalized === 'bankkartya' || normalized === 'bankkártya') return 'Bankkártya';
2539+
return value || 'Bankkártya';
25322540
};
25332541

25342542
const validateAndNormalizePayload = (input: JsonRecord) => {
@@ -2550,24 +2558,26 @@ const validateAndNormalizePayload = (input: JsonRecord) => {
25502558
const normalizedItems = items.map((item, index) => {
25512559
const qty = Number(item.qty);
25522560
const unitPriceNet = Number(item.unitPriceNet ?? item.unitPrice ?? 0);
2553-
const vatRate = Number(item.vatRate ?? 27);
2561+
const isTAM = item.vatRate === 'TAM';
2562+
const vatRateNum = isTAM ? 0 : Number(item.vatRate ?? 27);
2563+
const vatRateLabel: number | string = isTAM ? 'TAM' : vatRateNum;
25542564

25552565
if (!Number.isFinite(qty) || qty <= 0) errors.push(`Tétel ${index + 1}: qty > 0 kötelező`);
25562566
if (!Number.isFinite(unitPriceNet) || (!isVegszamla && unitPriceNet < 0))
25572567
errors.push(`Tétel ${index + 1}: unitPriceNet >= 0 kötelező`);
2558-
if (!Number.isFinite(vatRate) || vatRate < 0)
2568+
if (!isTAM && (!Number.isFinite(vatRateNum) || vatRateNum < 0))
25592569
errors.push(`Tétel ${index + 1}: vatRate >= 0 kötelező`);
25602570

25612571
const net = round(qty * unitPriceNet);
2562-
const vat = round((net * vatRate) / 100);
2572+
const vat = round((net * vatRateNum) / 100);
25632573
const gross = round(net + vat);
25642574

25652575
return {
25662576
name: item.name || `Tétel ${index + 1}`,
25672577
unit: item.unit || 'db',
25682578
qty,
25692579
unitPriceNet,
2570-
vatRate,
2580+
vatRate: vatRateLabel,
25712581
net,
25722582
vat,
25732583
gross,
@@ -2591,7 +2601,7 @@ const validateAndNormalizePayload = (input: JsonRecord) => {
25912601
seller,
25922602
buyer,
25932603
invoice: {
2594-
paymentMethod: normalizePaymentMethod(String(invoice.paymentMethod || 'atutalas')),
2604+
paymentMethod: normalizePaymentMethod(String(invoice.paymentMethod || 'bankkártya')),
25952605
fulfillmentDate: toDateOnly(String(invoice.fulfillmentDate || '')),
25962606
dueDate: toDateOnly(String(invoice.dueDate || '')),
25972607
issueDate: toDateOnly(String(invoice.issueDate || '')),
@@ -2620,7 +2630,7 @@ const buildInvoiceXml = (params: {
26202630
unit: unknown;
26212631
qty: number;
26222632
unitPriceNet: number;
2623-
vatRate: number;
2633+
vatRate: number | string;
26242634
net: number;
26252635
vat: number;
26262636
gross: number;

src/context/SettingsContext.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
interface SettingsContextType {
1515
settings: Settings;
1616
updateSettings: (settings: Settings) => void;
17+
refreshSettings: () => void;
1718
appLanguage: OdontogramLanguage;
1819
setAppLanguage: (language: OdontogramLanguage) => void;
1920
odontogramNumbering: OdontogramNumberingSystem;
@@ -41,6 +42,10 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) {
4142
setSettings(syncedSettings);
4243
}, [appLanguage]);
4344

45+
const refreshSettings = useCallback(() => {
46+
setSettings(storage.getSettings());
47+
}, []);
48+
4449
const setAppLanguage = useCallback((language: OdontogramLanguage) => {
4550
persistOdontogramLanguage(language);
4651
localStorage.setItem('dqc_app_language', language);
@@ -83,6 +88,7 @@ export function SettingsProvider({ children }: { children: React.ReactNode }) {
8388
value={{
8489
settings,
8590
updateSettings,
91+
refreshSettings,
8692
appLanguage,
8793
setAppLanguage,
8894
odontogramNumbering,

src/data/defaultSettings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export const defaultSettings: Settings = {
99
website: 'www.mackodental.hu',
1010
},
1111
doctors: [
12-
{ id: 'doc-1', name: 'Dr. Dul Zoltán', stampNumber: '' },
12+
{ id: 'DOC0001', name: 'Dr. Dul Zoltán', stampNumber: '' },
1313
],
1414
pdf: {
1515
hu: {
@@ -145,7 +145,8 @@ Bitte lesen Sie sorgfältig und kontaktieren Sie uns bei Fragen, dann bestätige
145145
invoice: {
146146
invoiceType: 'paper',
147147
defaultComment: '',
148-
defaultVatRate: 0,
148+
defaultVatRate: 'TAM',
149+
defaultPaymentMethod: 'bankkártya',
149150
},
150151
patient: {
151152
defaultCountry: 'Magyarország',

src/hooks/useQuotes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ export function useQuotes() {
243243
};
244244
const updated: Quote = {
245245
...existing,
246-
events: [...existing.events, fullEvent],
246+
events: [...(existing.events || []), fullEvent],
247247
};
248248
updateQuote(updated);
249249
return updated;
@@ -264,7 +264,7 @@ export function useQuotes() {
264264
...existing,
265265
quoteStatus: newStatus,
266266
lastStatusChangeAt: now,
267-
events: [...existing.events, createEvent(eventType, doctorName)],
267+
events: [...(existing.events || []), createEvent(eventType, doctorName)],
268268
};
269269
updateQuote(updated);
270270
return updated;

src/i18n/de.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export const de: TranslationKeys = {
129129
modifiedAt: 'Geändert',
130130
quoteType: 'Typ',
131131
validUntil: 'Gültig bis',
132-
quoteValidity: 'Angebotsgültigkeit',
132+
quoteValidity: 'Gültig bis',
133133
quoteValidityDescription: 'Das Angebot ist bis zum folgenden Datum gültig:',
134134
quoteValidityAdjust: 'Anpassen:',
135135
quoteValidityDays: 'Tage',
@@ -141,7 +141,7 @@ export const de: TranslationKeys = {
141141
statusStarted: 'Begonnen',
142142
statusCompleted: 'Abgeschlossen',
143143
statusDeleted: 'Gelöscht',
144-
acceptanceCardTitle: 'Kostenvoranschlag Status',
144+
acceptanceCardTitle: 'Status',
145145
acceptanceDraft: 'Der Kostenvoranschlag wird bearbeitet.',
146146
acceptanceClosed: 'Der Kostenvoranschlag wurde geschlossen.',
147147
acceptanceStarted: 'Der Kostenvoranschlag wurde angenommen.',
@@ -170,7 +170,8 @@ export const de: TranslationKeys = {
170170
unitPrice: 'Einzelpreis',
171171
lineDiscount: 'Rabatt',
172172
lineTotal: 'Gesamt',
173-
subtotal: 'Zwischensumme',
173+
subtotal: 'Gesamtsumme des Kostenvoranschlags',
174+
treatmentNTotal: 'Behandlung {n}',
174175
globalDiscount: 'Globaler Rabatt',
175176
total: 'Zu zahlen',
176177
discountPercent: '%',
@@ -381,6 +382,7 @@ export const de: TranslationKeys = {
381382
invoiceComment: 'Rechnungskommentar',
382383
invoiceCommentHelp: 'Standardkommentar, der auf der Rechnung erscheint',
383384
defaultVatRate: 'Standard-MwSt.-Satz',
385+
defaultPaymentMethod: 'Standard-Zahlungsmethode',
384386
quotesPerPageLabel: 'Kostenvoranschläge pro Seite',
385387
patientsPerPageLabel: 'Patienten pro Seite',
386388
},
@@ -479,6 +481,7 @@ export const de: TranslationKeys = {
479481
mockLoadDescription: 'Die Testdaten überschreiben die aktuellen Daten.',
480482
mockLoadButton: 'Test laden',
481483
mockLoadSuccess: 'Testdaten erfolgreich geladen!',
484+
mockLoadConfirm: 'Möchten Sie wirklich Testdaten laden? Alle aktuellen Daten werden gelöscht und überschrieben!',
482485
clearAllTitle: 'Alle Daten löschen',
483486
clearAllDescription: 'Alle Daten aus der Datenbank löschen und auf null zurücksetzen.',
484487
clearAllButton: 'Alle Daten löschen',
@@ -560,19 +563,27 @@ export const de: TranslationKeys = {
560563
invoiceTypeNormal: 'Rechnung',
561564
invoiceTypeAdvance: 'Vorschussrechnung',
562565
invoiceTypeFinal: 'Schlussrechnung',
566+
invoiceTypeStorno: 'Stornorechnung',
563567
advanceItemName: 'Zahnarztkosten-Vorschuss',
564568
advanceAmount: 'Vorschussbetrag',
565569
summarySection: 'Zusammenfassung',
566570
totalAmount: 'Gesamt',
567571
quoteRemainingPart: 'Verbleibender Angebotsbetrag',
568572
noneRemaining: 'keine',
569-
eventInvoicePartial: 'Rechnung erstellt ({number}) für {amount}. Restbetrag: {remaining}.',
570-
eventInvoiceFull: 'Rechnung erstellt ({number}) für {amount}. Der gesamte Betrag wurde in Rechnung gestellt.',
571-
eventAdvanceCreated: 'Vorschussrechnung erstellt ({number}) für {amount}. Restbetrag: {remaining}.',
572-
eventFinalCreated: 'Schlussrechnung erstellt ({number}) für {amount}. Der gesamte Betrag wurde in Rechnung gestellt.',
573+
eventInvoicePartial: 'Rechnung erstellt: {number}, Betrag: {amount}, Restbetrag: {remaining}.',
574+
eventInvoiceFull: 'Rechnung erstellt: {number}, Betrag: {amount}. Der gesamte Betrag wurde abgerechnet.',
575+
eventAdvanceCreated: 'Vorschussrechnung erstellt: {number}, Betrag: {amount}, Restbetrag: {remaining}.',
576+
eventFinalCreated: 'Schlussrechnung erstellt: {number}, Betrag: {amount}.',
577+
eventStornoCreated: 'Stornorechnung erstellt: {stornoNumber}, stornierte Rechnung: {originalNumber}, Betrag: -{amount}.',
573578
invoicedItem: 'Abgerechnete Position mit Rechnung {invoiceNumber}',
574579
completedAndFullyInvoiced: 'Das Angebot wurde abgeschlossen und vollständig abgerechnet.',
575580
maxInvoicesReached: 'Der Patient hat die maximale Anzahl von 999 Rechnungen erreicht. Wenden Sie sich an den Administrator!',
581+
billingDetails: 'Rechnungsdaten',
582+
billingName: 'Rechnungsname',
583+
billingAddress: 'Rechnungsadresse',
584+
relatedQuote: 'Zugehöriges Angebot',
585+
vatContent: 'MwSt.-Inhalt',
586+
noQuote: 'keine',
576587
},
577588
pdf: {
578589
quote: 'ANGEBOT',

src/i18n/en.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ export const en: TranslationKeys = {
136136
modifiedAt: 'Modified',
137137
quoteType: 'Type',
138138
validUntil: 'Valid Until',
139-
quoteValidity: 'Quote Validity',
139+
quoteValidity: 'Valid until',
140140
quoteValidityDescription: 'The quote is valid until the following date:',
141141
quoteValidityAdjust: 'Adjust:',
142142
quoteValidityDays: 'days',
@@ -149,7 +149,7 @@ export const en: TranslationKeys = {
149149
statusStarted: 'Started',
150150
statusCompleted: 'Completed',
151151
statusDeleted: 'Deleted',
152-
acceptanceCardTitle: 'Quote Status',
152+
acceptanceCardTitle: 'Status',
153153
acceptanceDraft: 'The quote is being edited.',
154154
acceptanceClosed: 'The quote has been closed.',
155155
acceptanceStarted: 'The quote has been accepted.',
@@ -181,7 +181,8 @@ export const en: TranslationKeys = {
181181
unitPrice: 'Unit Price',
182182
lineDiscount: 'Discount',
183183
lineTotal: 'Total',
184-
subtotal: 'Subtotal',
184+
subtotal: 'Quote Full Total',
185+
treatmentNTotal: 'Treatment {n}',
185186
globalDiscount: 'Global Discount',
186187
total: 'Total Due',
187188
discountPercent: '%',
@@ -400,6 +401,7 @@ export const en: TranslationKeys = {
400401
invoiceComment: 'Invoice Comment',
401402
invoiceCommentHelp: 'Default comment that appears on the invoice',
402403
defaultVatRate: 'Default VAT Rate',
404+
defaultPaymentMethod: 'Default Payment Method',
403405
quotesPerPageLabel: 'Quotes per page',
404406
patientsPerPageLabel: 'Patients per page',
405407
},
@@ -501,6 +503,7 @@ export const en: TranslationKeys = {
501503
mockLoadDescription: 'Sample data will overwrite the current data.',
502504
mockLoadButton: 'Load Sample',
503505
mockLoadSuccess: 'Sample data loaded successfully!',
506+
mockLoadConfirm: 'Are you sure you want to load sample data? This will delete and overwrite all current data!',
504507
clearAllTitle: 'Delete All Data',
505508
clearAllDescription: 'Delete all data from the database and reset to zero.',
506509
clearAllButton: 'Delete All Data',
@@ -584,19 +587,27 @@ export const en: TranslationKeys = {
584587
invoiceTypeNormal: 'Invoice',
585588
invoiceTypeAdvance: 'Advance invoice',
586589
invoiceTypeFinal: 'Final invoice',
590+
invoiceTypeStorno: 'Storno invoice',
587591
advanceItemName: 'Dental cost advance',
588592
advanceAmount: 'Advance amount',
589593
summarySection: 'Summary',
590594
totalAmount: 'Total',
591595
quoteRemainingPart: 'Quote remaining part',
592596
noneRemaining: 'none',
593-
eventInvoicePartial: 'Invoice issued ({number}) for {amount}. Remaining amount: {remaining}.',
594-
eventInvoiceFull: 'Invoice issued ({number}) for {amount}. The full amount has been invoiced.',
595-
eventAdvanceCreated: 'Advance invoice issued ({number}) for {amount}. Remaining amount: {remaining}.',
596-
eventFinalCreated: 'Final invoice issued ({number}) for {amount}. The full amount has been invoiced.',
597+
eventInvoicePartial: 'Invoice issued: {number}, Amount: {amount}, Remaining: {remaining}.',
598+
eventInvoiceFull: 'Invoice issued: {number}, Amount: {amount}. The full amount has been invoiced.',
599+
eventAdvanceCreated: 'Advance invoice issued: {number}, Amount: {amount}, Remaining: {remaining}.',
600+
eventFinalCreated: 'Final invoice issued: {number}, Amount: {amount}.',
601+
eventStornoCreated: 'Storno invoice issued: {stornoNumber}, cancelled invoice: {originalNumber}, Amount: -{amount}.',
597602
invoicedItem: 'Invoiced item with invoice {invoiceNumber}',
598603
completedAndFullyInvoiced: 'The quote has been completed and fully invoiced.',
599604
maxInvoicesReached: 'Patient has reached the maximum of 999 invoices. Contact the administrator for help!',
605+
billingDetails: 'Billing details',
606+
billingName: 'Billing name',
607+
billingAddress: 'Billing address',
608+
relatedQuote: 'Related quote',
609+
vatContent: 'VAT content',
610+
noQuote: 'none',
600611
},
601612

602613
// PDF

0 commit comments

Comments
 (0)