mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-17 20:09:41 +02:00
feat(invoices): full i18n coverage across 12 files — DE/EN/ES/FR/IT
Invoices module had 81 hardcoded German strings across ListView,
DetailView, InvoiceForm, SenderProfileForm, ClientPicker, LinesEditor,
SendModal, StatusBadge, the open-invoices widget, and 4 routes. New
`invoices` namespace (~215 keys × 5 locales = ~1075 translations) covers
list/detail/form/picker/sender-form/send-modal + Swiss + German VAT-rate
labels.
- constants.ts: STATUS_LABELS still kept as a literal map for non-Svelte
callers (mail-template, PDF renderer); Svelte components now use
`$_('invoices.status.<status>')`. VAT_RATES_CH/DE switched from
literal `label` to `i18nKey`, resolved per-component via $_.
- Locale-aware Date.toLocaleString in DetailView meta + SenderProfileForm
saved-at timestamp (was hardcoded 'de-DE'/default).
- Baseline ratchet: 1817 → 1753 (64 invoices strings + a handful of
SettingsSidebar follow-ons cleared).
- validate:i18n-parity: 40 namespaces × 5 locales — 3596 keys aligned
- svelte-check: 7647 files, 0 errors
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f10439e369
commit
679fb160c2
20 changed files with 1541 additions and 243 deletions
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/de.json
Normal file
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/de.json
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"list": {
|
||||
"title": "Rechnungen",
|
||||
"subtitle": "Rechnungen stellen und Zahlungen verfolgen",
|
||||
"new_invoice": "+ Neue Rechnung",
|
||||
"settings_aria": "Einstellungen",
|
||||
"stat_open": "Offen",
|
||||
"stat_overdue": "Überfällig",
|
||||
"stat_invoiced_ytd": "Fakturiert {year}",
|
||||
"stat_paid_ytd": "Bezahlt {year}",
|
||||
"stat_count": "{count} Rechnungen",
|
||||
"chip_all": "Alle",
|
||||
"search_placeholder": "Suchen (Nummer, Kunde, Betreff)",
|
||||
"empty_title": "Noch keine Rechnungen",
|
||||
"empty_body": "Stelle deine erste Rechnung — inklusive PDF-Export und Schweizer QR-Bill (M5).",
|
||||
"empty_cta": "Erste Rechnung erstellen",
|
||||
"empty_filtered": "Keine Rechnungen gefunden."
|
||||
},
|
||||
"status": {
|
||||
"draft": "Entwurf",
|
||||
"sent": "Versendet",
|
||||
"paid": "Bezahlt",
|
||||
"overdue": "Überfällig",
|
||||
"void": "Storniert"
|
||||
},
|
||||
"detail": {
|
||||
"title_default": "Rechnung",
|
||||
"due": "Fällig {date}",
|
||||
"edit": "Bearbeiten",
|
||||
"send_via_mail": "Per Mail versenden",
|
||||
"mark_sent": "Als versendet markieren",
|
||||
"mark_paid": "Als bezahlt markieren",
|
||||
"download_pdf": "PDF herunterladen",
|
||||
"duplicate": "Duplizieren",
|
||||
"cancel": "Stornieren",
|
||||
"delete": "Löschen",
|
||||
"confirm_void": "Diese Rechnung stornieren?",
|
||||
"confirm_delete": "Rechnung endgültig löschen?",
|
||||
"action_mark_sent": "Als versendet markieren",
|
||||
"action_mark_paid": "Als bezahlt markieren",
|
||||
"action_void": "Stornieren",
|
||||
"action_duplicate": "Duplizieren",
|
||||
"action_delete": "Löschen",
|
||||
"err_action_failed": "{label} fehlgeschlagen",
|
||||
"err_pdf_render": "PDF-Rendering fehlgeschlagen",
|
||||
"err_download": "Download fehlgeschlagen",
|
||||
"err_pdf": "PDF-Fehler: {error}",
|
||||
"preview_title": "Vorschau",
|
||||
"preview_rendering": "Rendert …",
|
||||
"preview_iframe_title": "Vorschau Rechnung {number}",
|
||||
"qr_warning_strong": "QR-Rechnung nicht eingefügt:",
|
||||
"qr_open_settings": "Einstellungen öffnen →",
|
||||
"raw_summary": "Strukturierte Daten anzeigen",
|
||||
"section_recipient": "Empfänger",
|
||||
"section_lines": "Positionen",
|
||||
"section_totals": "Summe",
|
||||
"section_notes": "Notizen",
|
||||
"section_terms": "Zahlungsbedingungen",
|
||||
"label_vat_number": "MwSt-Nr.: {number}",
|
||||
"th_position": "Position",
|
||||
"th_quantity": "Menge",
|
||||
"th_unit_price": "Einzelpreis",
|
||||
"th_vat": "MwSt.",
|
||||
"th_net": "Netto",
|
||||
"total_label_net": "Netto",
|
||||
"total_label_vat": "MwSt. {rate}%",
|
||||
"total_label_total": "Total",
|
||||
"meta_status": "Status: {label}",
|
||||
"meta_sent": "Versendet: {date}",
|
||||
"meta_paid": "Bezahlt: {date}"
|
||||
},
|
||||
"form": {
|
||||
"section_client": "Kunde",
|
||||
"section_invoice": "Rechnung",
|
||||
"section_lines": "Positionen",
|
||||
"section_totals": "Summe",
|
||||
"section_notes": "Notizen & Zahlungsbedingungen",
|
||||
"label_subject": "Betreff",
|
||||
"placeholder_subject": "Beratungsleistung April",
|
||||
"label_currency": "Währung",
|
||||
"label_issue_date": "Rechnungsdatum",
|
||||
"label_due_date": "Fällig am",
|
||||
"label_notes": "Notizen (unter den Positionen)",
|
||||
"label_terms": "Zahlungsbedingungen / AGB",
|
||||
"cancel": "Abbrechen",
|
||||
"saving": "Speichert …",
|
||||
"submit_create": "Als Entwurf speichern",
|
||||
"submit_update": "Änderungen speichern",
|
||||
"err_client_required": "Kunde ist erforderlich",
|
||||
"err_min_one_line": "Mindestens eine Position hinzufügen",
|
||||
"err_save": "Speichern fehlgeschlagen",
|
||||
"total_label_net": "Netto",
|
||||
"total_label_vat": "MwSt. {rate}%",
|
||||
"total_label_total": "Total"
|
||||
},
|
||||
"client_picker": {
|
||||
"label": "Kunde *",
|
||||
"placeholder": "Name tippen oder aus Kontakten wählen",
|
||||
"suggest_contact": "aus Kontakten",
|
||||
"suggest_invoice": "aus Rechnungen",
|
||||
"fallback_unnamed": "Unbenannter Kontakt",
|
||||
"label_street": "Strasse + Nr.",
|
||||
"label_zip": "PLZ",
|
||||
"label_city": "Ort",
|
||||
"label_country": "Land",
|
||||
"label_email": "E-Mail",
|
||||
"label_vat": "USt-IdNr. / MwSt-Nr.",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zürich",
|
||||
"placeholder_country": "CH",
|
||||
"placeholder_email": "kontakt@kunde.ch",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST"
|
||||
},
|
||||
"lines_editor": {
|
||||
"th_position": "Position",
|
||||
"th_quantity": "Menge",
|
||||
"th_unit": "Einheit",
|
||||
"th_unit_price": "Einzelpreis",
|
||||
"th_vat": "MwSt.",
|
||||
"th_total": "Total",
|
||||
"empty": "Noch keine Positionen. Füge die erste hinzu.",
|
||||
"placeholder_title": "Titel der Position",
|
||||
"placeholder_unit": "Std",
|
||||
"add_line": "+ Position",
|
||||
"aria_move_up": "Nach oben",
|
||||
"aria_move_down": "Nach unten",
|
||||
"aria_remove": "Entfernen",
|
||||
"label_total_net": "netto {amount}"
|
||||
},
|
||||
"send_modal": {
|
||||
"title": "Rechnung {number} versenden",
|
||||
"close": "Schließen",
|
||||
"intro_pre": "Die PDF wird heruntergeladen und dein Mail-Programm öffnet sich mit vorausgefüllten Feldern. ",
|
||||
"intro_strong": "Die PDF musst du manuell anhängen",
|
||||
"intro_post": " — Mana kann Anhänge (noch) nicht automatisch einbetten.",
|
||||
"label_to": "An",
|
||||
"placeholder_to": "kontakt@kunde.ch",
|
||||
"invalid_email_warn": "Keine gültige E-Mail — du kannst sie gleich im Mail-Programm ergänzen.",
|
||||
"label_subject": "Betreff",
|
||||
"label_body": "Nachricht",
|
||||
"cancel": "Abbrechen",
|
||||
"open_and_download": "Öffnen & herunterladen",
|
||||
"handoff_pdf": "PDF wurde heruntergeladen",
|
||||
"handoff_mail": "Mail-Programm wurde geöffnet",
|
||||
"handoff_hint": "Hänge die heruntergeladene PDF an und sende die Mail. Kehre danach hierher zurück und markiere die Rechnung als versendet.",
|
||||
"later": "Später",
|
||||
"confirming_sent": "Markiere …",
|
||||
"confirm_sent": "Rechnung wurde versendet",
|
||||
"err_open_failed": "Öffnen fehlgeschlagen",
|
||||
"err_mark_failed": "Markieren fehlgeschlagen"
|
||||
},
|
||||
"sender_form": {
|
||||
"loading": "Lade Einstellungen …",
|
||||
"section_sender": "Absender",
|
||||
"section_sender_hint": "Erscheint im Kopf jeder Rechnung.",
|
||||
"section_numbering": "Nummernkreis",
|
||||
"section_numbering_hint_pre": "Nächste Rechnung: ",
|
||||
"section_defaults": "Standards",
|
||||
"label_logo": "Logo",
|
||||
"logo_alt": "Logo-Vorschau",
|
||||
"logo_replace": "Ersetzen",
|
||||
"logo_remove": "Entfernen",
|
||||
"logo_uploading": "Lädt hoch …",
|
||||
"logo_drop": "+ Logo hochladen",
|
||||
"err_upload": "Upload fehlgeschlagen",
|
||||
"err_remove": "Entfernen fehlgeschlagen",
|
||||
"label_name": "Name *",
|
||||
"label_street": "Strasse + Nr. *",
|
||||
"label_zip": "PLZ *",
|
||||
"label_city": "Ort *",
|
||||
"label_country": "Land",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zürich",
|
||||
"placeholder_country": "CH",
|
||||
"legacy_address_summary": "Abweichende Adresse im PDF anzeigen (Freitext-Fallback)",
|
||||
"legacy_address_hint": "Wird nur verwendet, wenn die strukturierten Felder oben leer sind. Nützlich für Postfächer / c/o-Adressen, die nicht ins Strasse+PLZ+Ort-Schema passen.",
|
||||
"legacy_address_placeholder": "Postfach 123\n8021 Zürich",
|
||||
"label_email": "E-Mail *",
|
||||
"label_vat": "MwSt-Nummer",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST",
|
||||
"label_iban": "IBAN *",
|
||||
"placeholder_iban": "CH93 0076 2011 6238 5295 7",
|
||||
"label_bic": "BIC",
|
||||
"label_footer": "Fußzeile",
|
||||
"placeholder_footer": "Ergänzung unter jeder Rechnung (z.B. rechtliche Hinweise)",
|
||||
"label_prefix": "Präfix",
|
||||
"label_padding": "Stellen",
|
||||
"label_next_number": "Nächste Nummer",
|
||||
"label_currency": "Währung",
|
||||
"label_vat_rate": "MwSt.-Satz",
|
||||
"label_due_days": "Zahlungsfrist (Tage)",
|
||||
"label_terms": "Standard-AGB / Zahlungsbedingungen",
|
||||
"placeholder_terms": "Zahlbar innert 30 Tagen netto.",
|
||||
"saving": "Speichert …",
|
||||
"save": "Speichern",
|
||||
"saved_at": "Gespeichert um {time}"
|
||||
},
|
||||
"vat_ch": {
|
||||
"v0": "0% (ausgenommen)",
|
||||
"v2_6": "2.6% (reduziert)",
|
||||
"v3_8": "3.8% (Beherbergung)",
|
||||
"v8_1": "8.1% (Normalsatz)"
|
||||
},
|
||||
"vat_de": {
|
||||
"v0": "0%",
|
||||
"v7": "7% (ermäßigt)",
|
||||
"v19": "19% (Regelsatz)"
|
||||
},
|
||||
"widget": {
|
||||
"title": "Rechnungen",
|
||||
"all_link": "Alle →",
|
||||
"empty": "Noch keine Rechnungen gestellt.",
|
||||
"create_first": "Erste Rechnung",
|
||||
"open": "Offen",
|
||||
"overdue": "Überfällig",
|
||||
"overdue_count": "{count} Rechnungen",
|
||||
"days_overdue": "{n} Tage überfällig"
|
||||
},
|
||||
"routes": {
|
||||
"detail_title_fallback": "Rechnung",
|
||||
"detail_doc_title": "{number} - Mana",
|
||||
"detail_route_title": "Rechnung",
|
||||
"not_found": "Rechnung nicht gefunden.",
|
||||
"back_to_list": "Zurück zur Übersicht",
|
||||
"loading": "Lädt …",
|
||||
"edit_doc_title": "Rechnung bearbeiten - Mana",
|
||||
"edit_heading": "Rechnung {number} bearbeiten",
|
||||
"not_editable_title": "Rechnung kann nicht bearbeitet werden",
|
||||
"not_editable_body_pre": "Nur Entwürfe sind editierbar. Diese Rechnung hat Status ",
|
||||
"not_editable_body_post": ". Um eine versendete Rechnung zu ändern, storniere sie und dupliziere sie als neuen Entwurf.",
|
||||
"back_to_detail": "Zurück zum Detail",
|
||||
"new_doc_title": "Neue Rechnung - Mana",
|
||||
"new_heading": "Neue Rechnung",
|
||||
"new_subtitle": "Entwurf erstellen — wird nach dem Speichern noch nicht versendet.",
|
||||
"settings_doc_title": "Rechnungs-Einstellungen — Mana",
|
||||
"settings_heading": "Rechnungs-Einstellungen",
|
||||
"settings_sub": "Absender, Nummernkreis und Standards"
|
||||
}
|
||||
}
|
||||
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/en.json
Normal file
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/en.json
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"list": {
|
||||
"title": "Invoices",
|
||||
"subtitle": "Issue invoices and track payments",
|
||||
"new_invoice": "+ New invoice",
|
||||
"settings_aria": "Settings",
|
||||
"stat_open": "Open",
|
||||
"stat_overdue": "Overdue",
|
||||
"stat_invoiced_ytd": "Invoiced {year}",
|
||||
"stat_paid_ytd": "Paid {year}",
|
||||
"stat_count": "{count} invoices",
|
||||
"chip_all": "All",
|
||||
"search_placeholder": "Search (number, client, subject)",
|
||||
"empty_title": "No invoices yet",
|
||||
"empty_body": "Issue your first invoice — including PDF export and Swiss QR-Bill (M5).",
|
||||
"empty_cta": "Create first invoice",
|
||||
"empty_filtered": "No invoices found."
|
||||
},
|
||||
"status": {
|
||||
"draft": "Draft",
|
||||
"sent": "Sent",
|
||||
"paid": "Paid",
|
||||
"overdue": "Overdue",
|
||||
"void": "Void"
|
||||
},
|
||||
"detail": {
|
||||
"title_default": "Invoice",
|
||||
"due": "Due {date}",
|
||||
"edit": "Edit",
|
||||
"send_via_mail": "Send via email",
|
||||
"mark_sent": "Mark as sent",
|
||||
"mark_paid": "Mark as paid",
|
||||
"download_pdf": "Download PDF",
|
||||
"duplicate": "Duplicate",
|
||||
"cancel": "Void",
|
||||
"delete": "Delete",
|
||||
"confirm_void": "Void this invoice?",
|
||||
"confirm_delete": "Permanently delete invoice?",
|
||||
"action_mark_sent": "Mark as sent",
|
||||
"action_mark_paid": "Mark as paid",
|
||||
"action_void": "Void",
|
||||
"action_duplicate": "Duplicate",
|
||||
"action_delete": "Delete",
|
||||
"err_action_failed": "{label} failed",
|
||||
"err_pdf_render": "PDF rendering failed",
|
||||
"err_download": "Download failed",
|
||||
"err_pdf": "PDF error: {error}",
|
||||
"preview_title": "Preview",
|
||||
"preview_rendering": "Rendering …",
|
||||
"preview_iframe_title": "Preview invoice {number}",
|
||||
"qr_warning_strong": "QR-Bill not included:",
|
||||
"qr_open_settings": "Open settings →",
|
||||
"raw_summary": "Show structured data",
|
||||
"section_recipient": "Recipient",
|
||||
"section_lines": "Lines",
|
||||
"section_totals": "Totals",
|
||||
"section_notes": "Notes",
|
||||
"section_terms": "Payment terms",
|
||||
"label_vat_number": "VAT no.: {number}",
|
||||
"th_position": "Item",
|
||||
"th_quantity": "Quantity",
|
||||
"th_unit_price": "Unit price",
|
||||
"th_vat": "VAT",
|
||||
"th_net": "Net",
|
||||
"total_label_net": "Net",
|
||||
"total_label_vat": "VAT {rate}%",
|
||||
"total_label_total": "Total",
|
||||
"meta_status": "Status: {label}",
|
||||
"meta_sent": "Sent: {date}",
|
||||
"meta_paid": "Paid: {date}"
|
||||
},
|
||||
"form": {
|
||||
"section_client": "Client",
|
||||
"section_invoice": "Invoice",
|
||||
"section_lines": "Lines",
|
||||
"section_totals": "Totals",
|
||||
"section_notes": "Notes & payment terms",
|
||||
"label_subject": "Subject",
|
||||
"placeholder_subject": "Consulting April",
|
||||
"label_currency": "Currency",
|
||||
"label_issue_date": "Issue date",
|
||||
"label_due_date": "Due date",
|
||||
"label_notes": "Notes (below the lines)",
|
||||
"label_terms": "Payment terms / T&C",
|
||||
"cancel": "Cancel",
|
||||
"saving": "Saving …",
|
||||
"submit_create": "Save as draft",
|
||||
"submit_update": "Save changes",
|
||||
"err_client_required": "Client is required",
|
||||
"err_min_one_line": "Add at least one line",
|
||||
"err_save": "Save failed",
|
||||
"total_label_net": "Net",
|
||||
"total_label_vat": "VAT {rate}%",
|
||||
"total_label_total": "Total"
|
||||
},
|
||||
"client_picker": {
|
||||
"label": "Client *",
|
||||
"placeholder": "Type a name or pick from contacts",
|
||||
"suggest_contact": "from contacts",
|
||||
"suggest_invoice": "from invoices",
|
||||
"fallback_unnamed": "Unnamed contact",
|
||||
"label_street": "Street + No.",
|
||||
"label_zip": "ZIP",
|
||||
"label_city": "City",
|
||||
"label_country": "Country",
|
||||
"label_email": "Email",
|
||||
"label_vat": "VAT ID / VAT no.",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zurich",
|
||||
"placeholder_country": "CH",
|
||||
"placeholder_email": "contact@client.com",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST"
|
||||
},
|
||||
"lines_editor": {
|
||||
"th_position": "Item",
|
||||
"th_quantity": "Quantity",
|
||||
"th_unit": "Unit",
|
||||
"th_unit_price": "Unit price",
|
||||
"th_vat": "VAT",
|
||||
"th_total": "Total",
|
||||
"empty": "No lines yet. Add the first one.",
|
||||
"placeholder_title": "Line title",
|
||||
"placeholder_unit": "hr",
|
||||
"add_line": "+ Line",
|
||||
"aria_move_up": "Move up",
|
||||
"aria_move_down": "Move down",
|
||||
"aria_remove": "Remove",
|
||||
"label_total_net": "net {amount}"
|
||||
},
|
||||
"send_modal": {
|
||||
"title": "Send invoice {number}",
|
||||
"close": "Close",
|
||||
"intro_pre": "The PDF will download and your mail client will open with prefilled fields. ",
|
||||
"intro_strong": "You need to attach the PDF manually",
|
||||
"intro_post": " — Mana can't auto-attach (yet).",
|
||||
"label_to": "To",
|
||||
"placeholder_to": "contact@client.com",
|
||||
"invalid_email_warn": "Not a valid email — you can fix it in your mail client.",
|
||||
"label_subject": "Subject",
|
||||
"label_body": "Message",
|
||||
"cancel": "Cancel",
|
||||
"open_and_download": "Open & download",
|
||||
"handoff_pdf": "PDF was downloaded",
|
||||
"handoff_mail": "Mail client opened",
|
||||
"handoff_hint": "Attach the downloaded PDF and send the mail. Then come back here and mark the invoice as sent.",
|
||||
"later": "Later",
|
||||
"confirming_sent": "Marking …",
|
||||
"confirm_sent": "Invoice was sent",
|
||||
"err_open_failed": "Open failed",
|
||||
"err_mark_failed": "Marking failed"
|
||||
},
|
||||
"sender_form": {
|
||||
"loading": "Loading settings …",
|
||||
"section_sender": "Sender",
|
||||
"section_sender_hint": "Appears in the header of every invoice.",
|
||||
"section_numbering": "Number range",
|
||||
"section_numbering_hint_pre": "Next invoice: ",
|
||||
"section_defaults": "Defaults",
|
||||
"label_logo": "Logo",
|
||||
"logo_alt": "Logo preview",
|
||||
"logo_replace": "Replace",
|
||||
"logo_remove": "Remove",
|
||||
"logo_uploading": "Uploading …",
|
||||
"logo_drop": "+ Upload logo",
|
||||
"err_upload": "Upload failed",
|
||||
"err_remove": "Remove failed",
|
||||
"label_name": "Name *",
|
||||
"label_street": "Street + No. *",
|
||||
"label_zip": "ZIP *",
|
||||
"label_city": "City *",
|
||||
"label_country": "Country",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zurich",
|
||||
"placeholder_country": "CH",
|
||||
"legacy_address_summary": "Show alternate address in PDF (free-text fallback)",
|
||||
"legacy_address_hint": "Used only when the structured fields above are empty. Useful for PO boxes / c/o addresses that don't fit the street+ZIP+city schema.",
|
||||
"legacy_address_placeholder": "PO Box 123\n8021 Zurich",
|
||||
"label_email": "Email *",
|
||||
"label_vat": "VAT number",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST",
|
||||
"label_iban": "IBAN *",
|
||||
"placeholder_iban": "CH93 0076 2011 6238 5295 7",
|
||||
"label_bic": "BIC",
|
||||
"label_footer": "Footer",
|
||||
"placeholder_footer": "Note below every invoice (e.g. legal notes)",
|
||||
"label_prefix": "Prefix",
|
||||
"label_padding": "Digits",
|
||||
"label_next_number": "Next number",
|
||||
"label_currency": "Currency",
|
||||
"label_vat_rate": "VAT rate",
|
||||
"label_due_days": "Payment term (days)",
|
||||
"label_terms": "Default T&C / payment terms",
|
||||
"placeholder_terms": "Payable within 30 days net.",
|
||||
"saving": "Saving …",
|
||||
"save": "Save",
|
||||
"saved_at": "Saved at {time}"
|
||||
},
|
||||
"vat_ch": {
|
||||
"v0": "0% (exempt)",
|
||||
"v2_6": "2.6% (reduced)",
|
||||
"v3_8": "3.8% (lodging)",
|
||||
"v8_1": "8.1% (standard)"
|
||||
},
|
||||
"vat_de": {
|
||||
"v0": "0%",
|
||||
"v7": "7% (reduced)",
|
||||
"v19": "19% (standard)"
|
||||
},
|
||||
"widget": {
|
||||
"title": "Invoices",
|
||||
"all_link": "All →",
|
||||
"empty": "No invoices issued yet.",
|
||||
"create_first": "First invoice",
|
||||
"open": "Open",
|
||||
"overdue": "Overdue",
|
||||
"overdue_count": "{count} invoices",
|
||||
"days_overdue": "{n} days overdue"
|
||||
},
|
||||
"routes": {
|
||||
"detail_title_fallback": "Invoice",
|
||||
"detail_doc_title": "{number} - Mana",
|
||||
"detail_route_title": "Invoice",
|
||||
"not_found": "Invoice not found.",
|
||||
"back_to_list": "Back to overview",
|
||||
"loading": "Loading …",
|
||||
"edit_doc_title": "Edit invoice - Mana",
|
||||
"edit_heading": "Edit invoice {number}",
|
||||
"not_editable_title": "Invoice can't be edited",
|
||||
"not_editable_body_pre": "Only drafts are editable. This invoice has status ",
|
||||
"not_editable_body_post": ". To change a sent invoice, void it and duplicate it as a new draft.",
|
||||
"back_to_detail": "Back to detail",
|
||||
"new_doc_title": "New invoice - Mana",
|
||||
"new_heading": "New invoice",
|
||||
"new_subtitle": "Create a draft — won't be sent until you save.",
|
||||
"settings_doc_title": "Invoice settings — Mana",
|
||||
"settings_heading": "Invoice settings",
|
||||
"settings_sub": "Sender, number range and defaults"
|
||||
}
|
||||
}
|
||||
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/es.json
Normal file
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/es.json
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"list": {
|
||||
"title": "Facturas",
|
||||
"subtitle": "Emite facturas y haz seguimiento de los pagos",
|
||||
"new_invoice": "+ Nueva factura",
|
||||
"settings_aria": "Ajustes",
|
||||
"stat_open": "Pendiente",
|
||||
"stat_overdue": "Vencida",
|
||||
"stat_invoiced_ytd": "Facturado {year}",
|
||||
"stat_paid_ytd": "Pagado {year}",
|
||||
"stat_count": "{count} facturas",
|
||||
"chip_all": "Todas",
|
||||
"search_placeholder": "Buscar (número, cliente, asunto)",
|
||||
"empty_title": "Aún no hay facturas",
|
||||
"empty_body": "Emite tu primera factura — con exportación PDF y QR-Bill suizo (M5).",
|
||||
"empty_cta": "Crear primera factura",
|
||||
"empty_filtered": "No se han encontrado facturas."
|
||||
},
|
||||
"status": {
|
||||
"draft": "Borrador",
|
||||
"sent": "Enviada",
|
||||
"paid": "Pagada",
|
||||
"overdue": "Vencida",
|
||||
"void": "Anulada"
|
||||
},
|
||||
"detail": {
|
||||
"title_default": "Factura",
|
||||
"due": "Vence {date}",
|
||||
"edit": "Editar",
|
||||
"send_via_mail": "Enviar por correo",
|
||||
"mark_sent": "Marcar como enviada",
|
||||
"mark_paid": "Marcar como pagada",
|
||||
"download_pdf": "Descargar PDF",
|
||||
"duplicate": "Duplicar",
|
||||
"cancel": "Anular",
|
||||
"delete": "Eliminar",
|
||||
"confirm_void": "¿Anular esta factura?",
|
||||
"confirm_delete": "¿Eliminar la factura definitivamente?",
|
||||
"action_mark_sent": "Marcar como enviada",
|
||||
"action_mark_paid": "Marcar como pagada",
|
||||
"action_void": "Anular",
|
||||
"action_duplicate": "Duplicar",
|
||||
"action_delete": "Eliminar",
|
||||
"err_action_failed": "{label} ha fallado",
|
||||
"err_pdf_render": "Error al renderizar el PDF",
|
||||
"err_download": "Error al descargar",
|
||||
"err_pdf": "Error de PDF: {error}",
|
||||
"preview_title": "Vista previa",
|
||||
"preview_rendering": "Renderizando …",
|
||||
"preview_iframe_title": "Vista previa factura {number}",
|
||||
"qr_warning_strong": "QR-Bill no incluido:",
|
||||
"qr_open_settings": "Abrir ajustes →",
|
||||
"raw_summary": "Mostrar datos estructurados",
|
||||
"section_recipient": "Destinatario",
|
||||
"section_lines": "Líneas",
|
||||
"section_totals": "Totales",
|
||||
"section_notes": "Notas",
|
||||
"section_terms": "Condiciones de pago",
|
||||
"label_vat_number": "N.º IVA: {number}",
|
||||
"th_position": "Concepto",
|
||||
"th_quantity": "Cantidad",
|
||||
"th_unit_price": "Precio unit.",
|
||||
"th_vat": "IVA",
|
||||
"th_net": "Neto",
|
||||
"total_label_net": "Neto",
|
||||
"total_label_vat": "IVA {rate}%",
|
||||
"total_label_total": "Total",
|
||||
"meta_status": "Estado: {label}",
|
||||
"meta_sent": "Enviada: {date}",
|
||||
"meta_paid": "Pagada: {date}"
|
||||
},
|
||||
"form": {
|
||||
"section_client": "Cliente",
|
||||
"section_invoice": "Factura",
|
||||
"section_lines": "Líneas",
|
||||
"section_totals": "Totales",
|
||||
"section_notes": "Notas y condiciones de pago",
|
||||
"label_subject": "Asunto",
|
||||
"placeholder_subject": "Consultoría abril",
|
||||
"label_currency": "Moneda",
|
||||
"label_issue_date": "Fecha de emisión",
|
||||
"label_due_date": "Vencimiento",
|
||||
"label_notes": "Notas (debajo de las líneas)",
|
||||
"label_terms": "Condiciones / T&C",
|
||||
"cancel": "Cancelar",
|
||||
"saving": "Guardando …",
|
||||
"submit_create": "Guardar como borrador",
|
||||
"submit_update": "Guardar cambios",
|
||||
"err_client_required": "El cliente es obligatorio",
|
||||
"err_min_one_line": "Añade al menos una línea",
|
||||
"err_save": "Error al guardar",
|
||||
"total_label_net": "Neto",
|
||||
"total_label_vat": "IVA {rate}%",
|
||||
"total_label_total": "Total"
|
||||
},
|
||||
"client_picker": {
|
||||
"label": "Cliente *",
|
||||
"placeholder": "Escribe un nombre o elige de los contactos",
|
||||
"suggest_contact": "de contactos",
|
||||
"suggest_invoice": "de facturas",
|
||||
"fallback_unnamed": "Contacto sin nombre",
|
||||
"label_street": "Calle + n.º",
|
||||
"label_zip": "CP",
|
||||
"label_city": "Ciudad",
|
||||
"label_country": "País",
|
||||
"label_email": "Correo electrónico",
|
||||
"label_vat": "NIF / N.º IVA",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zúrich",
|
||||
"placeholder_country": "CH",
|
||||
"placeholder_email": "contacto@cliente.com",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST"
|
||||
},
|
||||
"lines_editor": {
|
||||
"th_position": "Concepto",
|
||||
"th_quantity": "Cantidad",
|
||||
"th_unit": "Unidad",
|
||||
"th_unit_price": "Precio unit.",
|
||||
"th_vat": "IVA",
|
||||
"th_total": "Total",
|
||||
"empty": "Aún no hay líneas. Añade la primera.",
|
||||
"placeholder_title": "Título de la línea",
|
||||
"placeholder_unit": "h",
|
||||
"add_line": "+ Línea",
|
||||
"aria_move_up": "Mover arriba",
|
||||
"aria_move_down": "Mover abajo",
|
||||
"aria_remove": "Quitar",
|
||||
"label_total_net": "neto {amount}"
|
||||
},
|
||||
"send_modal": {
|
||||
"title": "Enviar factura {number}",
|
||||
"close": "Cerrar",
|
||||
"intro_pre": "El PDF se descargará y tu cliente de correo se abrirá con los campos rellenados. ",
|
||||
"intro_strong": "Tienes que adjuntar el PDF manualmente",
|
||||
"intro_post": " — Mana aún no puede adjuntar automáticamente.",
|
||||
"label_to": "Para",
|
||||
"placeholder_to": "contacto@cliente.com",
|
||||
"invalid_email_warn": "No es un correo válido — puedes corregirlo en tu cliente de correo.",
|
||||
"label_subject": "Asunto",
|
||||
"label_body": "Mensaje",
|
||||
"cancel": "Cancelar",
|
||||
"open_and_download": "Abrir y descargar",
|
||||
"handoff_pdf": "El PDF se ha descargado",
|
||||
"handoff_mail": "Cliente de correo abierto",
|
||||
"handoff_hint": "Adjunta el PDF descargado y envía el correo. Luego vuelve aquí y marca la factura como enviada.",
|
||||
"later": "Más tarde",
|
||||
"confirming_sent": "Marcando …",
|
||||
"confirm_sent": "La factura se ha enviado",
|
||||
"err_open_failed": "Error al abrir",
|
||||
"err_mark_failed": "Error al marcar"
|
||||
},
|
||||
"sender_form": {
|
||||
"loading": "Cargando ajustes …",
|
||||
"section_sender": "Emisor",
|
||||
"section_sender_hint": "Aparece en el encabezado de cada factura.",
|
||||
"section_numbering": "Rango de numeración",
|
||||
"section_numbering_hint_pre": "Próxima factura: ",
|
||||
"section_defaults": "Valores predeterminados",
|
||||
"label_logo": "Logo",
|
||||
"logo_alt": "Vista previa del logo",
|
||||
"logo_replace": "Reemplazar",
|
||||
"logo_remove": "Quitar",
|
||||
"logo_uploading": "Subiendo …",
|
||||
"logo_drop": "+ Subir logo",
|
||||
"err_upload": "Error al subir",
|
||||
"err_remove": "Error al quitar",
|
||||
"label_name": "Nombre *",
|
||||
"label_street": "Calle + n.º *",
|
||||
"label_zip": "CP *",
|
||||
"label_city": "Ciudad *",
|
||||
"label_country": "País",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zúrich",
|
||||
"placeholder_country": "CH",
|
||||
"legacy_address_summary": "Mostrar dirección alternativa en el PDF (texto libre)",
|
||||
"legacy_address_hint": "Solo se usa cuando los campos estructurados de arriba están vacíos. Útil para apartados postales / direcciones c/o que no encajan en el esquema calle+CP+ciudad.",
|
||||
"legacy_address_placeholder": "Apdo. 123\n8021 Zúrich",
|
||||
"label_email": "Correo electrónico *",
|
||||
"label_vat": "Número de IVA",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST",
|
||||
"label_iban": "IBAN *",
|
||||
"placeholder_iban": "CH93 0076 2011 6238 5295 7",
|
||||
"label_bic": "BIC",
|
||||
"label_footer": "Pie de página",
|
||||
"placeholder_footer": "Nota debajo de cada factura (p. ej. avisos legales)",
|
||||
"label_prefix": "Prefijo",
|
||||
"label_padding": "Dígitos",
|
||||
"label_next_number": "Próximo número",
|
||||
"label_currency": "Moneda",
|
||||
"label_vat_rate": "Tipo de IVA",
|
||||
"label_due_days": "Plazo de pago (días)",
|
||||
"label_terms": "T&C / condiciones de pago por defecto",
|
||||
"placeholder_terms": "Pagadera en 30 días neto.",
|
||||
"saving": "Guardando …",
|
||||
"save": "Guardar",
|
||||
"saved_at": "Guardado a las {time}"
|
||||
},
|
||||
"vat_ch": {
|
||||
"v0": "0% (exento)",
|
||||
"v2_6": "2.6% (reducido)",
|
||||
"v3_8": "3.8% (alojamiento)",
|
||||
"v8_1": "8.1% (estándar)"
|
||||
},
|
||||
"vat_de": {
|
||||
"v0": "0%",
|
||||
"v7": "7% (reducido)",
|
||||
"v19": "19% (estándar)"
|
||||
},
|
||||
"widget": {
|
||||
"title": "Facturas",
|
||||
"all_link": "Todas →",
|
||||
"empty": "Aún no se ha emitido ninguna factura.",
|
||||
"create_first": "Primera factura",
|
||||
"open": "Pendiente",
|
||||
"overdue": "Vencida",
|
||||
"overdue_count": "{count} facturas",
|
||||
"days_overdue": "{n} días vencida"
|
||||
},
|
||||
"routes": {
|
||||
"detail_title_fallback": "Factura",
|
||||
"detail_doc_title": "{number} - Mana",
|
||||
"detail_route_title": "Factura",
|
||||
"not_found": "Factura no encontrada.",
|
||||
"back_to_list": "Volver al listado",
|
||||
"loading": "Cargando …",
|
||||
"edit_doc_title": "Editar factura - Mana",
|
||||
"edit_heading": "Editar factura {number}",
|
||||
"not_editable_title": "La factura no se puede editar",
|
||||
"not_editable_body_pre": "Solo los borradores son editables. Esta factura está en estado ",
|
||||
"not_editable_body_post": ". Para cambiar una factura enviada, anúlala y duplícala como nuevo borrador.",
|
||||
"back_to_detail": "Volver al detalle",
|
||||
"new_doc_title": "Nueva factura - Mana",
|
||||
"new_heading": "Nueva factura",
|
||||
"new_subtitle": "Crear borrador — no se enviará hasta guardar.",
|
||||
"settings_doc_title": "Ajustes de facturas — Mana",
|
||||
"settings_heading": "Ajustes de facturas",
|
||||
"settings_sub": "Emisor, rango de numeración y valores por defecto"
|
||||
}
|
||||
}
|
||||
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/fr.json
Normal file
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/fr.json
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"list": {
|
||||
"title": "Factures",
|
||||
"subtitle": "Émets des factures et suis les paiements",
|
||||
"new_invoice": "+ Nouvelle facture",
|
||||
"settings_aria": "Paramètres",
|
||||
"stat_open": "En attente",
|
||||
"stat_overdue": "En retard",
|
||||
"stat_invoiced_ytd": "Facturé {year}",
|
||||
"stat_paid_ytd": "Payé {year}",
|
||||
"stat_count": "{count} factures",
|
||||
"chip_all": "Toutes",
|
||||
"search_placeholder": "Rechercher (numéro, client, objet)",
|
||||
"empty_title": "Pas encore de facture",
|
||||
"empty_body": "Émets ta première facture — avec export PDF et QR-Bill suisse (M5).",
|
||||
"empty_cta": "Créer la première facture",
|
||||
"empty_filtered": "Aucune facture trouvée."
|
||||
},
|
||||
"status": {
|
||||
"draft": "Brouillon",
|
||||
"sent": "Envoyée",
|
||||
"paid": "Payée",
|
||||
"overdue": "En retard",
|
||||
"void": "Annulée"
|
||||
},
|
||||
"detail": {
|
||||
"title_default": "Facture",
|
||||
"due": "Échéance {date}",
|
||||
"edit": "Modifier",
|
||||
"send_via_mail": "Envoyer par e-mail",
|
||||
"mark_sent": "Marquer comme envoyée",
|
||||
"mark_paid": "Marquer comme payée",
|
||||
"download_pdf": "Télécharger le PDF",
|
||||
"duplicate": "Dupliquer",
|
||||
"cancel": "Annuler",
|
||||
"delete": "Supprimer",
|
||||
"confirm_void": "Annuler cette facture ?",
|
||||
"confirm_delete": "Supprimer définitivement la facture ?",
|
||||
"action_mark_sent": "Marquer comme envoyée",
|
||||
"action_mark_paid": "Marquer comme payée",
|
||||
"action_void": "Annuler",
|
||||
"action_duplicate": "Dupliquer",
|
||||
"action_delete": "Supprimer",
|
||||
"err_action_failed": "{label} a échoué",
|
||||
"err_pdf_render": "Échec du rendu PDF",
|
||||
"err_download": "Échec du téléchargement",
|
||||
"err_pdf": "Erreur PDF : {error}",
|
||||
"preview_title": "Aperçu",
|
||||
"preview_rendering": "Rendu …",
|
||||
"preview_iframe_title": "Aperçu facture {number}",
|
||||
"qr_warning_strong": "QR-facture non incluse :",
|
||||
"qr_open_settings": "Ouvrir les paramètres →",
|
||||
"raw_summary": "Afficher les données structurées",
|
||||
"section_recipient": "Destinataire",
|
||||
"section_lines": "Lignes",
|
||||
"section_totals": "Totaux",
|
||||
"section_notes": "Notes",
|
||||
"section_terms": "Conditions de paiement",
|
||||
"label_vat_number": "N° TVA : {number}",
|
||||
"th_position": "Désignation",
|
||||
"th_quantity": "Quantité",
|
||||
"th_unit_price": "Prix unit.",
|
||||
"th_vat": "TVA",
|
||||
"th_net": "Net",
|
||||
"total_label_net": "Net",
|
||||
"total_label_vat": "TVA {rate}%",
|
||||
"total_label_total": "Total",
|
||||
"meta_status": "Statut : {label}",
|
||||
"meta_sent": "Envoyée : {date}",
|
||||
"meta_paid": "Payée : {date}"
|
||||
},
|
||||
"form": {
|
||||
"section_client": "Client",
|
||||
"section_invoice": "Facture",
|
||||
"section_lines": "Lignes",
|
||||
"section_totals": "Totaux",
|
||||
"section_notes": "Notes et conditions de paiement",
|
||||
"label_subject": "Objet",
|
||||
"placeholder_subject": "Conseil avril",
|
||||
"label_currency": "Devise",
|
||||
"label_issue_date": "Date d'émission",
|
||||
"label_due_date": "Échéance",
|
||||
"label_notes": "Notes (sous les lignes)",
|
||||
"label_terms": "Conditions / CGV",
|
||||
"cancel": "Annuler",
|
||||
"saving": "Enregistrement …",
|
||||
"submit_create": "Enregistrer comme brouillon",
|
||||
"submit_update": "Enregistrer les modifications",
|
||||
"err_client_required": "Le client est obligatoire",
|
||||
"err_min_one_line": "Ajoute au moins une ligne",
|
||||
"err_save": "Échec de l'enregistrement",
|
||||
"total_label_net": "Net",
|
||||
"total_label_vat": "TVA {rate}%",
|
||||
"total_label_total": "Total"
|
||||
},
|
||||
"client_picker": {
|
||||
"label": "Client *",
|
||||
"placeholder": "Tape un nom ou choisis dans les contacts",
|
||||
"suggest_contact": "des contacts",
|
||||
"suggest_invoice": "des factures",
|
||||
"fallback_unnamed": "Contact sans nom",
|
||||
"label_street": "Rue + n°",
|
||||
"label_zip": "CP",
|
||||
"label_city": "Ville",
|
||||
"label_country": "Pays",
|
||||
"label_email": "E-mail",
|
||||
"label_vat": "N° TVA / TVA intra.",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zurich",
|
||||
"placeholder_country": "CH",
|
||||
"placeholder_email": "contact@client.com",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST"
|
||||
},
|
||||
"lines_editor": {
|
||||
"th_position": "Désignation",
|
||||
"th_quantity": "Quantité",
|
||||
"th_unit": "Unité",
|
||||
"th_unit_price": "Prix unit.",
|
||||
"th_vat": "TVA",
|
||||
"th_total": "Total",
|
||||
"empty": "Pas encore de lignes. Ajoute la première.",
|
||||
"placeholder_title": "Titre de la ligne",
|
||||
"placeholder_unit": "h",
|
||||
"add_line": "+ Ligne",
|
||||
"aria_move_up": "Monter",
|
||||
"aria_move_down": "Descendre",
|
||||
"aria_remove": "Retirer",
|
||||
"label_total_net": "net {amount}"
|
||||
},
|
||||
"send_modal": {
|
||||
"title": "Envoyer facture {number}",
|
||||
"close": "Fermer",
|
||||
"intro_pre": "Le PDF sera téléchargé et ton client mail s'ouvrira avec les champs préremplis. ",
|
||||
"intro_strong": "Tu dois joindre le PDF manuellement",
|
||||
"intro_post": " — Mana ne peut pas (encore) joindre automatiquement.",
|
||||
"label_to": "À",
|
||||
"placeholder_to": "contact@client.com",
|
||||
"invalid_email_warn": "Ce n'est pas un e-mail valide — tu peux le corriger dans ton client mail.",
|
||||
"label_subject": "Objet",
|
||||
"label_body": "Message",
|
||||
"cancel": "Annuler",
|
||||
"open_and_download": "Ouvrir et télécharger",
|
||||
"handoff_pdf": "Le PDF a été téléchargé",
|
||||
"handoff_mail": "Client mail ouvert",
|
||||
"handoff_hint": "Joins le PDF téléchargé et envoie le mail. Reviens ensuite ici et marque la facture comme envoyée.",
|
||||
"later": "Plus tard",
|
||||
"confirming_sent": "Marquage …",
|
||||
"confirm_sent": "La facture a été envoyée",
|
||||
"err_open_failed": "Échec de l'ouverture",
|
||||
"err_mark_failed": "Échec du marquage"
|
||||
},
|
||||
"sender_form": {
|
||||
"loading": "Chargement des paramètres …",
|
||||
"section_sender": "Émetteur",
|
||||
"section_sender_hint": "Apparaît en tête de chaque facture.",
|
||||
"section_numbering": "Numérotation",
|
||||
"section_numbering_hint_pre": "Prochaine facture : ",
|
||||
"section_defaults": "Valeurs par défaut",
|
||||
"label_logo": "Logo",
|
||||
"logo_alt": "Aperçu du logo",
|
||||
"logo_replace": "Remplacer",
|
||||
"logo_remove": "Retirer",
|
||||
"logo_uploading": "Téléversement …",
|
||||
"logo_drop": "+ Téléverser le logo",
|
||||
"err_upload": "Échec du téléversement",
|
||||
"err_remove": "Échec du retrait",
|
||||
"label_name": "Nom *",
|
||||
"label_street": "Rue + n° *",
|
||||
"label_zip": "CP *",
|
||||
"label_city": "Ville *",
|
||||
"label_country": "Pays",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zurich",
|
||||
"placeholder_country": "CH",
|
||||
"legacy_address_summary": "Afficher une adresse différente dans le PDF (champ libre)",
|
||||
"legacy_address_hint": "Utilisé seulement si les champs structurés ci-dessus sont vides. Pratique pour les boîtes postales / adresses c/o qui ne rentrent pas dans le schéma rue+CP+ville.",
|
||||
"legacy_address_placeholder": "Case postale 123\n8021 Zurich",
|
||||
"label_email": "E-mail *",
|
||||
"label_vat": "Numéro de TVA",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST",
|
||||
"label_iban": "IBAN *",
|
||||
"placeholder_iban": "CH93 0076 2011 6238 5295 7",
|
||||
"label_bic": "BIC",
|
||||
"label_footer": "Pied de page",
|
||||
"placeholder_footer": "Mention sous chaque facture (p. ex. mentions légales)",
|
||||
"label_prefix": "Préfixe",
|
||||
"label_padding": "Chiffres",
|
||||
"label_next_number": "Prochain numéro",
|
||||
"label_currency": "Devise",
|
||||
"label_vat_rate": "Taux de TVA",
|
||||
"label_due_days": "Délai de paiement (jours)",
|
||||
"label_terms": "CGV / conditions de paiement par défaut",
|
||||
"placeholder_terms": "Payable sous 30 jours net.",
|
||||
"saving": "Enregistrement …",
|
||||
"save": "Enregistrer",
|
||||
"saved_at": "Enregistré à {time}"
|
||||
},
|
||||
"vat_ch": {
|
||||
"v0": "0% (exonéré)",
|
||||
"v2_6": "2.6% (réduit)",
|
||||
"v3_8": "3.8% (hébergement)",
|
||||
"v8_1": "8.1% (taux normal)"
|
||||
},
|
||||
"vat_de": {
|
||||
"v0": "0%",
|
||||
"v7": "7% (réduit)",
|
||||
"v19": "19% (taux normal)"
|
||||
},
|
||||
"widget": {
|
||||
"title": "Factures",
|
||||
"all_link": "Toutes →",
|
||||
"empty": "Aucune facture émise.",
|
||||
"create_first": "Première facture",
|
||||
"open": "En attente",
|
||||
"overdue": "En retard",
|
||||
"overdue_count": "{count} factures",
|
||||
"days_overdue": "{n} jours de retard"
|
||||
},
|
||||
"routes": {
|
||||
"detail_title_fallback": "Facture",
|
||||
"detail_doc_title": "{number} - Mana",
|
||||
"detail_route_title": "Facture",
|
||||
"not_found": "Facture introuvable.",
|
||||
"back_to_list": "Retour à la liste",
|
||||
"loading": "Chargement …",
|
||||
"edit_doc_title": "Modifier facture - Mana",
|
||||
"edit_heading": "Modifier facture {number}",
|
||||
"not_editable_title": "La facture ne peut pas être modifiée",
|
||||
"not_editable_body_pre": "Seuls les brouillons sont modifiables. Cette facture a le statut ",
|
||||
"not_editable_body_post": ". Pour modifier une facture envoyée, annule-la et duplique-la en nouveau brouillon.",
|
||||
"back_to_detail": "Retour au détail",
|
||||
"new_doc_title": "Nouvelle facture - Mana",
|
||||
"new_heading": "Nouvelle facture",
|
||||
"new_subtitle": "Créer un brouillon — ne sera pas envoyé avant l'enregistrement.",
|
||||
"settings_doc_title": "Paramètres factures — Mana",
|
||||
"settings_heading": "Paramètres factures",
|
||||
"settings_sub": "Émetteur, numérotation et valeurs par défaut"
|
||||
}
|
||||
}
|
||||
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/it.json
Normal file
241
apps/mana/apps/web/src/lib/i18n/locales/invoices/it.json
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
{
|
||||
"list": {
|
||||
"title": "Fatture",
|
||||
"subtitle": "Emetti fatture e tieni traccia dei pagamenti",
|
||||
"new_invoice": "+ Nuova fattura",
|
||||
"settings_aria": "Impostazioni",
|
||||
"stat_open": "Aperte",
|
||||
"stat_overdue": "Scadute",
|
||||
"stat_invoiced_ytd": "Fatturato {year}",
|
||||
"stat_paid_ytd": "Pagato {year}",
|
||||
"stat_count": "{count} fatture",
|
||||
"chip_all": "Tutte",
|
||||
"search_placeholder": "Cerca (numero, cliente, oggetto)",
|
||||
"empty_title": "Nessuna fattura",
|
||||
"empty_body": "Emetti la tua prima fattura — con export PDF e QR-Bill svizzera (M5).",
|
||||
"empty_cta": "Crea prima fattura",
|
||||
"empty_filtered": "Nessuna fattura trovata."
|
||||
},
|
||||
"status": {
|
||||
"draft": "Bozza",
|
||||
"sent": "Inviata",
|
||||
"paid": "Pagata",
|
||||
"overdue": "Scaduta",
|
||||
"void": "Annullata"
|
||||
},
|
||||
"detail": {
|
||||
"title_default": "Fattura",
|
||||
"due": "Scadenza {date}",
|
||||
"edit": "Modifica",
|
||||
"send_via_mail": "Invia per e-mail",
|
||||
"mark_sent": "Segna come inviata",
|
||||
"mark_paid": "Segna come pagata",
|
||||
"download_pdf": "Scarica PDF",
|
||||
"duplicate": "Duplica",
|
||||
"cancel": "Annulla",
|
||||
"delete": "Elimina",
|
||||
"confirm_void": "Annullare questa fattura?",
|
||||
"confirm_delete": "Eliminare definitivamente la fattura?",
|
||||
"action_mark_sent": "Segna come inviata",
|
||||
"action_mark_paid": "Segna come pagata",
|
||||
"action_void": "Annulla",
|
||||
"action_duplicate": "Duplica",
|
||||
"action_delete": "Elimina",
|
||||
"err_action_failed": "{label} non riuscito",
|
||||
"err_pdf_render": "Rendering PDF non riuscito",
|
||||
"err_download": "Download non riuscito",
|
||||
"err_pdf": "Errore PDF: {error}",
|
||||
"preview_title": "Anteprima",
|
||||
"preview_rendering": "Rendering …",
|
||||
"preview_iframe_title": "Anteprima fattura {number}",
|
||||
"qr_warning_strong": "QR-fattura non inclusa:",
|
||||
"qr_open_settings": "Apri impostazioni →",
|
||||
"raw_summary": "Mostra dati strutturati",
|
||||
"section_recipient": "Destinatario",
|
||||
"section_lines": "Righe",
|
||||
"section_totals": "Totali",
|
||||
"section_notes": "Note",
|
||||
"section_terms": "Condizioni di pagamento",
|
||||
"label_vat_number": "N. IVA: {number}",
|
||||
"th_position": "Voce",
|
||||
"th_quantity": "Quantità",
|
||||
"th_unit_price": "Prezzo unit.",
|
||||
"th_vat": "IVA",
|
||||
"th_net": "Netto",
|
||||
"total_label_net": "Netto",
|
||||
"total_label_vat": "IVA {rate}%",
|
||||
"total_label_total": "Totale",
|
||||
"meta_status": "Stato: {label}",
|
||||
"meta_sent": "Inviata: {date}",
|
||||
"meta_paid": "Pagata: {date}"
|
||||
},
|
||||
"form": {
|
||||
"section_client": "Cliente",
|
||||
"section_invoice": "Fattura",
|
||||
"section_lines": "Righe",
|
||||
"section_totals": "Totali",
|
||||
"section_notes": "Note e condizioni di pagamento",
|
||||
"label_subject": "Oggetto",
|
||||
"placeholder_subject": "Consulenza aprile",
|
||||
"label_currency": "Valuta",
|
||||
"label_issue_date": "Data emissione",
|
||||
"label_due_date": "Scadenza",
|
||||
"label_notes": "Note (sotto le righe)",
|
||||
"label_terms": "Condizioni di pagamento / T&C",
|
||||
"cancel": "Annulla",
|
||||
"saving": "Salvataggio …",
|
||||
"submit_create": "Salva come bozza",
|
||||
"submit_update": "Salva modifiche",
|
||||
"err_client_required": "Il cliente è obbligatorio",
|
||||
"err_min_one_line": "Aggiungi almeno una riga",
|
||||
"err_save": "Salvataggio non riuscito",
|
||||
"total_label_net": "Netto",
|
||||
"total_label_vat": "IVA {rate}%",
|
||||
"total_label_total": "Totale"
|
||||
},
|
||||
"client_picker": {
|
||||
"label": "Cliente *",
|
||||
"placeholder": "Digita un nome o scegli dai contatti",
|
||||
"suggest_contact": "dai contatti",
|
||||
"suggest_invoice": "dalle fatture",
|
||||
"fallback_unnamed": "Contatto senza nome",
|
||||
"label_street": "Via + n.",
|
||||
"label_zip": "CAP",
|
||||
"label_city": "Città",
|
||||
"label_country": "Paese",
|
||||
"label_email": "E-mail",
|
||||
"label_vat": "P. IVA / N. IVA",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zurigo",
|
||||
"placeholder_country": "CH",
|
||||
"placeholder_email": "contatto@cliente.com",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST"
|
||||
},
|
||||
"lines_editor": {
|
||||
"th_position": "Voce",
|
||||
"th_quantity": "Quantità",
|
||||
"th_unit": "Unità",
|
||||
"th_unit_price": "Prezzo unit.",
|
||||
"th_vat": "IVA",
|
||||
"th_total": "Totale",
|
||||
"empty": "Ancora nessuna riga. Aggiungi la prima.",
|
||||
"placeholder_title": "Titolo della riga",
|
||||
"placeholder_unit": "h",
|
||||
"add_line": "+ Riga",
|
||||
"aria_move_up": "Sposta su",
|
||||
"aria_move_down": "Sposta giù",
|
||||
"aria_remove": "Rimuovi",
|
||||
"label_total_net": "netto {amount}"
|
||||
},
|
||||
"send_modal": {
|
||||
"title": "Invia fattura {number}",
|
||||
"close": "Chiudi",
|
||||
"intro_pre": "Il PDF verrà scaricato e il client mail si aprirà con i campi precompilati. ",
|
||||
"intro_strong": "Devi allegare il PDF manualmente",
|
||||
"intro_post": " — Mana non può ancora allegarlo automaticamente.",
|
||||
"label_to": "A",
|
||||
"placeholder_to": "contatto@cliente.com",
|
||||
"invalid_email_warn": "Non è un'e-mail valida — puoi correggerla nel client mail.",
|
||||
"label_subject": "Oggetto",
|
||||
"label_body": "Messaggio",
|
||||
"cancel": "Annulla",
|
||||
"open_and_download": "Apri e scarica",
|
||||
"handoff_pdf": "PDF scaricato",
|
||||
"handoff_mail": "Client mail aperto",
|
||||
"handoff_hint": "Allega il PDF scaricato e invia la mail. Poi torna qui e segna la fattura come inviata.",
|
||||
"later": "Più tardi",
|
||||
"confirming_sent": "Marcatura …",
|
||||
"confirm_sent": "La fattura è stata inviata",
|
||||
"err_open_failed": "Apertura non riuscita",
|
||||
"err_mark_failed": "Marcatura non riuscita"
|
||||
},
|
||||
"sender_form": {
|
||||
"loading": "Caricamento impostazioni …",
|
||||
"section_sender": "Mittente",
|
||||
"section_sender_hint": "Compare in testa a ogni fattura.",
|
||||
"section_numbering": "Numerazione",
|
||||
"section_numbering_hint_pre": "Prossima fattura: ",
|
||||
"section_defaults": "Predefiniti",
|
||||
"label_logo": "Logo",
|
||||
"logo_alt": "Anteprima logo",
|
||||
"logo_replace": "Sostituisci",
|
||||
"logo_remove": "Rimuovi",
|
||||
"logo_uploading": "Caricamento …",
|
||||
"logo_drop": "+ Carica logo",
|
||||
"err_upload": "Caricamento non riuscito",
|
||||
"err_remove": "Rimozione non riuscita",
|
||||
"label_name": "Nome *",
|
||||
"label_street": "Via + n. *",
|
||||
"label_zip": "CAP *",
|
||||
"label_city": "Città *",
|
||||
"label_country": "Paese",
|
||||
"placeholder_street": "Bahnhofstrasse 1",
|
||||
"placeholder_zip": "8000",
|
||||
"placeholder_city": "Zurigo",
|
||||
"placeholder_country": "CH",
|
||||
"legacy_address_summary": "Mostra indirizzo alternativo nel PDF (testo libero)",
|
||||
"legacy_address_hint": "Usato solo quando i campi strutturati sopra sono vuoti. Utile per caselle postali / indirizzi c/o che non rientrano nello schema via+CAP+città.",
|
||||
"legacy_address_placeholder": "C.P. 123\n8021 Zurigo",
|
||||
"label_email": "E-mail *",
|
||||
"label_vat": "Numero IVA",
|
||||
"placeholder_vat": "CHE-123.456.789 MWST",
|
||||
"label_iban": "IBAN *",
|
||||
"placeholder_iban": "CH93 0076 2011 6238 5295 7",
|
||||
"label_bic": "BIC",
|
||||
"label_footer": "Piè di pagina",
|
||||
"placeholder_footer": "Nota sotto ogni fattura (es. avvisi legali)",
|
||||
"label_prefix": "Prefisso",
|
||||
"label_padding": "Cifre",
|
||||
"label_next_number": "Prossimo numero",
|
||||
"label_currency": "Valuta",
|
||||
"label_vat_rate": "Aliquota IVA",
|
||||
"label_due_days": "Termine di pagamento (giorni)",
|
||||
"label_terms": "T&C / condizioni di pagamento predefinite",
|
||||
"placeholder_terms": "Pagabile entro 30 giorni netti.",
|
||||
"saving": "Salvataggio …",
|
||||
"save": "Salva",
|
||||
"saved_at": "Salvato alle {time}"
|
||||
},
|
||||
"vat_ch": {
|
||||
"v0": "0% (esente)",
|
||||
"v2_6": "2.6% (ridotta)",
|
||||
"v3_8": "3.8% (alloggio)",
|
||||
"v8_1": "8.1% (standard)"
|
||||
},
|
||||
"vat_de": {
|
||||
"v0": "0%",
|
||||
"v7": "7% (ridotta)",
|
||||
"v19": "19% (standard)"
|
||||
},
|
||||
"widget": {
|
||||
"title": "Fatture",
|
||||
"all_link": "Tutte →",
|
||||
"empty": "Nessuna fattura emessa.",
|
||||
"create_first": "Prima fattura",
|
||||
"open": "Aperte",
|
||||
"overdue": "Scadute",
|
||||
"overdue_count": "{count} fatture",
|
||||
"days_overdue": "{n} giorni di ritardo"
|
||||
},
|
||||
"routes": {
|
||||
"detail_title_fallback": "Fattura",
|
||||
"detail_doc_title": "{number} - Mana",
|
||||
"detail_route_title": "Fattura",
|
||||
"not_found": "Fattura non trovata.",
|
||||
"back_to_list": "Torna all'elenco",
|
||||
"loading": "Caricamento …",
|
||||
"edit_doc_title": "Modifica fattura - Mana",
|
||||
"edit_heading": "Modifica fattura {number}",
|
||||
"not_editable_title": "La fattura non può essere modificata",
|
||||
"not_editable_body_pre": "Solo le bozze sono modificabili. Questa fattura ha stato ",
|
||||
"not_editable_body_post": ". Per modificare una fattura inviata, annullala e duplicala come nuova bozza.",
|
||||
"back_to_detail": "Torna al dettaglio",
|
||||
"new_doc_title": "Nuova fattura - Mana",
|
||||
"new_heading": "Nuova fattura",
|
||||
"new_subtitle": "Crea una bozza — non verrà inviata fino al salvataggio.",
|
||||
"settings_doc_title": "Impostazioni fatture — Mana",
|
||||
"settings_heading": "Impostazioni fatture",
|
||||
"settings_sub": "Mittente, numerazione e predefiniti"
|
||||
}
|
||||
}
|
||||
|
|
@ -4,9 +4,9 @@
|
|||
search + row navigation. FAB → /invoices/new, settings icon → /invoices/settings.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { useAllInvoices, computeStats, formatAmount, searchInvoices } from './queries';
|
||||
import { STATUS_LABELS } from './constants';
|
||||
import StatusBadge from './components/StatusBadge.svelte';
|
||||
import type { Invoice, InvoiceStatus, Currency } from './types';
|
||||
|
||||
|
|
@ -46,48 +46,56 @@
|
|||
<div class="invoices-shell">
|
||||
<header class="head">
|
||||
<div>
|
||||
<h1>Rechnungen</h1>
|
||||
<p class="subtitle">Rechnungen stellen und Zahlungen verfolgen</p>
|
||||
<h1>{$_('invoices.list.title')}</h1>
|
||||
<p class="subtitle">{$_('invoices.list.subtitle')}</p>
|
||||
</div>
|
||||
<div class="head-actions">
|
||||
<button
|
||||
class="btn-icon"
|
||||
type="button"
|
||||
title="Einstellungen"
|
||||
aria-label="Einstellungen"
|
||||
title={$_('invoices.list.settings_aria')}
|
||||
aria-label={$_('invoices.list.settings_aria')}
|
||||
onclick={() => goto('/invoices/settings')}
|
||||
>
|
||||
⚙
|
||||
</button>
|
||||
<button class="btn-primary" type="button" onclick={() => goto('/invoices/new')}>
|
||||
+ Neue Rechnung
|
||||
{$_('invoices.list.new_invoice')}
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="stats">
|
||||
<div class="stat">
|
||||
<div class="stat-label">Offen</div>
|
||||
<div class="stat-label">{$_('invoices.list.stat_open')}</div>
|
||||
<div class="stat-value">{formatAmount(stats.openByCurrency[openCurrency], openCurrency)}</div>
|
||||
<div class="stat-sub">
|
||||
{stats.totalByStatus.sent + stats.totalByStatus.overdue} Rechnungen
|
||||
{$_('invoices.list.stat_count', {
|
||||
values: { count: stats.totalByStatus.sent + stats.totalByStatus.overdue },
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat stat-warn" class:empty={stats.overdueByCurrency[overdueCurrency] === 0}>
|
||||
<div class="stat-label">Überfällig</div>
|
||||
<div class="stat-label">{$_('invoices.list.stat_overdue')}</div>
|
||||
<div class="stat-value">
|
||||
{formatAmount(stats.overdueByCurrency[overdueCurrency], overdueCurrency)}
|
||||
</div>
|
||||
<div class="stat-sub">{stats.totalByStatus.overdue} Rechnungen</div>
|
||||
<div class="stat-sub">
|
||||
{$_('invoices.list.stat_count', { values: { count: stats.totalByStatus.overdue } })}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Fakturiert {currentYear}</div>
|
||||
<div class="stat-label">
|
||||
{$_('invoices.list.stat_invoiced_ytd', { values: { year: currentYear } })}
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
{formatAmount(stats.invoicedYtdByCurrency[ytdCurrency], ytdCurrency)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-label">Bezahlt {currentYear}</div>
|
||||
<div class="stat-label">
|
||||
{$_('invoices.list.stat_paid_ytd', { values: { year: currentYear } })}
|
||||
</div>
|
||||
<div class="stat-value">
|
||||
{formatAmount(stats.paidYtdByCurrency[ytdCurrency], ytdCurrency)}
|
||||
</div>
|
||||
|
|
@ -101,7 +109,7 @@
|
|||
class:active={activeStatus === 'all'}
|
||||
onclick={() => (activeStatus = 'all')}
|
||||
>
|
||||
Alle <span class="count">{invoices.length}</span>
|
||||
{$_('invoices.list.chip_all')} <span class="count">{invoices.length}</span>
|
||||
</button>
|
||||
{#each ['draft', 'sent', 'overdue', 'paid', 'void'] as status (status)}
|
||||
<button
|
||||
|
|
@ -109,7 +117,7 @@
|
|||
class:active={activeStatus === status}
|
||||
onclick={() => (activeStatus = status as InvoiceStatus)}
|
||||
>
|
||||
{STATUS_LABELS[status as InvoiceStatus].de}
|
||||
{$_('invoices.status.' + status)}
|
||||
<span class="count">{stats.totalByStatus[status as InvoiceStatus]}</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -117,7 +125,7 @@
|
|||
<input
|
||||
class="search"
|
||||
type="search"
|
||||
placeholder="Suchen (Nummer, Kunde, Betreff)"
|
||||
placeholder={$_('invoices.list.search_placeholder')}
|
||||
bind:value={searchQuery}
|
||||
/>
|
||||
</section>
|
||||
|
|
@ -125,15 +133,15 @@
|
|||
{#if invoices.length === 0}
|
||||
<div class="empty">
|
||||
<div class="empty-icon">📄</div>
|
||||
<h2>Noch keine Rechnungen</h2>
|
||||
<p>Stelle deine erste Rechnung — inklusive PDF-Export und Schweizer QR-Bill (M5).</p>
|
||||
<h2>{$_('invoices.list.empty_title')}</h2>
|
||||
<p>{$_('invoices.list.empty_body')}</p>
|
||||
<button class="btn-primary" onclick={() => goto('/invoices/new')}>
|
||||
Erste Rechnung erstellen
|
||||
{$_('invoices.list.empty_cta')}
|
||||
</button>
|
||||
</div>
|
||||
{:else if filtered.length === 0}
|
||||
<div class="empty">
|
||||
<p>Keine Rechnungen gefunden.</p>
|
||||
<p>{$_('invoices.list.empty_filtered')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<ul class="list" role="list">
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
downstream store which table (if any) the backing record lives in.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { useAllContacts } from '$lib/modules/contacts/queries';
|
||||
import { useInvoiceClients } from '../queries';
|
||||
import type { InvoiceClientSnapshot, ClientSource } from '../types';
|
||||
|
|
@ -43,7 +44,7 @@
|
|||
.map((c) => ({
|
||||
id: c.id,
|
||||
source: 'contact' as ClientSource,
|
||||
name: c.displayName ?? 'Unbenannter Kontakt',
|
||||
name: c.displayName ?? $_('invoices.client_picker.fallback_unnamed'),
|
||||
email: c.email,
|
||||
// Contacts already have structured fields — map them over directly
|
||||
// so picking a contact populates Strasse/PLZ/Ort without lossy
|
||||
|
|
@ -101,10 +102,10 @@
|
|||
|
||||
<div class="picker">
|
||||
<label class="field">
|
||||
<span class="label">Kunde *</span>
|
||||
<span class="label">{$_('invoices.client_picker.label')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name tippen oder aus Kontakten wählen"
|
||||
placeholder={$_('invoices.client_picker.placeholder')}
|
||||
value={query || snapshot.name}
|
||||
oninput={(e) => setName(e.currentTarget.value)}
|
||||
onfocus={() => (showSuggest = query.length > 0)}
|
||||
|
|
@ -116,7 +117,9 @@
|
|||
<button type="button" class="suggest-row" onclick={() => select(s)}>
|
||||
<span class="suggest-name">{s.name}</span>
|
||||
<span class="suggest-source">
|
||||
{s.source === 'contact' ? 'aus Kontakten' : 'aus Rechnungen'}
|
||||
{s.source === 'contact'
|
||||
? $_('invoices.client_picker.suggest_contact')
|
||||
: $_('invoices.client_picker.suggest_invoice')}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
|
|
@ -126,37 +129,37 @@
|
|||
|
||||
<div class="address-grid">
|
||||
<label class="field street">
|
||||
<span class="label">Strasse + Nr.</span>
|
||||
<span class="label">{$_('invoices.client_picker.label_street')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Bahnhofstrasse 1"
|
||||
placeholder={$_('invoices.client_picker.placeholder_street')}
|
||||
value={snapshot.street ?? ''}
|
||||
oninput={(e) => (snapshot = { ...snapshot, street: e.currentTarget.value || undefined })}
|
||||
/>
|
||||
</label>
|
||||
<label class="field zip">
|
||||
<span class="label">PLZ</span>
|
||||
<span class="label">{$_('invoices.client_picker.label_zip')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="8000"
|
||||
placeholder={$_('invoices.client_picker.placeholder_zip')}
|
||||
value={snapshot.zip ?? ''}
|
||||
oninput={(e) => (snapshot = { ...snapshot, zip: e.currentTarget.value || undefined })}
|
||||
/>
|
||||
</label>
|
||||
<label class="field city">
|
||||
<span class="label">Ort</span>
|
||||
<span class="label">{$_('invoices.client_picker.label_city')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Zürich"
|
||||
placeholder={$_('invoices.client_picker.placeholder_city')}
|
||||
value={snapshot.city ?? ''}
|
||||
oninput={(e) => (snapshot = { ...snapshot, city: e.currentTarget.value || undefined })}
|
||||
/>
|
||||
</label>
|
||||
<label class="field country">
|
||||
<span class="label">Land</span>
|
||||
<span class="label">{$_('invoices.client_picker.label_country')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="CH"
|
||||
placeholder={$_('invoices.client_picker.placeholder_country')}
|
||||
maxlength="2"
|
||||
value={snapshot.country ?? ''}
|
||||
oninput={(e) =>
|
||||
|
|
@ -166,20 +169,20 @@
|
|||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">E-Mail</span>
|
||||
<span class="label">{$_('invoices.client_picker.label_email')}</span>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="kontakt@kunde.ch"
|
||||
placeholder={$_('invoices.client_picker.placeholder_email')}
|
||||
value={snapshot.email ?? ''}
|
||||
oninput={(e) => (snapshot = { ...snapshot, email: e.currentTarget.value || undefined })}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span class="label">USt-IdNr. / MwSt-Nr.</span>
|
||||
<span class="label">{$_('invoices.client_picker.label_vat')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="CHE-123.456.789 MWST"
|
||||
placeholder={$_('invoices.client_picker.placeholder_vat')}
|
||||
value={snapshot.vatNumber ?? ''}
|
||||
oninput={(e) => (snapshot = { ...snapshot, vatNumber: e.currentTarget.value || undefined })}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import ClientPicker from './ClientPicker.svelte';
|
||||
import LinesEditor from './LinesEditor.svelte';
|
||||
|
|
@ -83,11 +84,11 @@
|
|||
async function saveDraft() {
|
||||
error = null;
|
||||
if (!snapshot.name?.trim()) {
|
||||
error = 'Kunde ist erforderlich';
|
||||
error = $_('invoices.form.err_client_required');
|
||||
return;
|
||||
}
|
||||
if (lines.length === 0) {
|
||||
error = 'Mindestens eine Position hinzufügen';
|
||||
error = $_('invoices.form.err_min_one_line');
|
||||
return;
|
||||
}
|
||||
saving = true;
|
||||
|
|
@ -122,7 +123,7 @@
|
|||
goto(`/invoices/${newId}`);
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Speichern fehlgeschlagen';
|
||||
error = e instanceof Error ? e.message : $_('invoices.form.err_save');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
|
|
@ -137,19 +138,23 @@
|
|||
class="form"
|
||||
>
|
||||
<section class="section">
|
||||
<h3>Kunde</h3>
|
||||
<h3>{$_('invoices.form.section_client')}</h3>
|
||||
<ClientPicker bind:clientId bind:clientSource bind:snapshot />
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h3>Rechnung</h3>
|
||||
<h3>{$_('invoices.form.section_invoice')}</h3>
|
||||
<div class="grid-3">
|
||||
<label class="field">
|
||||
<span>Betreff</span>
|
||||
<input type="text" placeholder="Beratungsleistung April" bind:value={subject} />
|
||||
<span>{$_('invoices.form.label_subject')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={$_('invoices.form.placeholder_subject')}
|
||||
bind:value={subject}
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Währung</span>
|
||||
<span>{$_('invoices.form.label_currency')}</span>
|
||||
<select bind:value={currency}>
|
||||
{#each Object.keys(CURRENCIES) as c (c)}
|
||||
<option value={c}>{c}</option>
|
||||
|
|
@ -157,43 +162,43 @@
|
|||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Rechnungsdatum</span>
|
||||
<span>{$_('invoices.form.label_issue_date')}</span>
|
||||
<input type="date" bind:value={issueDate} />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Fällig am</span>
|
||||
<span>{$_('invoices.form.label_due_date')}</span>
|
||||
<input type="date" bind:value={dueDate} />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h3>Positionen</h3>
|
||||
<h3>{$_('invoices.form.section_lines')}</h3>
|
||||
<LinesEditor bind:lines {currency} {vatRegime} />
|
||||
</section>
|
||||
|
||||
<section class="section totals-section">
|
||||
<h3>Summe</h3>
|
||||
<h3>{$_('invoices.form.section_totals')}</h3>
|
||||
<dl class="totals">
|
||||
<dt>Netto</dt>
|
||||
<dt>{$_('invoices.form.total_label_net')}</dt>
|
||||
<dd>{formatAmount(totals.net, currency)}</dd>
|
||||
{#each totals.vatBreakdown as b (b.rate)}
|
||||
<dt>MwSt. {b.rate}%</dt>
|
||||
<dt>{$_('invoices.form.total_label_vat', { values: { rate: b.rate } })}</dt>
|
||||
<dd>{formatAmount(b.tax, currency)}</dd>
|
||||
{/each}
|
||||
<dt class="gross-label">Total</dt>
|
||||
<dt class="gross-label">{$_('invoices.form.total_label_total')}</dt>
|
||||
<dd class="gross-value">{formatAmount(totals.gross, currency)}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h3>Notizen & Zahlungsbedingungen</h3>
|
||||
<h3>{$_('invoices.form.section_notes')}</h3>
|
||||
<label class="field">
|
||||
<span>Notizen (unter den Positionen)</span>
|
||||
<span>{$_('invoices.form.label_notes')}</span>
|
||||
<textarea rows="2" bind:value={notes}></textarea>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Zahlungsbedingungen / AGB</span>
|
||||
<span>{$_('invoices.form.label_terms')}</span>
|
||||
<textarea rows="2" bind:value={terms}></textarea>
|
||||
</label>
|
||||
</section>
|
||||
|
|
@ -203,9 +208,15 @@
|
|||
{/if}
|
||||
|
||||
<div class="actions">
|
||||
<button type="button" class="btn-secondary" onclick={() => history.back()}> Abbrechen </button>
|
||||
<button type="button" class="btn-secondary" onclick={() => history.back()}>
|
||||
{$_('invoices.form.cancel')}
|
||||
</button>
|
||||
<button type="submit" class="btn-primary" disabled={saving}>
|
||||
{saving ? 'Speichert …' : isEdit ? 'Änderungen speichern' : 'Als Entwurf speichern'}
|
||||
{saving
|
||||
? $_('invoices.form.saving')
|
||||
: isEdit
|
||||
? $_('invoices.form.submit_update')
|
||||
: $_('invoices.form.submit_create')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
blur / emit. All in-component math uses minor units.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { VAT_RATES_CH, VAT_RATES_DE, CURRENCIES } from '../constants';
|
||||
import { computeLineTotal } from '../totals';
|
||||
import type { LocalInvoiceLine, Currency } from '../types';
|
||||
|
|
@ -72,17 +73,17 @@
|
|||
|
||||
<div class="editor">
|
||||
<div class="head">
|
||||
<span class="col-title">Position</span>
|
||||
<span class="col-qty">Menge</span>
|
||||
<span class="col-unit">Einheit</span>
|
||||
<span class="col-price">Einzelpreis</span>
|
||||
<span class="col-vat">MwSt.</span>
|
||||
<span class="col-total">Total</span>
|
||||
<span class="col-title">{$_('invoices.lines_editor.th_position')}</span>
|
||||
<span class="col-qty">{$_('invoices.lines_editor.th_quantity')}</span>
|
||||
<span class="col-unit">{$_('invoices.lines_editor.th_unit')}</span>
|
||||
<span class="col-price">{$_('invoices.lines_editor.th_unit_price')}</span>
|
||||
<span class="col-vat">{$_('invoices.lines_editor.th_vat')}</span>
|
||||
<span class="col-total">{$_('invoices.lines_editor.th_total')}</span>
|
||||
<span class="col-actions"></span>
|
||||
</div>
|
||||
|
||||
{#if lines.length === 0}
|
||||
<p class="empty">Noch keine Positionen. Füge die erste hinzu.</p>
|
||||
<p class="empty">{$_('invoices.lines_editor.empty')}</p>
|
||||
{/if}
|
||||
|
||||
{#each lines as line (line.id)}
|
||||
|
|
@ -91,7 +92,7 @@
|
|||
<input
|
||||
class="col-title"
|
||||
type="text"
|
||||
placeholder="Titel der Position"
|
||||
placeholder={$_('invoices.lines_editor.placeholder_title')}
|
||||
value={line.title}
|
||||
oninput={(e) => updateLine(line.id, { title: e.currentTarget.value })}
|
||||
/>
|
||||
|
|
@ -106,7 +107,7 @@
|
|||
<input
|
||||
class="col-unit"
|
||||
type="text"
|
||||
placeholder="Std"
|
||||
placeholder={$_('invoices.lines_editor.placeholder_unit')}
|
||||
value={line.unit ?? ''}
|
||||
oninput={(e) => updateLine(line.id, { unit: e.currentTarget.value || null })}
|
||||
/>
|
||||
|
|
@ -125,38 +126,43 @@
|
|||
onchange={(e) => updateLine(line.id, { vatRate: Number(e.currentTarget.value) })}
|
||||
>
|
||||
{#each vatOptions as option (option.value)}
|
||||
<option value={option.value}>{option.label}</option>
|
||||
<option value={option.value}>{$_(option.i18nKey)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="col-total total-cell">
|
||||
<strong>{formatMinor(net + tax)}</strong>
|
||||
<small>netto {formatMinor(net)}</small>
|
||||
<small
|
||||
>{$_('invoices.lines_editor.label_total_net', {
|
||||
values: { amount: formatMinor(net) },
|
||||
})}</small
|
||||
>
|
||||
</span>
|
||||
<span class="col-actions">
|
||||
<button
|
||||
type="button"
|
||||
title="Nach oben"
|
||||
title={$_('invoices.lines_editor.aria_move_up')}
|
||||
onclick={() => moveLine(line.id, -1)}
|
||||
aria-label="Nach oben">↑</button
|
||||
aria-label={$_('invoices.lines_editor.aria_move_up')}>↑</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
title="Nach unten"
|
||||
title={$_('invoices.lines_editor.aria_move_down')}
|
||||
onclick={() => moveLine(line.id, 1)}
|
||||
aria-label="Nach unten">↓</button
|
||||
aria-label={$_('invoices.lines_editor.aria_move_down')}>↓</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="remove"
|
||||
title="Entfernen"
|
||||
title={$_('invoices.lines_editor.aria_remove')}
|
||||
onclick={() => removeLine(line.id)}
|
||||
aria-label="Entfernen">×</button
|
||||
aria-label={$_('invoices.lines_editor.aria_remove')}>×</button
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<button type="button" class="add" onclick={addLine}>+ Position</button>
|
||||
<button type="button" class="add" onclick={addLine}>{$_('invoices.lines_editor.add_line')}</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { untrack } from 'svelte';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import type { Invoice, InvoiceSettings } from '../types';
|
||||
import { buildInvoiceMailDraft, mailDraftToMailto, looksLikeEmail } from '../mail-template';
|
||||
import { invoicesStore } from '../stores/invoices.svelte';
|
||||
|
|
@ -71,7 +72,7 @@
|
|||
|
||||
phase = 'handed-off';
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Öffnen fehlgeschlagen';
|
||||
error = e instanceof Error ? e.message : $_('invoices.send_modal.err_open_failed');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,7 +85,7 @@
|
|||
onClose();
|
||||
} catch (e) {
|
||||
phase = 'handed-off';
|
||||
error = e instanceof Error ? e.message : 'Markieren fehlgeschlagen';
|
||||
error = e instanceof Error ? e.message : $_('invoices.send_modal.err_mark_failed');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,35 +105,44 @@
|
|||
<div class="backdrop" onclick={onBackdropClick} role="presentation">
|
||||
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="send-title" tabindex="-1">
|
||||
<header class="head">
|
||||
<h2 id="send-title">Rechnung {invoice.number} versenden</h2>
|
||||
<button type="button" class="close" onclick={onClose} aria-label="Schließen">×</button>
|
||||
<h2 id="send-title">
|
||||
{$_('invoices.send_modal.title', { values: { number: invoice.number } })}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="close"
|
||||
onclick={onClose}
|
||||
aria-label={$_('invoices.send_modal.close')}>×</button
|
||||
>
|
||||
</header>
|
||||
|
||||
{#if phase === 'compose'}
|
||||
<div class="body">
|
||||
<p class="intro">
|
||||
Die PDF wird heruntergeladen und dein Mail-Programm öffnet sich mit vorausgefüllten
|
||||
Feldern. <strong>Die PDF musst du manuell anhängen</strong>
|
||||
— Mana kann Anhänge (noch) nicht automatisch einbetten.
|
||||
{$_('invoices.send_modal.intro_pre')}<strong
|
||||
>{$_('invoices.send_modal.intro_strong')}</strong
|
||||
>{$_('invoices.send_modal.intro_post')}
|
||||
</p>
|
||||
|
||||
<label class="field">
|
||||
<span>An</span>
|
||||
<input type="email" bind:value={to} placeholder="kontakt@kunde.ch" />
|
||||
<span>{$_('invoices.send_modal.label_to')}</span>
|
||||
<input
|
||||
type="email"
|
||||
bind:value={to}
|
||||
placeholder={$_('invoices.send_modal.placeholder_to')}
|
||||
/>
|
||||
{#if !hasRecipient}
|
||||
<span class="hint-warn">
|
||||
Keine gültige E-Mail — du kannst sie gleich im Mail-Programm ergänzen.
|
||||
</span>
|
||||
<span class="hint-warn">{$_('invoices.send_modal.invalid_email_warn')}</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Betreff</span>
|
||||
<span>{$_('invoices.send_modal.label_subject')}</span>
|
||||
<input type="text" bind:value={subject} />
|
||||
</label>
|
||||
|
||||
<label class="field">
|
||||
<span>Nachricht</span>
|
||||
<span>{$_('invoices.send_modal.label_body')}</span>
|
||||
<textarea rows="10" bind:value={body}></textarea>
|
||||
</label>
|
||||
|
||||
|
|
@ -140,39 +150,42 @@
|
|||
</div>
|
||||
|
||||
<footer class="foot">
|
||||
<button type="button" class="btn" onclick={onClose}>Abbrechen</button>
|
||||
<button type="button" class="btn" onclick={onClose}
|
||||
>{$_('invoices.send_modal.cancel')}</button
|
||||
>
|
||||
<button type="button" class="btn btn-primary" onclick={openAndDownload}>
|
||||
Öffnen & herunterladen
|
||||
{$_('invoices.send_modal.open_and_download')}
|
||||
</button>
|
||||
</footer>
|
||||
{:else if phase === 'handed-off' || phase === 'marking-sent'}
|
||||
<div class="body handoff">
|
||||
<div class="check-row">
|
||||
<span class="check">✓</span>
|
||||
<span>PDF wurde heruntergeladen</span>
|
||||
<span>{$_('invoices.send_modal.handoff_pdf')}</span>
|
||||
</div>
|
||||
<div class="check-row">
|
||||
<span class="check">✓</span>
|
||||
<span>Mail-Programm wurde geöffnet</span>
|
||||
<span>{$_('invoices.send_modal.handoff_mail')}</span>
|
||||
</div>
|
||||
|
||||
<p class="handoff-hint">
|
||||
Hänge die heruntergeladene PDF an und sende die Mail. Kehre danach hierher zurück und
|
||||
markiere die Rechnung als versendet.
|
||||
</p>
|
||||
<p class="handoff-hint">{$_('invoices.send_modal.handoff_hint')}</p>
|
||||
|
||||
{#if error}<div class="error">{error}</div>{/if}
|
||||
</div>
|
||||
|
||||
<footer class="foot">
|
||||
<button type="button" class="btn" onclick={onClose}>Später</button>
|
||||
<button type="button" class="btn" onclick={onClose}
|
||||
>{$_('invoices.send_modal.later')}</button
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
onclick={markSent}
|
||||
disabled={phase === 'marking-sent'}
|
||||
>
|
||||
{phase === 'marking-sent' ? 'Markiere …' : 'Rechnung wurde versendet'}
|
||||
{phase === 'marking-sent'
|
||||
? $_('invoices.send_modal.confirming_sent')
|
||||
: $_('invoices.send_modal.confirm_sent')}
|
||||
</button>
|
||||
</footer>
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
used on every PDF the user issues. Also carries the number-sequence state.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { invoiceSettingsStore } from '../stores/settings.svelte';
|
||||
import type { InvoiceSettings, Currency } from '../types';
|
||||
import { VAT_RATES_CH, VAT_RATES_DE, CURRENCIES } from '../constants';
|
||||
|
|
@ -29,7 +31,7 @@
|
|||
await invoiceSettingsStore.update({ logoMediaId: mediaId });
|
||||
settings.logoMediaId = mediaId;
|
||||
} catch (e) {
|
||||
logoError = e instanceof Error ? e.message : 'Upload fehlgeschlagen';
|
||||
logoError = e instanceof Error ? e.message : $_('invoices.sender_form.err_upload');
|
||||
} finally {
|
||||
uploadingLogo = false;
|
||||
input.value = '';
|
||||
|
|
@ -44,7 +46,7 @@
|
|||
await invoiceSettingsStore.update({ logoMediaId: null });
|
||||
settings.logoMediaId = null;
|
||||
} catch (e) {
|
||||
logoError = e instanceof Error ? e.message : 'Entfernen fehlgeschlagen';
|
||||
logoError = e instanceof Error ? e.message : $_('invoices.sender_form.err_remove');
|
||||
} finally {
|
||||
uploadingLogo = false;
|
||||
}
|
||||
|
|
@ -80,7 +82,7 @@
|
|||
defaultDueDays: settings.defaultDueDays,
|
||||
defaultTerms: settings.defaultTerms,
|
||||
});
|
||||
savedAt = new Date().toLocaleTimeString();
|
||||
savedAt = new Date().toLocaleTimeString(get(locale) ?? 'de');
|
||||
} finally {
|
||||
saving = false;
|
||||
}
|
||||
|
|
@ -96,7 +98,7 @@
|
|||
</script>
|
||||
|
||||
{#if !settings}
|
||||
<p class="loading">Lade Einstellungen …</p>
|
||||
<p class="loading">{$_('invoices.sender_form.loading')}</p>
|
||||
{:else}
|
||||
<form
|
||||
onsubmit={(e) => {
|
||||
|
|
@ -106,17 +108,17 @@
|
|||
class="form"
|
||||
>
|
||||
<section class="section">
|
||||
<h3>Absender</h3>
|
||||
<p class="hint">Erscheint im Kopf jeder Rechnung.</p>
|
||||
<h3>{$_('invoices.sender_form.section_sender')}</h3>
|
||||
<p class="hint">{$_('invoices.sender_form.section_sender_hint')}</p>
|
||||
|
||||
<div class="field logo-field">
|
||||
<span>Logo</span>
|
||||
<span>{$_('invoices.sender_form.label_logo')}</span>
|
||||
<div class="logo-row">
|
||||
{#if settings.logoMediaId}
|
||||
<img
|
||||
class="logo-preview"
|
||||
src={logoPreviewUrl(settings.logoMediaId)}
|
||||
alt="Logo-Vorschau"
|
||||
alt={$_('invoices.sender_form.logo_alt')}
|
||||
/>
|
||||
<div class="logo-actions">
|
||||
<button
|
||||
|
|
@ -125,7 +127,7 @@
|
|||
onclick={() => logoInput?.click()}
|
||||
disabled={uploadingLogo}
|
||||
>
|
||||
Ersetzen
|
||||
{$_('invoices.sender_form.logo_replace')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -133,7 +135,7 @@
|
|||
onclick={removeLogo}
|
||||
disabled={uploadingLogo}
|
||||
>
|
||||
Entfernen
|
||||
{$_('invoices.sender_form.logo_remove')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -143,7 +145,9 @@
|
|||
onclick={() => logoInput?.click()}
|
||||
disabled={uploadingLogo}
|
||||
>
|
||||
{uploadingLogo ? 'Lädt hoch …' : '+ Logo hochladen'}
|
||||
{uploadingLogo
|
||||
? $_('invoices.sender_form.logo_uploading')
|
||||
: $_('invoices.sender_form.logo_drop')}
|
||||
</button>
|
||||
{/if}
|
||||
<input
|
||||
|
|
@ -160,25 +164,25 @@
|
|||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>Name *</span>
|
||||
<span>{$_('invoices.sender_form.label_name')}</span>
|
||||
<input type="text" bind:value={settings.senderName} required />
|
||||
</label>
|
||||
|
||||
<div class="grid-2">
|
||||
<label class="field">
|
||||
<span>Strasse + Nr. *</span>
|
||||
<span>{$_('invoices.sender_form.label_street')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Bahnhofstrasse 1"
|
||||
placeholder={$_('invoices.sender_form.placeholder_street')}
|
||||
value={settings.senderStreet ?? ''}
|
||||
oninput={(e) => settings && (settings.senderStreet = e.currentTarget.value || null)}
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>PLZ *</span>
|
||||
<span>{$_('invoices.sender_form.label_zip')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="8000"
|
||||
placeholder={$_('invoices.sender_form.placeholder_zip')}
|
||||
value={settings.senderZip ?? ''}
|
||||
oninput={(e) => settings && (settings.senderZip = e.currentTarget.value || null)}
|
||||
/>
|
||||
|
|
@ -187,19 +191,19 @@
|
|||
|
||||
<div class="grid-2">
|
||||
<label class="field">
|
||||
<span>Ort *</span>
|
||||
<span>{$_('invoices.sender_form.label_city')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Zürich"
|
||||
placeholder={$_('invoices.sender_form.placeholder_city')}
|
||||
value={settings.senderCity ?? ''}
|
||||
oninput={(e) => settings && (settings.senderCity = e.currentTarget.value || null)}
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Land</span>
|
||||
<span>{$_('invoices.sender_form.label_country')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="CH"
|
||||
placeholder={$_('invoices.sender_form.placeholder_country')}
|
||||
maxlength="2"
|
||||
value={settings.senderCountry ?? 'CH'}
|
||||
oninput={(e) =>
|
||||
|
|
@ -209,28 +213,25 @@
|
|||
</div>
|
||||
|
||||
<details class="legacy-address">
|
||||
<summary>Abweichende Adresse im PDF anzeigen (Freitext-Fallback)</summary>
|
||||
<p class="hint">
|
||||
Wird nur verwendet, wenn die strukturierten Felder oben leer sind. Nützlich für Postfächer
|
||||
/ c/o-Adressen, die nicht ins Strasse+PLZ+Ort-Schema passen.
|
||||
</p>
|
||||
<summary>{$_('invoices.sender_form.legacy_address_summary')}</summary>
|
||||
<p class="hint">{$_('invoices.sender_form.legacy_address_hint')}</p>
|
||||
<textarea
|
||||
rows="3"
|
||||
placeholder="Postfach 123 8021 Zürich"
|
||||
placeholder={$_('invoices.sender_form.legacy_address_placeholder')}
|
||||
bind:value={settings.senderAddress}
|
||||
></textarea>
|
||||
</details>
|
||||
|
||||
<div class="grid-2">
|
||||
<label class="field">
|
||||
<span>E-Mail *</span>
|
||||
<span>{$_('invoices.sender_form.label_email')}</span>
|
||||
<input type="email" bind:value={settings.senderEmail} required />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>MwSt-Nummer</span>
|
||||
<span>{$_('invoices.sender_form.label_vat')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="CHE-123.456.789 MWST"
|
||||
placeholder={$_('invoices.sender_form.placeholder_vat')}
|
||||
value={settings.senderVatNumber ?? ''}
|
||||
oninput={(e) => settings && (settings.senderVatNumber = e.currentTarget.value || null)}
|
||||
/>
|
||||
|
|
@ -239,16 +240,16 @@
|
|||
|
||||
<div class="grid-2">
|
||||
<label class="field">
|
||||
<span>IBAN *</span>
|
||||
<span>{$_('invoices.sender_form.label_iban')}</span>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="CH93 0076 2011 6238 5295 7"
|
||||
placeholder={$_('invoices.sender_form.placeholder_iban')}
|
||||
bind:value={settings.senderIban}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>BIC</span>
|
||||
<span>{$_('invoices.sender_form.label_bic')}</span>
|
||||
<input
|
||||
type="text"
|
||||
value={settings.senderBic ?? ''}
|
||||
|
|
@ -258,10 +259,10 @@
|
|||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>Fußzeile</span>
|
||||
<span>{$_('invoices.sender_form.label_footer')}</span>
|
||||
<textarea
|
||||
rows="2"
|
||||
placeholder="Ergänzung unter jeder Rechnung (z.B. rechtliche Hinweise)"
|
||||
placeholder={$_('invoices.sender_form.placeholder_footer')}
|
||||
value={settings.footer ?? ''}
|
||||
oninput={(e) => settings && (settings.footer = e.currentTarget.value || null)}
|
||||
></textarea>
|
||||
|
|
@ -269,31 +270,33 @@
|
|||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h3>Nummernkreis</h3>
|
||||
<p class="hint">Nächste Rechnung: <code>{nextPreview}</code></p>
|
||||
<h3>{$_('invoices.sender_form.section_numbering')}</h3>
|
||||
<p class="hint">
|
||||
{$_('invoices.sender_form.section_numbering_hint_pre')}<code>{nextPreview}</code>
|
||||
</p>
|
||||
|
||||
<div class="grid-3">
|
||||
<label class="field">
|
||||
<span>Präfix</span>
|
||||
<span>{$_('invoices.sender_form.label_prefix')}</span>
|
||||
<input type="text" bind:value={settings.numberPrefix} />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Stellen</span>
|
||||
<span>{$_('invoices.sender_form.label_padding')}</span>
|
||||
<input type="number" min="1" max="8" bind:value={settings.numberPadding} />
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Nächste Nummer</span>
|
||||
<span>{$_('invoices.sender_form.label_next_number')}</span>
|
||||
<input type="number" min="1" bind:value={settings.nextNumber} />
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section">
|
||||
<h3>Standards</h3>
|
||||
<h3>{$_('invoices.sender_form.section_defaults')}</h3>
|
||||
|
||||
<div class="grid-3">
|
||||
<label class="field">
|
||||
<span>Währung</span>
|
||||
<span>{$_('invoices.sender_form.label_currency')}</span>
|
||||
<select bind:value={settings.defaultCurrency}>
|
||||
{#each Object.keys(CURRENCIES) as c (c)}
|
||||
<option value={c}>{c}</option>
|
||||
|
|
@ -301,27 +304,27 @@
|
|||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>MwSt.-Satz</span>
|
||||
<span>{$_('invoices.sender_form.label_vat_rate')}</span>
|
||||
<select
|
||||
value={settings.defaultVatRate}
|
||||
onchange={(e) => settings && (settings.defaultVatRate = Number(e.currentTarget.value))}
|
||||
>
|
||||
{#each vatOptions as o (o.value)}
|
||||
<option value={o.value}>{o.label}</option>
|
||||
<option value={o.value}>{$_(o.i18nKey)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<label class="field">
|
||||
<span>Zahlungsfrist (Tage)</span>
|
||||
<span>{$_('invoices.sender_form.label_due_days')}</span>
|
||||
<input type="number" min="0" max="365" bind:value={settings.defaultDueDays} />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="field">
|
||||
<span>Standard-AGB / Zahlungsbedingungen</span>
|
||||
<span>{$_('invoices.sender_form.label_terms')}</span>
|
||||
<textarea
|
||||
rows="2"
|
||||
placeholder="Zahlbar innert 30 Tagen netto."
|
||||
placeholder={$_('invoices.sender_form.placeholder_terms')}
|
||||
value={settings.defaultTerms ?? ''}
|
||||
oninput={(e) => settings && (settings.defaultTerms = e.currentTarget.value || null)}
|
||||
></textarea>
|
||||
|
|
@ -330,10 +333,12 @@
|
|||
|
||||
<div class="actions">
|
||||
<button type="submit" class="btn-primary" disabled={saving}>
|
||||
{saving ? 'Speichert …' : 'Speichern'}
|
||||
{saving ? $_('invoices.sender_form.saving') : $_('invoices.sender_form.save')}
|
||||
</button>
|
||||
{#if savedAt}
|
||||
<span class="saved">Gespeichert um {savedAt}</span>
|
||||
<span class="saved"
|
||||
>{$_('invoices.sender_form.saved_at', { values: { time: savedAt } })}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { STATUS_LABELS, STATUS_COLORS } from '../constants';
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { STATUS_COLORS } from '../constants';
|
||||
import type { InvoiceStatus } from '../types';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -11,7 +12,7 @@
|
|||
|
||||
<span class="badge" style="--dot: {STATUS_COLORS[status]}">
|
||||
<span class="dot"></span>
|
||||
{STATUS_LABELS[status].de}
|
||||
{$_('invoices.status.' + status)}
|
||||
</span>
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,11 @@
|
|||
import type { Currency, InvoiceStatus } from './types';
|
||||
|
||||
/**
|
||||
* Status display labels are now sourced from the `invoices.status.*` i18n
|
||||
* namespace. Components should call `$_('invoices.status.' + status)`
|
||||
* directly. The legacy STATUS_LABELS export is kept for non-Svelte callers
|
||||
* (mail-template, PDF renderer) that don't have access to the i18n store.
|
||||
*/
|
||||
export const STATUS_LABELS: Record<InvoiceStatus, { de: string; en: string }> = {
|
||||
draft: { de: 'Entwurf', en: 'Draft' },
|
||||
sent: { de: 'Versendet', en: 'Sent' },
|
||||
|
|
@ -16,19 +22,22 @@ export const STATUS_COLORS: Record<InvoiceStatus, string> = {
|
|||
void: '#94a3b8',
|
||||
};
|
||||
|
||||
/** Swiss VAT rates as of 2024 (MwSt-Satz). */
|
||||
/**
|
||||
* VAT rate option lists carry i18n keys (under `invoices.vat_ch.*` /
|
||||
* `invoices.vat_de.*`) instead of literal labels. Consumers resolve to
|
||||
* the active locale via `$_(option.i18nKey)`.
|
||||
*/
|
||||
export const VAT_RATES_CH = [
|
||||
{ value: 0, label: '0% (ausgenommen)' },
|
||||
{ value: 2.6, label: '2.6% (reduziert)' },
|
||||
{ value: 3.8, label: '3.8% (Beherbergung)' },
|
||||
{ value: 8.1, label: '8.1% (Normalsatz)' },
|
||||
{ value: 0, i18nKey: 'invoices.vat_ch.v0' },
|
||||
{ value: 2.6, i18nKey: 'invoices.vat_ch.v2_6' },
|
||||
{ value: 3.8, i18nKey: 'invoices.vat_ch.v3_8' },
|
||||
{ value: 8.1, i18nKey: 'invoices.vat_ch.v8_1' },
|
||||
];
|
||||
|
||||
/** German VAT rates. */
|
||||
export const VAT_RATES_DE = [
|
||||
{ value: 0, label: '0%' },
|
||||
{ value: 7, label: '7% (ermäßigt)' },
|
||||
{ value: 19, label: '19% (Regelsatz)' },
|
||||
{ value: 0, i18nKey: 'invoices.vat_de.v0' },
|
||||
{ value: 7, i18nKey: 'invoices.vat_de.v7' },
|
||||
{ value: 19, i18nKey: 'invoices.vat_de.v19' },
|
||||
];
|
||||
|
||||
export const CURRENCIES: Record<Currency, { symbol: string; minorUnit: number }> = {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@
|
|||
M4 adds PDF preview on the right; M6 adds the send flow.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _, locale } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
import { goto } from '$app/navigation';
|
||||
import StatusBadge from '../components/StatusBadge.svelte';
|
||||
import SendModal from '../components/SendModal.svelte';
|
||||
|
|
@ -10,7 +12,6 @@
|
|||
import { invoiceSettingsStore } from '../stores/settings.svelte';
|
||||
import { formatAmount } from '../queries';
|
||||
import type { Invoice, InvoiceSettings } from '../types';
|
||||
import { STATUS_LABELS } from '../constants';
|
||||
|
||||
// Dynamic import — pdf-lib + swissqrbill together are ~350 KB, only needed
|
||||
// when a user actually opens an invoice (or sends one). Lazy-loading them
|
||||
|
|
@ -58,7 +59,7 @@
|
|||
if (pdfUrl) URL.revokeObjectURL(pdfUrl);
|
||||
pdfUrl = URL.createObjectURL(blob);
|
||||
} catch (e) {
|
||||
pdfError = e instanceof Error ? e.message : 'PDF-Rendering fehlgeschlagen';
|
||||
pdfError = e instanceof Error ? e.message : $_('invoices.detail.err_pdf_render');
|
||||
} finally {
|
||||
renderingPdf = false;
|
||||
}
|
||||
|
|
@ -100,7 +101,7 @@
|
|||
// Revoke after the browser has started the download.
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
} catch (e) {
|
||||
pdfError = e instanceof Error ? e.message : 'Download fehlgeschlagen';
|
||||
pdfError = e instanceof Error ? e.message : $_('invoices.detail.err_download');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -110,35 +111,38 @@
|
|||
try {
|
||||
await fn();
|
||||
} catch (e) {
|
||||
actionError = e instanceof Error ? e.message : `${label} fehlgeschlagen`;
|
||||
actionError =
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: $_('invoices.detail.err_action_failed', { values: { label } });
|
||||
} finally {
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function onMarkSent() {
|
||||
await run('Als versendet markieren', () => invoicesStore.markSent(invoice.id));
|
||||
await run($_('invoices.detail.action_mark_sent'), () => invoicesStore.markSent(invoice.id));
|
||||
}
|
||||
|
||||
async function onMarkPaid() {
|
||||
await run('Als bezahlt markieren', () => invoicesStore.markPaid(invoice.id));
|
||||
await run($_('invoices.detail.action_mark_paid'), () => invoicesStore.markPaid(invoice.id));
|
||||
}
|
||||
|
||||
async function onVoid() {
|
||||
if (!confirm('Diese Rechnung stornieren?')) return;
|
||||
await run('Stornieren', () => invoicesStore.voidInvoice(invoice.id));
|
||||
if (!confirm($_('invoices.detail.confirm_void'))) return;
|
||||
await run($_('invoices.detail.action_void'), () => invoicesStore.voidInvoice(invoice.id));
|
||||
}
|
||||
|
||||
async function onDuplicate() {
|
||||
await run('Duplizieren', async () => {
|
||||
await run($_('invoices.detail.action_duplicate'), async () => {
|
||||
const newId = await invoicesStore.duplicate(invoice.id);
|
||||
goto(`/invoices/${newId}`);
|
||||
});
|
||||
}
|
||||
|
||||
async function onDelete() {
|
||||
if (!confirm('Rechnung endgültig löschen?')) return;
|
||||
await run('Löschen', async () => {
|
||||
if (!confirm($_('invoices.detail.confirm_delete'))) return;
|
||||
await run($_('invoices.detail.action_delete'), async () => {
|
||||
await invoicesStore.deleteInvoice(invoice.id);
|
||||
goto('/invoices');
|
||||
});
|
||||
|
|
@ -153,35 +157,45 @@
|
|||
<header class="head">
|
||||
<div class="head-left">
|
||||
<div class="number">{invoice.number}</div>
|
||||
<h1>{invoice.subject || 'Rechnung'}</h1>
|
||||
<h1>{invoice.subject || $_('invoices.detail.title_default')}</h1>
|
||||
<StatusBadge status={invoice.status} />
|
||||
</div>
|
||||
<div class="head-right">
|
||||
<div class="amount">{formatAmount(invoice.totals.gross, invoice.currency)}</div>
|
||||
<div class="due">
|
||||
Fällig {invoice.dueDate}
|
||||
{$_('invoices.detail.due', { values: { date: invoice.dueDate } })}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="actions">
|
||||
{#if invoice.status === 'draft'}
|
||||
<button class="btn" onclick={onEdit}>Bearbeiten</button>
|
||||
<button class="btn btn-primary" onclick={openSendModal}>Per Mail versenden</button>
|
||||
<button class="btn" onclick={onMarkSent} disabled={busy}>Als versendet markieren</button>
|
||||
<button class="btn" onclick={onEdit}>{$_('invoices.detail.edit')}</button>
|
||||
<button class="btn btn-primary" onclick={openSendModal}
|
||||
>{$_('invoices.detail.send_via_mail')}</button
|
||||
>
|
||||
<button class="btn" onclick={onMarkSent} disabled={busy}
|
||||
>{$_('invoices.detail.mark_sent')}</button
|
||||
>
|
||||
{/if}
|
||||
{#if invoice.status === 'sent' || invoice.status === 'overdue'}
|
||||
<button class="btn btn-primary" onclick={onMarkPaid} disabled={busy}>
|
||||
Als bezahlt markieren
|
||||
{$_('invoices.detail.mark_paid')}
|
||||
</button>
|
||||
{/if}
|
||||
<button class="btn" onclick={downloadPdf}>PDF herunterladen</button>
|
||||
<button class="btn" onclick={onDuplicate} disabled={busy}>Duplizieren</button>
|
||||
<button class="btn" onclick={downloadPdf}>{$_('invoices.detail.download_pdf')}</button>
|
||||
<button class="btn" onclick={onDuplicate} disabled={busy}
|
||||
>{$_('invoices.detail.duplicate')}</button
|
||||
>
|
||||
{#if invoice.status !== 'paid' && invoice.status !== 'void'}
|
||||
<button class="btn btn-danger" onclick={onVoid} disabled={busy}> Stornieren </button>
|
||||
<button class="btn btn-danger" onclick={onVoid} disabled={busy}>
|
||||
{$_('invoices.detail.cancel')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if invoice.status === 'draft' || invoice.status === 'void'}
|
||||
<button class="btn btn-danger" onclick={onDelete} disabled={busy}> Löschen </button>
|
||||
<button class="btn btn-danger" onclick={onDelete} disabled={busy}>
|
||||
{$_('invoices.detail.delete')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
|
@ -191,35 +205,37 @@
|
|||
|
||||
<section class="block pdf-preview-block">
|
||||
<div class="preview-head">
|
||||
<h3>Vorschau</h3>
|
||||
<h3>{$_('invoices.detail.preview_title')}</h3>
|
||||
{#if renderingPdf}
|
||||
<span class="preview-status">Rendert …</span>
|
||||
<span class="preview-status">{$_('invoices.detail.preview_rendering')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if qrWarning}
|
||||
<div class="warning">
|
||||
<strong>QR-Rechnung nicht eingefügt:</strong>
|
||||
<strong>{$_('invoices.detail.qr_warning_strong')}</strong>
|
||||
{qrWarning}
|
||||
<a href="/invoices/settings">Einstellungen öffnen →</a>
|
||||
<a href="/invoices/settings">{$_('invoices.detail.qr_open_settings')}</a>
|
||||
</div>
|
||||
{/if}
|
||||
{#if pdfError}
|
||||
<div class="error">PDF-Fehler: {pdfError}</div>
|
||||
<div class="error">{$_('invoices.detail.err_pdf', { values: { error: pdfError } })}</div>
|
||||
{:else if pdfUrl}
|
||||
<iframe
|
||||
class="pdf-frame"
|
||||
src={pdfUrl}
|
||||
title="Vorschau Rechnung {invoice.number}"
|
||||
title={$_('invoices.detail.preview_iframe_title', {
|
||||
values: { number: invoice.number },
|
||||
})}
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<details class="raw-details">
|
||||
<summary>Strukturierte Daten anzeigen</summary>
|
||||
<summary>{$_('invoices.detail.raw_summary')}</summary>
|
||||
|
||||
<section class="block">
|
||||
<h3>Empfänger</h3>
|
||||
<h3>{$_('invoices.detail.section_recipient')}</h3>
|
||||
<div class="client">
|
||||
<div class="client-name">{invoice.clientSnapshot.name}</div>
|
||||
{#if invoice.clientSnapshot.street && invoice.clientSnapshot.city}
|
||||
|
|
@ -238,21 +254,25 @@
|
|||
<div class="client-meta">{invoice.clientSnapshot.email}</div>
|
||||
{/if}
|
||||
{#if invoice.clientSnapshot.vatNumber}
|
||||
<div class="client-meta">MwSt-Nr.: {invoice.clientSnapshot.vatNumber}</div>
|
||||
<div class="client-meta">
|
||||
{$_('invoices.detail.label_vat_number', {
|
||||
values: { number: invoice.clientSnapshot.vatNumber },
|
||||
})}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="block">
|
||||
<h3>Positionen</h3>
|
||||
<h3>{$_('invoices.detail.section_lines')}</h3>
|
||||
<table class="lines">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Position</th>
|
||||
<th>Menge</th>
|
||||
<th>Einzelpreis</th>
|
||||
<th>MwSt.</th>
|
||||
<th class="right">Netto</th>
|
||||
<th>{$_('invoices.detail.th_position')}</th>
|
||||
<th>{$_('invoices.detail.th_quantity')}</th>
|
||||
<th>{$_('invoices.detail.th_unit_price')}</th>
|
||||
<th>{$_('invoices.detail.th_vat')}</th>
|
||||
<th class="right">{$_('invoices.detail.th_net')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -275,38 +295,50 @@
|
|||
</section>
|
||||
|
||||
<section class="block totals-block">
|
||||
<h3>Summe</h3>
|
||||
<h3>{$_('invoices.detail.section_totals')}</h3>
|
||||
<dl class="totals">
|
||||
<dt>Netto</dt>
|
||||
<dt>{$_('invoices.detail.total_label_net')}</dt>
|
||||
<dd>{formatAmount(invoice.totals.net, invoice.currency)}</dd>
|
||||
{#each invoice.totals.vatBreakdown as b (b.rate)}
|
||||
<dt>MwSt. {b.rate}%</dt>
|
||||
<dt>{$_('invoices.detail.total_label_vat', { values: { rate: b.rate } })}</dt>
|
||||
<dd>{formatAmount(b.tax, invoice.currency)}</dd>
|
||||
{/each}
|
||||
<dt class="gross">Total</dt>
|
||||
<dt class="gross">{$_('invoices.detail.total_label_total')}</dt>
|
||||
<dd class="gross">{formatAmount(invoice.totals.gross, invoice.currency)}</dd>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
{#if invoice.notes}
|
||||
<section class="block">
|
||||
<h3>Notizen</h3>
|
||||
<h3>{$_('invoices.detail.section_notes')}</h3>
|
||||
<p class="prose">{invoice.notes}</p>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
{#if invoice.terms}
|
||||
<section class="block">
|
||||
<h3>Zahlungsbedingungen</h3>
|
||||
<h3>{$_('invoices.detail.section_terms')}</h3>
|
||||
<p class="prose">{invoice.terms}</p>
|
||||
</section>
|
||||
{/if}
|
||||
</details>
|
||||
|
||||
<footer class="meta">
|
||||
<div>Status: {STATUS_LABELS[invoice.status].de}</div>
|
||||
{#if invoice.sentAt}<div>Versendet: {new Date(invoice.sentAt).toLocaleString()}</div>{/if}
|
||||
{#if invoice.paidAt}<div>Bezahlt: {new Date(invoice.paidAt).toLocaleString()}</div>{/if}
|
||||
<div>
|
||||
{$_('invoices.detail.meta_status', {
|
||||
values: { label: $_('invoices.status.' + invoice.status) },
|
||||
})}
|
||||
</div>
|
||||
{#if invoice.sentAt}<div>
|
||||
{$_('invoices.detail.meta_sent', {
|
||||
values: { date: new Date(invoice.sentAt).toLocaleString(get(locale) ?? 'de') },
|
||||
})}
|
||||
</div>{/if}
|
||||
{#if invoice.paidAt}<div>
|
||||
{$_('invoices.detail.meta_paid', {
|
||||
values: { date: new Date(invoice.paidAt).toLocaleString(get(locale) ?? 'de') },
|
||||
})}
|
||||
</div>{/if}
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
* detaillierte Aufschlüsselung gibt's im ListView.
|
||||
*/
|
||||
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { invoiceTable } from '$lib/modules/invoices/collections';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
|
|
@ -76,9 +77,11 @@
|
|||
<div class="mb-3 flex items-center justify-between">
|
||||
<h3 class="flex items-center gap-2 text-lg font-semibold">
|
||||
<span aria-hidden="true">📄</span>
|
||||
Rechnungen
|
||||
{$_('invoices.widget.title')}
|
||||
</h3>
|
||||
<a href="/invoices" class="text-xs text-muted-foreground hover:text-foreground"> Alle → </a>
|
||||
<a href="/invoices" class="text-xs text-muted-foreground hover:text-foreground">
|
||||
{$_('invoices.widget.all_link')}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
|
|
@ -89,18 +92,18 @@
|
|||
</div>
|
||||
{:else if invoices.length === 0}
|
||||
<div class="py-4 text-center">
|
||||
<p class="text-sm text-muted-foreground">Noch keine Rechnungen gestellt.</p>
|
||||
<p class="text-sm text-muted-foreground">{$_('invoices.widget.empty')}</p>
|
||||
<a
|
||||
href="/invoices/new"
|
||||
class="mt-3 inline-block rounded-lg bg-primary/10 px-4 py-2 text-sm font-medium text-primary hover:bg-primary/20"
|
||||
>
|
||||
Erste Rechnung
|
||||
{$_('invoices.widget.create_first')}
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mb-3 grid grid-cols-2 gap-2">
|
||||
<div class="rounded-lg bg-surface-hover p-2.5">
|
||||
<div class="text-xs text-muted-foreground">Offen</div>
|
||||
<div class="text-xs text-muted-foreground">{$_('invoices.widget.open')}</div>
|
||||
<div class="text-lg font-semibold tabular-nums">
|
||||
{formatAmount(openSum, openCurrency)}
|
||||
</div>
|
||||
|
|
@ -110,12 +113,14 @@
|
|||
class:bg-surface-hover={overdueSum === 0}
|
||||
class:bg-red-50={overdueSum > 0}
|
||||
>
|
||||
<div class="text-xs text-muted-foreground">Überfällig</div>
|
||||
<div class="text-xs text-muted-foreground">{$_('invoices.widget.overdue')}</div>
|
||||
<div class="text-lg font-semibold tabular-nums" class:text-red-700={overdueSum > 0}>
|
||||
{formatAmount(overdueSum, openCurrency)}
|
||||
</div>
|
||||
{#if overdueCount > 0}
|
||||
<div class="text-[10px] text-red-700">{overdueCount} Rechnungen</div>
|
||||
<div class="text-[10px] text-red-700">
|
||||
{$_('invoices.widget.overdue_count', { values: { count: overdueCount } })}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -132,7 +137,7 @@
|
|||
{invoice.clientSnapshot.name}
|
||||
</div>
|
||||
<div class="text-xs text-red-700">
|
||||
{daysSince(invoice.dueDate)} Tage überfällig
|
||||
{$_('invoices.widget.days_overdue', { values: { n: daysSince(invoice.dueDate) } })}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-2 text-sm font-medium tabular-nums">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { page } from '$app/stores';
|
||||
import { useAllInvoices } from '$lib/modules/invoices/queries';
|
||||
import DetailView from '$lib/modules/invoices/views/DetailView.svelte';
|
||||
|
|
@ -11,19 +12,23 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{invoice?.number ?? 'Rechnung'} - Mana</title>
|
||||
<title
|
||||
>{$_('invoices.routes.detail_doc_title', {
|
||||
values: { number: invoice?.number ?? $_('invoices.routes.detail_title_fallback') },
|
||||
})}</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="invoices" backHref="/invoices" title="Rechnung">
|
||||
<RoutePage appId="invoices" backHref="/invoices" title={$_('invoices.routes.detail_route_title')}>
|
||||
{#if invoice}
|
||||
<DetailView {invoice} />
|
||||
{:else if invoices$.value !== undefined}
|
||||
<div class="not-found">
|
||||
<p>Rechnung nicht gefunden.</p>
|
||||
<a href="/invoices">Zurück zur Übersicht</a>
|
||||
<p>{$_('invoices.routes.not_found')}</p>
|
||||
<a href="/invoices">{$_('invoices.routes.back_to_list')}</a>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="loading">Lädt …</div>
|
||||
<div class="loading">{$_('invoices.routes.loading')}</div>
|
||||
{/if}
|
||||
</RoutePage>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { page } from '$app/stores';
|
||||
import { useAllInvoices } from '$lib/modules/invoices/queries';
|
||||
import InvoiceForm from '$lib/modules/invoices/components/InvoiceForm.svelte';
|
||||
|
|
@ -12,33 +13,35 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Rechnung bearbeiten - Mana</title>
|
||||
<title>{$_('invoices.routes.edit_doc_title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="invoices" backHref="/invoices" title="Rechnung">
|
||||
<RoutePage appId="invoices" backHref="/invoices" title={$_('invoices.routes.detail_route_title')}>
|
||||
<div class="page">
|
||||
{#if !invoice && invoices$.value !== undefined}
|
||||
<div class="not-found">
|
||||
<p>Rechnung nicht gefunden.</p>
|
||||
<a href="/invoices">Zurück zur Übersicht</a>
|
||||
<p>{$_('invoices.routes.not_found')}</p>
|
||||
<a href="/invoices">{$_('invoices.routes.back_to_list')}</a>
|
||||
</div>
|
||||
{:else if invoice && !canEdit}
|
||||
<div class="not-editable">
|
||||
<h2>Rechnung kann nicht bearbeitet werden</h2>
|
||||
<h2>{$_('invoices.routes.not_editable_title')}</h2>
|
||||
<p>
|
||||
Nur Entwürfe sind editierbar. Diese Rechnung hat Status
|
||||
<strong>{invoice.status}</strong>. Um eine versendete Rechnung zu ändern, storniere sie
|
||||
und dupliziere sie als neuen Entwurf.
|
||||
{$_('invoices.routes.not_editable_body_pre')}<strong
|
||||
>{$_('invoices.status.' + invoice.status)}</strong
|
||||
>{$_('invoices.routes.not_editable_body_post')}
|
||||
</p>
|
||||
<a href="/invoices/{invoice.id}">Zurück zum Detail</a>
|
||||
<a href="/invoices/{invoice.id}">{$_('invoices.routes.back_to_detail')}</a>
|
||||
</div>
|
||||
{:else if invoice}
|
||||
<header class="head">
|
||||
<h1>Rechnung {invoice.number} bearbeiten</h1>
|
||||
<h1>
|
||||
{$_('invoices.routes.edit_heading', { values: { number: invoice.number } })}
|
||||
</h1>
|
||||
</header>
|
||||
<InvoiceForm existing={invoice} />
|
||||
{:else}
|
||||
<div class="loading">Lädt …</div>
|
||||
<div class="loading">{$_('invoices.routes.loading')}</div>
|
||||
{/if}
|
||||
</div>
|
||||
</RoutePage>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import InvoiceForm from '$lib/modules/invoices/components/InvoiceForm.svelte';
|
||||
import { RoutePage } from '$lib/components/shell';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Neue Rechnung - Mana</title>
|
||||
<title>{$_('invoices.routes.new_doc_title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="invoices" backHref="/invoices">
|
||||
<div class="page">
|
||||
<header class="head">
|
||||
<h1>Neue Rechnung</h1>
|
||||
<p class="subtitle">Entwurf erstellen — wird nach dem Speichern noch nicht versendet.</p>
|
||||
<h1>{$_('invoices.routes.new_heading')}</h1>
|
||||
<p class="subtitle">{$_('invoices.routes.new_subtitle')}</p>
|
||||
</header>
|
||||
|
||||
<InvoiceForm />
|
||||
|
|
|
|||
|
|
@ -3,20 +3,21 @@
|
|||
Reached via the ⚙ button in the invoices module; not a workbench card.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import SenderProfileForm from '$lib/modules/invoices/components/SenderProfileForm.svelte';
|
||||
import { RoutePage } from '$lib/components/shell';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Rechnungs-Einstellungen — Mana</title>
|
||||
<title>{$_('invoices.routes.settings_doc_title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="invoices" backHref="/invoices">
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>Rechnungs-Einstellungen</strong>
|
||||
<span class="sub">Absender, Nummernkreis und Standards</span>
|
||||
<strong>{$_('invoices.routes.settings_heading')}</strong>
|
||||
<span class="sub">{$_('invoices.routes.settings_sub')}</span>
|
||||
</div>
|
||||
</header>
|
||||
<SenderProfileForm />
|
||||
|
|
|
|||
|
|
@ -153,16 +153,9 @@
|
|||
"apps/mana/apps/web/src/lib/modules/habits/ListView.svelte": 5,
|
||||
"apps/mana/apps/web/src/lib/modules/inventory/ListView.svelte": 1,
|
||||
"apps/mana/apps/web/src/lib/modules/inventory/views/DetailView.svelte": 5,
|
||||
"apps/mana/apps/web/src/lib/modules/invoices/components/ClientPicker.svelte": 3,
|
||||
"apps/mana/apps/web/src/lib/modules/invoices/components/InvoiceForm.svelte": 10,
|
||||
"apps/mana/apps/web/src/lib/modules/invoices/components/LinesEditor.svelte": 6,
|
||||
"apps/mana/apps/web/src/lib/modules/invoices/components/SenderProfileForm.svelte": 14,
|
||||
"apps/mana/apps/web/src/lib/modules/invoices/components/SendModal.svelte": 7,
|
||||
"apps/mana/apps/web/src/lib/modules/invoices/ListView.svelte": 6,
|
||||
"apps/mana/apps/web/src/lib/modules/invoices/views/DetailView.svelte": 18,
|
||||
"apps/mana/apps/web/src/lib/modules/invoices/widgets/InvoicesOpenWidget.svelte": 3,
|
||||
"apps/mana/apps/web/src/lib/modules/journal/ListView.svelte": 6,
|
||||
"apps/mana/apps/web/src/lib/modules/kontext/KontextView.svelte": 1,
|
||||
"apps/mana/apps/web/src/lib/modules/lasts/SharedLastView.svelte": 4,
|
||||
"apps/mana/apps/web/src/lib/modules/library/components/EntryForm.svelte": 5,
|
||||
"apps/mana/apps/web/src/lib/modules/library/components/ProgressControls.svelte": 3,
|
||||
"apps/mana/apps/web/src/lib/modules/library/ListView.svelte": 1,
|
||||
|
|
@ -342,10 +335,6 @@
|
|||
"apps/mana/apps/web/src/routes/(app)/inventory/items/[id]/+page.svelte": 7,
|
||||
"apps/mana/apps/web/src/routes/(app)/inventory/locations/+page.svelte": 2,
|
||||
"apps/mana/apps/web/src/routes/(app)/inventory/search/+page.svelte": 1,
|
||||
"apps/mana/apps/web/src/routes/(app)/invoices/[id]/+page.svelte": 2,
|
||||
"apps/mana/apps/web/src/routes/(app)/invoices/[id]/edit/+page.svelte": 4,
|
||||
"apps/mana/apps/web/src/routes/(app)/invoices/new/+page.svelte": 1,
|
||||
"apps/mana/apps/web/src/routes/(app)/invoices/settings/+page.svelte": 1,
|
||||
"apps/mana/apps/web/src/routes/(app)/library/entry/[id]/+page.svelte": 1,
|
||||
"apps/mana/apps/web/src/routes/(app)/llm-test/+page.svelte": 29,
|
||||
"apps/mana/apps/web/src/routes/(app)/meditate/+page.svelte": 4,
|
||||
|
|
@ -376,6 +365,7 @@
|
|||
"apps/mana/apps/web/src/routes/(app)/onboarding/look/+page.svelte": 6,
|
||||
"apps/mana/apps/web/src/routes/(app)/onboarding/name/+page.svelte": 2,
|
||||
"apps/mana/apps/web/src/routes/(app)/onboarding/templates/+page.svelte": 2,
|
||||
"apps/mana/apps/web/src/routes/(app)/onboarding/wish/+page.svelte": 7,
|
||||
"apps/mana/apps/web/src/routes/(app)/organizations/[id]/+page.svelte": 6,
|
||||
"apps/mana/apps/web/src/routes/(app)/organizations/+page.svelte": 1,
|
||||
"apps/mana/apps/web/src/routes/(app)/photos/+page.svelte": 3,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue