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:
Till JS 2026-04-26 21:56:48 +02:00
parent f10439e369
commit 679fb160c2
20 changed files with 1541 additions and 243 deletions

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View 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"
}
}

View file

@ -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">

View file

@ -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 })}
/>

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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&#10;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>

View file

@ -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>

View file

@ -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 }> = {

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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 />

View file

@ -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 />

View file

@ -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,