Преглед изворни кода

Split invoice settings into three cards, move markup and customer numbers home (#366)

Bernt Christian Egeland пре 3 недеља
родитељ
комит
a1cffaa593

+ 4 - 0
messages/de/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Kunde konnte nicht gelöscht werden",
     "deleteError": "Kunde konnte nicht gelöscht werden",
     "error": "Kunden konnten nicht geladen werden",
     "error": "Kunden konnten nicht geladen werden",
     "importCustomers": "Importieren",
     "importCustomers": "Importieren",
+    "assignNumbers": "Nummern vergeben",
+    "assignNumbersTitle": "Kundennummern vergeben?",
+    "assignNumbersDescription": "{count, plural, one {# Kunde hat} other {# Kunden haben}} noch keine Nummer. Jeder erhält die nächste Nummer der Reihe nach, älteste zuerst. Kunden mit Nummer bleiben unverändert.",
+    "assignNumbersDone": "{count, plural, one {# Kunde} other {# Kunden}} nummeriert",
     "selectedCount": "{count} ausgewählt",
     "selectedCount": "{count} ausgewählt",
     "clearSelection": "Auswahl aufheben",
     "clearSelection": "Auswahl aufheben",
     "batchDelete": "Löschen ({count})",
     "batchDelete": "Löschen ({count})",

+ 12 - 13
messages/de/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Rechnungslayout",
     "title": "Rechnungslayout",
     "description": "Konfigurieren Sie, was auf generierten PDF-Rechnungen erscheint.",
     "description": "Konfigurieren Sie, was auf generierten PDF-Rechnungen erscheint.",
+    "numberingTitle": "Nummerierung und Fristen",
+    "numberingDescription": "Wie Rechnungen und Angebote nummeriert werden, wann Rechnungen fällig sind und wie lange Angebote gelten.",
+    "documentsTitle": "Dokumente",
+    "documentsDescription": "Was jede Rechnung und jedes Angebot mitbringt.",
     "tabs": {
     "tabs": {
       "general": "Allgemein",
       "general": "Allgemein",
       "layout": "Layout",
       "layout": "Layout",
@@ -550,12 +554,6 @@
     "customFooter": "Benutzerdefinierte Rechnungsfusszeile",
     "customFooter": "Benutzerdefinierte Rechnungsfusszeile",
     "footerPlaceholder": "Vielen Dank für Ihren Auftrag!",
     "footerPlaceholder": "Vielen Dank für Ihren Auftrag!",
     "footerHint": "Dieser Text erscheint am Ende jeder Rechnung",
     "footerHint": "Dieser Text erscheint am Ende jeder Rechnung",
-    "partsMarkupTitle": "Teile-Aufschlag",
-    "partsMarkupDescription": "Aufschlag auf den Einkaufspreis beim Hinzufügen von Teilen. Nur intern — der Kunde sieht weder Kosten noch Aufschlag, nur den Endpreis.",
-    "defaultMarkupPercent": "Standard-Aufschlag %",
-    "defaultMarkupPercentHint": "Neue manuell hinzugefügte Teile werden mit diesem Aufschlag vorausgefüllt. Auf 0 lassen, um Preise manuell einzugeben.",
-    "markupAppliesToInventory": "Aufschlag auf Lagerteile anwenden",
-    "markupAppliesToInventoryHint": "Wenn aktiviert, verwenden aus dem Lager gewählte Teile ebenfalls Einkaufspreis + Standard-Aufschlag (überschreibt den Verkaufspreis des Lagers).",
     "saveInvoice": "Rechnungseinstellungen speichern",
     "saveInvoice": "Rechnungseinstellungen speichern",
     "saved": "Rechnungseinstellungen gespeichert",
     "saved": "Rechnungseinstellungen gespeichert",
     "layoutDescription": "Passen Sie die Reihenfolge und Sichtbarkeit der Abschnitte in Ihren Rechnungen und Angeboten an.",
     "layoutDescription": "Passen Sie die Reihenfolge und Sichtbarkeit der Abschnitte in Ihren Rechnungen und Angeboten an.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Angebot",
     "layoutDocQuote": "Angebot",
     "saveLayout": "Layout speichern",
     "saveLayout": "Layout speichern",
     "resetLayout": "Zurücksetzen",
     "resetLayout": "Zurücksetzen",
-    "sectionCustomers": "Kunden",
-    "assignCustomerNumbers": "Kundennummern vergeben",
-    "assignCustomerNumbersHint": "{count} Kunden haben noch keine Nummer. Fortlaufende Nummern vergeben (bestehende bleiben erhalten).",
-    "customerNumbersAssigned": "{count} Kunden nummeriert",
-    "allCustomersNumbered": "Alle Kunden haben eine Nummer.",
-    "assignCustomerNumbersConfirmTitle": "Kundennummern vergeben?",
-    "assignCustomerNumbersConfirmDescription": "Die {count} Kunden ohne Nummer erhalten fortlaufende Nummern in der Reihenfolge ihrer Erstellung, beginnend nach der höchsten vorhandenen Nummer. Kunden mit Nummer bleiben unverändert. Nummern können danach bearbeitet werden.",
     "layoutColorsHint": "Farben, Schriften und der Briefkopf-Stil liegen unter Vorlagen.",
     "layoutColorsHint": "Farben, Schriften und der Briefkopf-Stil liegen unter Vorlagen.",
     "goToTemplates": "Vorlagen öffnen",
     "goToTemplates": "Vorlagen öffnen",
     "lockTitle": "Abgeschlossene Dokumente sperren",
     "lockTitle": "Abgeschlossene Dokumente sperren",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Gesperrt werden: Teile, Arbeit, Mengen, Preise, Rabatt, Steuer, Belegnummer und Datum. Auch das Löschen des Dokuments wird verhindert.",
     "lockWhatItDoes": "Gesperrt werden: Teile, Arbeit, Mengen, Preise, Rabatt, Steuer, Belegnummer und Datum. Auch das Löschen des Dokuments wird verhindert.",
     "lockWhatItAllows": "Weiterhin möglich: Zahlungen erfassen, Online-Kartenzahlung, Statusänderungen, Versand und erneuter Versand, Anhänge und interne Notizen. Eine gesperrte Rechnung kann immer bezahlt werden.",
     "lockWhatItAllows": "Weiterhin möglich: Zahlungen erfassen, Online-Kartenzahlung, Statusänderungen, Versand und erneuter Versand, Anhänge und interne Notizen. Eine gesperrte Rechnung kann immer bezahlt werden.",
     "lockUnlockNote": "Inhaber und Administratoren können ein einzelnes Dokument auf dessen eigener Seite entsperren, wenn wirklich etwas korrigiert werden muss. Jede Entsperrung wird im Audit-Log festgehalten.",
     "lockUnlockNote": "Inhaber und Administratoren können ein einzelnes Dokument auf dessen eigener Seite entsperren, wenn wirklich etwas korrigiert werden muss. Jede Entsperrung wird im Audit-Log festgehalten.",
+    "lockDetailsShow": "Was das Sperren bewirkt",
+    "lockDetailsHide": "Ausblenden",
     "lockInvoicesLabel": "Rechnungen sperren",
     "lockInvoicesLabel": "Rechnungen sperren",
     "lockTriggerSent": "Sobald die Rechnung versendet ist",
     "lockTriggerSent": "Sobald die Rechnung versendet ist",
     "lockTriggerPaid": "Sobald die Rechnung vollständig bezahlt ist",
     "lockTriggerPaid": "Sobald die Rechnung vollständig bezahlt ist",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Neu hinzufügen",
     "addNewTech": "+ Neu hinzufügen",
     "defaultLaborRate": "Standard-Stundensatz",
     "defaultLaborRate": "Standard-Stundensatz",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
+    "partsMarkupTitle": "Teile-Aufschlag",
+    "partsMarkupDescription": "Aufschlag auf den Einkaufspreis beim Hinzufügen von Teilen. Nur intern. Der Kunde sieht weder Kosten noch Aufschlag, nur den Endpreis.",
+    "defaultMarkupPercent": "Standard-Aufschlag %",
+    "defaultMarkupPercentHint": "Neue manuell hinzugefügte Teile werden mit diesem Aufschlag vorausgefüllt. Auf 0 lassen, um Preise manuell einzugeben.",
+    "markupAppliesToInventory": "Aufschlag auf Lagerteile anwenden",
+    "markupAppliesToInventoryHint": "Wenn aktiviert, verwenden aus dem Lager gewählte Teile ebenfalls Einkaufspreis + Standard-Aufschlag (überschreibt den Verkaufspreis des Lagers).",
     "unitsTitle": "Einheiten",
     "unitsTitle": "Einheiten",
     "unitsDescription": "Wählen Sie zwischen metrisch (km, Liter) und imperial (Meilen, Gallonen).",
     "unitsDescription": "Wählen Sie zwischen metrisch (km, Liter) und imperial (Meilen, Gallonen).",
     "unitSystem": "Einheitensystem",
     "unitSystem": "Einheitensystem",

+ 4 - 0
messages/en/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Failed to delete customer",
     "deleteError": "Failed to delete customer",
     "error": "Failed to load customers",
     "error": "Failed to load customers",
     "importCustomers": "Import",
     "importCustomers": "Import",
+    "assignNumbers": "Assign numbers",
+    "assignNumbersTitle": "Assign customer numbers?",
+    "assignNumbersDescription": "{count, plural, one {# customer has} other {# customers have}} no number yet. Each gets the next number in sequence, oldest first. Customers that already have a number are not touched.",
+    "assignNumbersDone": "{count, plural, one {# customer} other {# customers}} numbered",
     "selectedCount": "{count} selected",
     "selectedCount": "{count} selected",
     "clearSelection": "Clear",
     "clearSelection": "Clear",
     "batchDelete": "Delete ({count})",
     "batchDelete": "Delete ({count})",

+ 12 - 13
messages/en/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Invoice & Quotes",
     "title": "Invoice & Quotes",
     "description": "Configure invoice settings, layout and custom fields.",
     "description": "Configure invoice settings, layout and custom fields.",
+    "numberingTitle": "Numbering and dates",
+    "numberingDescription": "How invoices and quotes are numbered, when invoices fall due and how long quotes stay valid.",
+    "documentsTitle": "Documents",
+    "documentsDescription": "What every invoice and quote carries with it.",
     "tabs": {
     "tabs": {
       "general": "General",
       "general": "General",
       "layout": "Layout",
       "layout": "Layout",
@@ -550,12 +554,6 @@
     "customFooter": "Custom Invoice Footer",
     "customFooter": "Custom Invoice Footer",
     "footerPlaceholder": "Thank you for your business!",
     "footerPlaceholder": "Thank you for your business!",
     "footerHint": "This text appears at the bottom of every invoice",
     "footerHint": "This text appears at the bottom of every invoice",
-    "partsMarkupTitle": "Parts Markup",
-    "partsMarkupDescription": "Markup applied to vendor cost when adding parts. Internal only — the customer never sees the cost or markup, only the final price.",
-    "defaultMarkupPercent": "Default Markup %",
-    "defaultMarkupPercentHint": "New manually-added parts pre-fill with this markup. Leave at 0 to enter prices manually.",
-    "markupAppliesToInventory": "Apply markup to inventory parts",
-    "markupAppliesToInventoryHint": "When enabled, parts picked from inventory also use cost + default markup (overrides the inventory sell price).",
     "saveInvoice": "Save Invoice Settings",
     "saveInvoice": "Save Invoice Settings",
     "saved": "Invoice settings saved",
     "saved": "Invoice settings saved",
     "layoutDescription": "Customize the order and visibility of sections in your invoices and quotes.",
     "layoutDescription": "Customize the order and visibility of sections in your invoices and quotes.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Quote",
     "layoutDocQuote": "Quote",
     "saveLayout": "Save Layout",
     "saveLayout": "Save Layout",
     "resetLayout": "Reset to Default",
     "resetLayout": "Reset to Default",
-    "sectionCustomers": "Customers",
-    "assignCustomerNumbers": "Assign customer numbers",
-    "assignCustomerNumbersHint": "{count} customers have no number yet. Assign sequential numbers to them (existing numbers are kept).",
-    "customerNumbersAssigned": "Assigned numbers to {count} customers",
-    "allCustomersNumbered": "All customers have a number.",
-    "assignCustomerNumbersConfirmTitle": "Assign customer numbers?",
-    "assignCustomerNumbersConfirmDescription": "This will give sequential numbers to the {count} customers that have no number yet, in the order they were created, continuing after your highest existing number. Customers that already have a number are not changed. Numbers can be edited afterwards.",
     "layoutColorsHint": "Colors, fonts and the header style live in Templates.",
     "layoutColorsHint": "Colors, fonts and the header style live in Templates.",
     "goToTemplates": "Open Templates",
     "goToTemplates": "Open Templates",
     "lockTitle": "Locking finished documents",
     "lockTitle": "Locking finished documents",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "What it freezes: parts, labour, quantities, prices, discount, tax, the document number and its date. Deleting the document is blocked too.",
     "lockWhatItDoes": "What it freezes: parts, labour, quantities, prices, discount, tax, the document number and its date. Deleting the document is blocked too.",
     "lockWhatItAllows": "What still works: recording payments, online card payments, status changes, sending and re-sending, attachments and internal notes. A locked invoice can always still be paid.",
     "lockWhatItAllows": "What still works: recording payments, online card payments, status changes, sending and re-sending, attachments and internal notes. A locked invoice can always still be paid.",
     "lockUnlockNote": "Owners and admins can unlock a single document from its own page when something genuinely needs correcting. Every unlock is recorded in the audit log.",
     "lockUnlockNote": "Owners and admins can unlock a single document from its own page when something genuinely needs correcting. Every unlock is recorded in the audit log.",
+    "lockDetailsShow": "What locking does",
+    "lockDetailsHide": "Hide",
     "lockInvoicesLabel": "Lock invoices",
     "lockInvoicesLabel": "Lock invoices",
     "lockTriggerSent": "Once the invoice is sent",
     "lockTriggerSent": "Once the invoice is sent",
     "lockTriggerPaid": "Once the invoice is paid in full",
     "lockTriggerPaid": "Once the invoice is paid in full",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Add New",
     "addNewTech": "+ Add New",
     "defaultLaborRate": "Default Labor Rate (per hour)",
     "defaultLaborRate": "Default Labor Rate (per hour)",
     "laborRatePlaceholder": "75.00",
     "laborRatePlaceholder": "75.00",
+    "partsMarkupTitle": "Parts Markup",
+    "partsMarkupDescription": "Markup added to the vendor cost when parts go on a job. Internal only. The customer sees the final price, never the cost or the markup.",
+    "defaultMarkupPercent": "Default Markup %",
+    "defaultMarkupPercentHint": "New manually-added parts pre-fill with this markup. Leave at 0 to enter prices manually.",
+    "markupAppliesToInventory": "Apply markup to inventory parts",
+    "markupAppliesToInventoryHint": "When enabled, parts picked from inventory also use cost + default markup (overrides the inventory sell price).",
     "unitsTitle": "Units",
     "unitsTitle": "Units",
     "unitsDescription": "Choose between metric (km, liters) and imperial (miles, gallons) units.",
     "unitsDescription": "Choose between metric (km, liters) and imperial (miles, gallons) units.",
     "unitSystem": "Unit System",
     "unitSystem": "Unit System",

+ 4 - 0
messages/es/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Error al eliminar el cliente",
     "deleteError": "Error al eliminar el cliente",
     "error": "Error al cargar los clientes",
     "error": "Error al cargar los clientes",
     "importCustomers": "Importar",
     "importCustomers": "Importar",
+    "assignNumbers": "Asignar números",
+    "assignNumbersTitle": "¿Asignar números de cliente?",
+    "assignNumbersDescription": "{count, plural, one {# cliente no tiene} other {# clientes no tienen}} número todavía. Cada uno recibe el siguiente número de la secuencia, empezando por el más antiguo. Los clientes que ya tienen número no se modifican.",
+    "assignNumbersDone": "{count, plural, one {# cliente numerado} other {# clientes numerados}}",
     "selectedCount": "{count} seleccionado(s)",
     "selectedCount": "{count} seleccionado(s)",
     "clearSelection": "Limpiar",
     "clearSelection": "Limpiar",
     "batchDelete": "Eliminar ({count})",
     "batchDelete": "Eliminar ({count})",

+ 12 - 13
messages/es/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Diseño de factura",
     "title": "Diseño de factura",
     "description": "Configure lo que aparece en las facturas PDF generadas.",
     "description": "Configure lo que aparece en las facturas PDF generadas.",
+    "numberingTitle": "Numeración y plazos",
+    "numberingDescription": "Cómo se numeran facturas y presupuestos, cuándo vencen las facturas y cuánto tiempo son válidos los presupuestos.",
+    "documentsTitle": "Documentos",
+    "documentsDescription": "Lo que acompaña a cada factura y presupuesto.",
     "tabs": {
     "tabs": {
       "general": "General",
       "general": "General",
       "layout": "Diseño",
       "layout": "Diseño",
@@ -550,12 +554,6 @@
     "customFooter": "Pie de página personalizado",
     "customFooter": "Pie de página personalizado",
     "footerPlaceholder": "Gracias por su preferencia!",
     "footerPlaceholder": "Gracias por su preferencia!",
     "footerHint": "Este texto aparece al final de cada factura",
     "footerHint": "Este texto aparece al final de cada factura",
-    "partsMarkupTitle": "Margen de Repuestos",
-    "partsMarkupDescription": "Margen aplicado al costo del proveedor al agregar repuestos. Solo interno — el cliente nunca ve el costo ni el margen, solo el precio final.",
-    "defaultMarkupPercent": "Margen Predeterminado %",
-    "defaultMarkupPercentHint": "Los repuestos nuevos agregados manualmente se prellenan con este margen. Déjelo en 0 para ingresar precios manualmente.",
-    "markupAppliesToInventory": "Aplicar margen a repuestos de inventario",
-    "markupAppliesToInventoryHint": "Cuando está habilitado, los repuestos seleccionados del inventario también usan costo + margen predeterminado (anula el precio de venta del inventario).",
     "saveInvoice": "Guardar configuración de factura",
     "saveInvoice": "Guardar configuración de factura",
     "saved": "Configuración de factura guardada",
     "saved": "Configuración de factura guardada",
     "layoutDescription": "Personaliza el orden y la visibilidad de las secciones en tus facturas y presupuestos.",
     "layoutDescription": "Personaliza el orden y la visibilidad de las secciones en tus facturas y presupuestos.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Presupuesto",
     "layoutDocQuote": "Presupuesto",
     "saveLayout": "Guardar diseño",
     "saveLayout": "Guardar diseño",
     "resetLayout": "Restablecer",
     "resetLayout": "Restablecer",
-    "sectionCustomers": "Clientes",
-    "assignCustomerNumbers": "Asignar números de cliente",
-    "assignCustomerNumbersHint": "{count} clientes aún no tienen número. Asignarles números secuenciales (los existentes se conservan).",
-    "customerNumbersAssigned": "Números asignados a {count} clientes",
-    "allCustomersNumbered": "Todos los clientes tienen número.",
-    "assignCustomerNumbersConfirmTitle": "¿Asignar números de cliente?",
-    "assignCustomerNumbersConfirmDescription": "Los {count} clientes sin número recibirán números secuenciales en el orden en que se crearon, continuando después del número existente más alto. Los clientes que ya tienen número no cambian. Los números se pueden editar después.",
     "layoutColorsHint": "Los colores, las fuentes y el estilo de encabezado están en Plantillas.",
     "layoutColorsHint": "Los colores, las fuentes y el estilo de encabezado están en Plantillas.",
     "goToTemplates": "Abrir Plantillas",
     "goToTemplates": "Abrir Plantillas",
     "lockTitle": "Bloqueo de documentos finalizados",
     "lockTitle": "Bloqueo de documentos finalizados",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Se bloquea: piezas, mano de obra, cantidades, precios, descuento, impuestos, número de documento y fecha. También se impide eliminarlo.",
     "lockWhatItDoes": "Se bloquea: piezas, mano de obra, cantidades, precios, descuento, impuestos, número de documento y fecha. También se impide eliminarlo.",
     "lockWhatItAllows": "Sigue funcionando: registrar pagos, pago con tarjeta en línea, cambios de estado, envío y reenvío, adjuntos y notas internas. Una factura bloqueada siempre se puede pagar.",
     "lockWhatItAllows": "Sigue funcionando: registrar pagos, pago con tarjeta en línea, cambios de estado, envío y reenvío, adjuntos y notas internas. Una factura bloqueada siempre se puede pagar.",
     "lockUnlockNote": "Los propietarios y administradores pueden desbloquear un documento concreto desde su propia página cuando algo deba corregirse. Cada desbloqueo queda en el registro de auditoría.",
     "lockUnlockNote": "Los propietarios y administradores pueden desbloquear un documento concreto desde su propia página cuando algo deba corregirse. Cada desbloqueo queda en el registro de auditoría.",
+    "lockDetailsShow": "Qué hace el bloqueo",
+    "lockDetailsHide": "Ocultar",
     "lockInvoicesLabel": "Bloquear facturas",
     "lockInvoicesLabel": "Bloquear facturas",
     "lockTriggerSent": "Cuando se envía la factura",
     "lockTriggerSent": "Cuando se envía la factura",
     "lockTriggerPaid": "Cuando la factura está pagada por completo",
     "lockTriggerPaid": "Cuando la factura está pagada por completo",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Agregar nuevo",
     "addNewTech": "+ Agregar nuevo",
     "defaultLaborRate": "Tarifa de mano de obra predeterminada (por hora)",
     "defaultLaborRate": "Tarifa de mano de obra predeterminada (por hora)",
     "laborRatePlaceholder": "75.00",
     "laborRatePlaceholder": "75.00",
+    "partsMarkupTitle": "Margen de Repuestos",
+    "partsMarkupDescription": "Margen aplicado al costo del proveedor al agregar repuestos. Solo interno. El cliente nunca ve el costo ni el margen, solo el precio final.",
+    "defaultMarkupPercent": "Margen Predeterminado %",
+    "defaultMarkupPercentHint": "Los repuestos nuevos agregados manualmente se prellenan con este margen. Déjelo en 0 para ingresar precios manualmente.",
+    "markupAppliesToInventory": "Aplicar margen a repuestos de inventario",
+    "markupAppliesToInventoryHint": "Cuando está habilitado, los repuestos seleccionados del inventario también usan costo + margen predeterminado (anula el precio de venta del inventario).",
     "unitsTitle": "Unidades",
     "unitsTitle": "Unidades",
     "unitsDescription": "Elija entre metrico (km, litros) e imperial (millas, galones).",
     "unitsDescription": "Elija entre metrico (km, litros) e imperial (millas, galones).",
     "unitSystem": "Sistema de unidades",
     "unitSystem": "Sistema de unidades",

+ 4 - 0
messages/fr/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Échec de la suppression du client",
     "deleteError": "Échec de la suppression du client",
     "error": "Échec du chargement des clients",
     "error": "Échec du chargement des clients",
     "importCustomers": "Importer",
     "importCustomers": "Importer",
+    "assignNumbers": "Attribuer des numéros",
+    "assignNumbersTitle": "Attribuer des numéros de client ?",
+    "assignNumbersDescription": "{count, plural, one {# client n'a} other {# clients n'ont}} pas encore de numéro. Chacun reçoit le numéro suivant de la séquence, le plus ancien en premier. Les clients qui ont déjà un numéro ne sont pas modifiés.",
+    "assignNumbersDone": "{count, plural, one {# client numéroté} other {# clients numérotés}}",
     "selectedCount": "{count} sélectionné(s)",
     "selectedCount": "{count} sélectionné(s)",
     "clearSelection": "Effacer",
     "clearSelection": "Effacer",
     "batchDelete": "Supprimer ({count})",
     "batchDelete": "Supprimer ({count})",

+ 12 - 13
messages/fr/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Mise en page de facture",
     "title": "Mise en page de facture",
     "description": "Configurez ce qui apparait sur les factures PDF générées.",
     "description": "Configurez ce qui apparait sur les factures PDF générées.",
+    "numberingTitle": "Numérotation et délais",
+    "numberingDescription": "Comment les factures et les devis sont numérotés, quand les factures arrivent à échéance et combien de temps les devis restent valables.",
+    "documentsTitle": "Documents",
+    "documentsDescription": "Ce que chaque facture et chaque devis emporte avec lui.",
     "tabs": {
     "tabs": {
       "general": "Général",
       "general": "Général",
       "layout": "Mise en page",
       "layout": "Mise en page",
@@ -550,12 +554,6 @@
     "customFooter": "Pied de page personnalise",
     "customFooter": "Pied de page personnalise",
     "footerPlaceholder": "Merci pour votre confiance !",
     "footerPlaceholder": "Merci pour votre confiance !",
     "footerHint": "Ce texte apparait en bas de chaque facture",
     "footerHint": "Ce texte apparait en bas de chaque facture",
-    "partsMarkupTitle": "Marge sur Pièces",
-    "partsMarkupDescription": "Marge appliquée au coût fournisseur lors de l'ajout de pièces. Interne uniquement — le client ne voit jamais le coût ou la marge, seulement le prix final.",
-    "defaultMarkupPercent": "Marge par Défaut %",
-    "defaultMarkupPercentHint": "Les nouvelles pièces ajoutées manuellement sont préremplies avec cette marge. Laisser à 0 pour saisir les prix manuellement.",
-    "markupAppliesToInventory": "Appliquer la marge aux pièces du stock",
-    "markupAppliesToInventoryHint": "Lorsque activé, les pièces sélectionnées dans le stock utilisent aussi coût + marge par défaut (remplace le prix de vente du stock).",
     "saveInvoice": "Enregistrer les paramètres de facture",
     "saveInvoice": "Enregistrer les paramètres de facture",
     "saved": "Paramètres de facture enregistrés",
     "saved": "Paramètres de facture enregistrés",
     "layoutDescription": "Personnalisez l'ordre et la visibilité des sections dans vos factures et devis.",
     "layoutDescription": "Personnalisez l'ordre et la visibilité des sections dans vos factures et devis.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Devis",
     "layoutDocQuote": "Devis",
     "saveLayout": "Enregistrer la mise en page",
     "saveLayout": "Enregistrer la mise en page",
     "resetLayout": "Réinitialiser",
     "resetLayout": "Réinitialiser",
-    "sectionCustomers": "Clients",
-    "assignCustomerNumbers": "Attribuer des numéros clients",
-    "assignCustomerNumbersHint": "{count} clients n'ont pas encore de numéro. Leur attribuer des numéros séquentiels (les existants sont conservés).",
-    "customerNumbersAssigned": "Numéros attribués à {count} clients",
-    "allCustomersNumbered": "Tous les clients ont un numéro.",
-    "assignCustomerNumbersConfirmTitle": "Attribuer des numéros clients ?",
-    "assignCustomerNumbersConfirmDescription": "Les {count} clients sans numéro recevront des numéros séquentiels dans leur ordre de création, à la suite du numéro existant le plus élevé. Les clients ayant déjà un numéro ne sont pas modifiés. Les numéros restent modifiables ensuite.",
     "layoutColorsHint": "Les couleurs, les polices et le style d’en-tête sont dans Modèles.",
     "layoutColorsHint": "Les couleurs, les polices et le style d’en-tête sont dans Modèles.",
     "goToTemplates": "Ouvrir Modèles",
     "goToTemplates": "Ouvrir Modèles",
     "lockTitle": "Verrouillage des documents finalisés",
     "lockTitle": "Verrouillage des documents finalisés",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Ce qui est verrouillé : pièces, main-d’œuvre, quantités, prix, remise, taxe, numéro et date du document. La suppression est également bloquée.",
     "lockWhatItDoes": "Ce qui est verrouillé : pièces, main-d’œuvre, quantités, prix, remise, taxe, numéro et date du document. La suppression est également bloquée.",
     "lockWhatItAllows": "Ce qui fonctionne encore : enregistrer des paiements, le paiement en ligne, les changements de statut, l’envoi et le renvoi, les pièces jointes et les notes internes. Une facture verrouillée reste toujours payable.",
     "lockWhatItAllows": "Ce qui fonctionne encore : enregistrer des paiements, le paiement en ligne, les changements de statut, l’envoi et le renvoi, les pièces jointes et les notes internes. Une facture verrouillée reste toujours payable.",
     "lockUnlockNote": "Les propriétaires et administrateurs peuvent déverrouiller un document depuis sa propre page lorsqu’une correction est réellement nécessaire. Chaque déverrouillage est consigné dans le journal d’audit.",
     "lockUnlockNote": "Les propriétaires et administrateurs peuvent déverrouiller un document depuis sa propre page lorsqu’une correction est réellement nécessaire. Chaque déverrouillage est consigné dans le journal d’audit.",
+    "lockDetailsShow": "Ce que fait le verrouillage",
+    "lockDetailsHide": "Masquer",
     "lockInvoicesLabel": "Verrouiller les factures",
     "lockInvoicesLabel": "Verrouiller les factures",
     "lockTriggerSent": "Une fois la facture envoyée",
     "lockTriggerSent": "Une fois la facture envoyée",
     "lockTriggerPaid": "Une fois la facture intégralement payée",
     "lockTriggerPaid": "Une fois la facture intégralement payée",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Ajouter nouveau",
     "addNewTech": "+ Ajouter nouveau",
     "defaultLaborRate": "Taux horaire par défaut",
     "defaultLaborRate": "Taux horaire par défaut",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
+    "partsMarkupTitle": "Marge sur Pièces",
+    "partsMarkupDescription": "Marge appliquée au coût fournisseur lors de l'ajout de pièces. Interne uniquement. Le client ne voit jamais le coût ou la marge, seulement le prix final.",
+    "defaultMarkupPercent": "Marge par Défaut %",
+    "defaultMarkupPercentHint": "Les nouvelles pièces ajoutées manuellement sont préremplies avec cette marge. Laisser à 0 pour saisir les prix manuellement.",
+    "markupAppliesToInventory": "Appliquer la marge aux pièces du stock",
+    "markupAppliesToInventoryHint": "Lorsque activé, les pièces sélectionnées dans le stock utilisent aussi coût + marge par défaut (remplace le prix de vente du stock).",
     "unitsTitle": "Unites",
     "unitsTitle": "Unites",
     "unitsDescription": "Choisissez entre metrique (km, litres) et imperial (miles, gallons).",
     "unitsDescription": "Choisissez entre metrique (km, litres) et imperial (miles, gallons).",
     "unitSystem": "Système d'unites",
     "unitSystem": "Système d'unites",

+ 4 - 0
messages/it/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Eliminazione del cliente non riuscita",
     "deleteError": "Eliminazione del cliente non riuscita",
     "error": "Caricamento dei clienti non riuscito",
     "error": "Caricamento dei clienti non riuscito",
     "importCustomers": "Importa",
     "importCustomers": "Importa",
+    "assignNumbers": "Assegna numeri",
+    "assignNumbersTitle": "Assegnare i numeri cliente?",
+    "assignNumbersDescription": "{count, plural, one {# cliente non ha} other {# clienti non hanno}} ancora un numero. Ognuno riceve il numero successivo della sequenza, partendo dal più vecchio. I clienti che hanno già un numero non vengono toccati.",
+    "assignNumbersDone": "{count, plural, one {# cliente numerato} other {# clienti numerati}}",
     "selectedCount": "{count} selezionato/i",
     "selectedCount": "{count} selezionato/i",
     "clearSelection": "Deseleziona",
     "clearSelection": "Deseleziona",
     "batchDelete": "Elimina ({count})",
     "batchDelete": "Elimina ({count})",

+ 12 - 13
messages/it/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Layout fattura",
     "title": "Layout fattura",
     "description": "Configura cosa appare sulle fatture PDF generate.",
     "description": "Configura cosa appare sulle fatture PDF generate.",
+    "numberingTitle": "Numerazione e scadenze",
+    "numberingDescription": "Come vengono numerati fatture e preventivi, quando scadono le fatture e per quanto tempo restano validi i preventivi.",
+    "documentsTitle": "Documenti",
+    "documentsDescription": "Ciò che ogni fattura e preventivo porta con sé.",
     "tabs": {
     "tabs": {
       "general": "Generale",
       "general": "Generale",
       "layout": "Layout",
       "layout": "Layout",
@@ -550,12 +554,6 @@
     "customFooter": "Pie di pagina fattura personalizzato",
     "customFooter": "Pie di pagina fattura personalizzato",
     "footerPlaceholder": "Grazie per la vostra fiducia!",
     "footerPlaceholder": "Grazie per la vostra fiducia!",
     "footerHint": "Questo testo appare in fondo a ogni fattura",
     "footerHint": "Questo testo appare in fondo a ogni fattura",
-    "partsMarkupTitle": "Ricarico Ricambi",
-    "partsMarkupDescription": "Ricarico applicato al costo del fornitore quando si aggiungono ricambi. Solo interno — il cliente non vede mai il costo o il ricarico, solo il prezzo finale.",
-    "defaultMarkupPercent": "Ricarico Predefinito %",
-    "defaultMarkupPercentHint": "I nuovi ricambi aggiunti manualmente vengono precompilati con questo ricarico. Lasciare a 0 per inserire i prezzi manualmente.",
-    "markupAppliesToInventory": "Applica ricarico ai ricambi a magazzino",
-    "markupAppliesToInventoryHint": "Quando attivato, i ricambi selezionati dal magazzino utilizzano anche costo + ricarico predefinito (sovrascrive il prezzo di vendita del magazzino).",
     "saveInvoice": "Salva impostazioni fattura",
     "saveInvoice": "Salva impostazioni fattura",
     "saved": "Impostazioni fattura salvate",
     "saved": "Impostazioni fattura salvate",
     "layoutDescription": "Personalizza l'ordine e la visibilità delle sezioni nelle tue fatture e preventivi.",
     "layoutDescription": "Personalizza l'ordine e la visibilità delle sezioni nelle tue fatture e preventivi.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Preventivo",
     "layoutDocQuote": "Preventivo",
     "saveLayout": "Salva layout",
     "saveLayout": "Salva layout",
     "resetLayout": "Ripristina",
     "resetLayout": "Ripristina",
-    "sectionCustomers": "Clienti",
-    "assignCustomerNumbers": "Assegna numeri cliente",
-    "assignCustomerNumbersHint": "{count} clienti non hanno ancora un numero. Assegna numeri sequenziali (quelli esistenti restano invariati).",
-    "customerNumbersAssigned": "Numeri assegnati a {count} clienti",
-    "allCustomersNumbered": "Tutti i clienti hanno un numero.",
-    "assignCustomerNumbersConfirmTitle": "Assegnare i numeri cliente?",
-    "assignCustomerNumbersConfirmDescription": "I {count} clienti senza numero riceveranno numeri sequenziali nell'ordine di creazione, proseguendo dal numero esistente più alto. I clienti che hanno già un numero non vengono modificati. I numeri restano modificabili.",
     "layoutColorsHint": "Colori, caratteri e stile dell’intestazione sono in Modelli.",
     "layoutColorsHint": "Colori, caratteri e stile dell’intestazione sono in Modelli.",
     "goToTemplates": "Apri Modelli",
     "goToTemplates": "Apri Modelli",
     "lockTitle": "Blocco dei documenti conclusi",
     "lockTitle": "Blocco dei documenti conclusi",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Viene bloccato: ricambi, manodopera, quantità, prezzi, sconto, imposta, numero e data del documento. Anche l’eliminazione è impedita.",
     "lockWhatItDoes": "Viene bloccato: ricambi, manodopera, quantità, prezzi, sconto, imposta, numero e data del documento. Anche l’eliminazione è impedita.",
     "lockWhatItAllows": "Continua a funzionare: registrare pagamenti, pagamento online con carta, cambi di stato, invio e reinvio, allegati e note interne. Una fattura bloccata resta sempre pagabile.",
     "lockWhatItAllows": "Continua a funzionare: registrare pagamenti, pagamento online con carta, cambi di stato, invio e reinvio, allegati e note interne. Una fattura bloccata resta sempre pagabile.",
     "lockUnlockNote": "Titolari e amministratori possono sbloccare un singolo documento dalla sua pagina quando serve davvero una correzione. Ogni sblocco viene registrato nel log di audit.",
     "lockUnlockNote": "Titolari e amministratori possono sbloccare un singolo documento dalla sua pagina quando serve davvero una correzione. Ogni sblocco viene registrato nel log di audit.",
+    "lockDetailsShow": "Cosa fa il blocco",
+    "lockDetailsHide": "Nascondi",
     "lockInvoicesLabel": "Blocca le fatture",
     "lockInvoicesLabel": "Blocca le fatture",
     "lockTriggerSent": "Quando la fattura è inviata",
     "lockTriggerSent": "Quando la fattura è inviata",
     "lockTriggerPaid": "Quando la fattura è saldata per intero",
     "lockTriggerPaid": "Quando la fattura è saldata per intero",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Aggiungi nuovo",
     "addNewTech": "+ Aggiungi nuovo",
     "defaultLaborRate": "Tariffa manodopera predefinita (all'ora)",
     "defaultLaborRate": "Tariffa manodopera predefinita (all'ora)",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
+    "partsMarkupTitle": "Ricarico Ricambi",
+    "partsMarkupDescription": "Ricarico applicato al costo del fornitore quando si aggiungono ricambi. Solo interno. Il cliente non vede mai il costo o il ricarico, solo il prezzo finale.",
+    "defaultMarkupPercent": "Ricarico Predefinito %",
+    "defaultMarkupPercentHint": "I nuovi ricambi aggiunti manualmente vengono precompilati con questo ricarico. Lasciare a 0 per inserire i prezzi manualmente.",
+    "markupAppliesToInventory": "Applica ricarico ai ricambi a magazzino",
+    "markupAppliesToInventoryHint": "Quando attivato, i ricambi selezionati dal magazzino utilizzano anche costo + ricarico predefinito (sovrascrive il prezzo di vendita del magazzino).",
     "unitsTitle": "Unita",
     "unitsTitle": "Unita",
     "unitsDescription": "Scegli tra sistema metrico (km, litri) e imperiale (miglia, galloni).",
     "unitsDescription": "Scegli tra sistema metrico (km, litri) e imperiale (miglia, galloni).",
     "unitSystem": "Sistema di unita",
     "unitSystem": "Sistema di unita",

+ 4 - 0
messages/lt/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Nepavyko ištrinti kliento",
     "deleteError": "Nepavyko ištrinti kliento",
     "error": "Nepavyko įkelti klientų",
     "error": "Nepavyko įkelti klientų",
     "importCustomers": "Importuoti",
     "importCustomers": "Importuoti",
+    "assignNumbers": "Priskirti numerius",
+    "assignNumbersTitle": "Priskirti klientų numerius?",
+    "assignNumbersDescription": "{count, plural, one {# klientas dar neturi} few {# klientai dar neturi} many {# klientų dar neturi} other {# klientų dar neturi}} numerio. Kiekvienas gauna kitą numerį iš eilės, seniausi pirmi. Klientai, kurie jau turi numerį, nekeičiami.",
+    "assignNumbersDone": "Sunumeruota klientų: {count}",
     "selectedCount": "{count} pasirinkta",
     "selectedCount": "{count} pasirinkta",
     "clearSelection": "Valyti",
     "clearSelection": "Valyti",
     "batchDelete": "Ištrinti ({count})",
     "batchDelete": "Ištrinti ({count})",

+ 12 - 13
messages/lt/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Sąskaitos ir pasiūlymai",
     "title": "Sąskaitos ir pasiūlymai",
     "description": "Konfigūruokite sąskaitų nustatymus, išdėstymą ir pasirinktinius laukus.",
     "description": "Konfigūruokite sąskaitų nustatymus, išdėstymą ir pasirinktinius laukus.",
+    "numberingTitle": "Numeracija ir terminai",
+    "numberingDescription": "Kaip numeruojamos sąskaitos ir pasiūlymai, kada sąskaitos tampa mokėtinos ir kiek laiko galioja pasiūlymai.",
+    "documentsTitle": "Dokumentai",
+    "documentsDescription": "Ką kiekviena sąskaita ir pasiūlymas turi su savimi.",
     "tabs": {
     "tabs": {
       "general": "Bendra",
       "general": "Bendra",
       "layout": "Išdėstymas",
       "layout": "Išdėstymas",
@@ -550,12 +554,6 @@
     "customFooter": "Pasirinktinė sąskaitos poraštė",
     "customFooter": "Pasirinktinė sąskaitos poraštė",
     "footerPlaceholder": "Dėkojame už jūsų pasitikėjimą!",
     "footerPlaceholder": "Dėkojame už jūsų pasitikėjimą!",
     "footerHint": "Šis tekstas rodomas kiekvienos sąskaitos apačioje",
     "footerHint": "Šis tekstas rodomas kiekvienos sąskaitos apačioje",
-    "partsMarkupTitle": "Dalių Antkainis",
-    "partsMarkupDescription": "Antkainis taikomas tiekėjo savikainai pridedant dalis. Tik vidinis — klientas niekada nemato savikainos ar antkainio, tik galutinę kainą.",
-    "defaultMarkupPercent": "Numatytasis Antkainis %",
-    "defaultMarkupPercentHint": "Naujos rankiniu būdu pridėtos dalys užpildomos šiuo antkainiu. Palikite 0, kad įvestumėte kainas rankiniu būdu.",
-    "markupAppliesToInventory": "Taikyti antkainį sandėlio dalims",
-    "markupAppliesToInventoryHint": "Įjungus, dalys parinktos iš sandėlio taip pat naudoja savikainą + numatytąjį antkainį (perrašo sandėlio pardavimo kainą).",
     "saveInvoice": "Išsaugoti sąskaitos nustatymus",
     "saveInvoice": "Išsaugoti sąskaitos nustatymus",
     "saved": "Sąskaitos nustatymai išsaugoti",
     "saved": "Sąskaitos nustatymai išsaugoti",
     "layoutDescription": "Pritaikykite sekcijų tvarką ir matomumą sąskaitose ir pasiūlymuose.",
     "layoutDescription": "Pritaikykite sekcijų tvarką ir matomumą sąskaitose ir pasiūlymuose.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Pasiūlymas",
     "layoutDocQuote": "Pasiūlymas",
     "saveLayout": "Išsaugoti išdėstymą",
     "saveLayout": "Išsaugoti išdėstymą",
     "resetLayout": "Atstatyti numatytuosius",
     "resetLayout": "Atstatyti numatytuosius",
-    "sectionCustomers": "Klientai",
-    "assignCustomerNumbers": "Priskirti klientų numerius",
-    "assignCustomerNumbersHint": "{count} klientai dar neturi numerio. Priskirti eilės numerius (esami išlieka).",
-    "customerNumbersAssigned": "Numeriai priskirti {count} klientams",
-    "allCustomersNumbered": "Visi klientai turi numerį.",
-    "assignCustomerNumbersConfirmTitle": "Priskirti klientų numerius?",
-    "assignCustomerNumbersConfirmDescription": "{count} klientams be numerio bus priskirti eilės numeriai sukūrimo tvarka, tęsiant nuo didžiausio esamo numerio. Klientai, jau turintys numerį, nekeičiami. Numerius galima redaguoti vėliau.",
     "layoutColorsHint": "Spalvos, šriftai ir antraštės stilius yra skiltyje Šablonai.",
     "layoutColorsHint": "Spalvos, šriftai ir antraštės stilius yra skiltyje Šablonai.",
     "goToTemplates": "Atverti šablonus",
     "goToTemplates": "Atverti šablonus",
     "lockTitle": "Užbaigtų dokumentų užrakinimas",
     "lockTitle": "Užbaigtų dokumentų užrakinimas",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Užrakinama: dalys, darbas, kiekiai, kainos, nuolaida, mokestis, dokumento numeris ir data. Dokumento ištrynimas taip pat blokuojamas.",
     "lockWhatItDoes": "Užrakinama: dalys, darbas, kiekiai, kainos, nuolaida, mokestis, dokumento numeris ir data. Dokumento ištrynimas taip pat blokuojamas.",
     "lockWhatItAllows": "Toliau veikia: mokėjimų registravimas, mokėjimas kortele internetu, būsenos keitimas, siuntimas ir pakartotinis siuntimas, priedai ir vidinės pastabos. Užrakintą sąskaitą visada galima apmokėti.",
     "lockWhatItAllows": "Toliau veikia: mokėjimų registravimas, mokėjimas kortele internetu, būsenos keitimas, siuntimas ir pakartotinis siuntimas, priedai ir vidinės pastabos. Užrakintą sąskaitą visada galima apmokėti.",
     "lockUnlockNote": "Savininkai ir administratoriai gali atrakinti atskirą dokumentą jo paties puslapyje, kai kažką tikrai reikia pataisyti. Kiekvienas atrakinimas įrašomas į audito žurnalą.",
     "lockUnlockNote": "Savininkai ir administratoriai gali atrakinti atskirą dokumentą jo paties puslapyje, kai kažką tikrai reikia pataisyti. Kiekvienas atrakinimas įrašomas į audito žurnalą.",
+    "lockDetailsShow": "Ką daro užrakinimas",
+    "lockDetailsHide": "Slėpti",
     "lockInvoicesLabel": "Užrakinti sąskaitas",
     "lockInvoicesLabel": "Užrakinti sąskaitas",
     "lockTriggerSent": "Kai sąskaita išsiųsta",
     "lockTriggerSent": "Kai sąskaita išsiųsta",
     "lockTriggerPaid": "Kai sąskaita visiškai apmokėta",
     "lockTriggerPaid": "Kai sąskaita visiškai apmokėta",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Pridėti naują",
     "addNewTech": "+ Pridėti naują",
     "defaultLaborRate": "Numatytasis darbo tarifas (per valandą)",
     "defaultLaborRate": "Numatytasis darbo tarifas (per valandą)",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
+    "partsMarkupTitle": "Dalių Antkainis",
+    "partsMarkupDescription": "Antkainis taikomas tiekėjo savikainai pridedant dalis. Tik vidinis. Klientas niekada nemato savikainos ar antkainio, tik galutinę kainą.",
+    "defaultMarkupPercent": "Numatytasis Antkainis %",
+    "defaultMarkupPercentHint": "Naujos rankiniu būdu pridėtos dalys užpildomos šiuo antkainiu. Palikite 0, kad įvestumėte kainas rankiniu būdu.",
+    "markupAppliesToInventory": "Taikyti antkainį sandėlio dalims",
+    "markupAppliesToInventoryHint": "Įjungus, dalys parinktos iš sandėlio taip pat naudoja savikainą + numatytąjį antkainį (perrašo sandėlio pardavimo kainą).",
     "unitsTitle": "Vienetai",
     "unitsTitle": "Vienetai",
     "unitsDescription": "Pasirinkite tarp metrinės (km, litrai) ir imperinės (mylios, galonai) sistemos.",
     "unitsDescription": "Pasirinkite tarp metrinės (km, litrai) ir imperinės (mylios, galonai) sistemos.",
     "unitSystem": "Vienetų sistema",
     "unitSystem": "Vienetų sistema",

+ 4 - 0
messages/nb/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Kunne ikke slette kunde",
     "deleteError": "Kunne ikke slette kunde",
     "error": "Kunne ikke laste inn kunder",
     "error": "Kunne ikke laste inn kunder",
     "importCustomers": "Importer",
     "importCustomers": "Importer",
+    "assignNumbers": "Tildel numre",
+    "assignNumbersTitle": "Tildele kundenumre?",
+    "assignNumbersDescription": "{count, plural, one {# kunde har} other {# kunder har}} ikke noe nummer ennå. Hver får neste nummer i rekken, eldste først. Kunder som allerede har et nummer røres ikke.",
+    "assignNumbersDone": "{count, plural, one {# kunde} other {# kunder}} fikk nummer",
     "selectedCount": "{count} valgt",
     "selectedCount": "{count} valgt",
     "clearSelection": "Fjern valg",
     "clearSelection": "Fjern valg",
     "batchDelete": "Slett ({count})",
     "batchDelete": "Slett ({count})",

+ 12 - 13
messages/nb/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Faktura og tilbud",
     "title": "Faktura og tilbud",
     "description": "Konfigurer fakturainnstillinger, layout og egendefinerte felter.",
     "description": "Konfigurer fakturainnstillinger, layout og egendefinerte felter.",
+    "numberingTitle": "Nummerering og frister",
+    "numberingDescription": "Hvordan fakturaer og tilbud nummereres, når fakturaer forfaller og hvor lenge tilbud gjelder.",
+    "documentsTitle": "Dokumenter",
+    "documentsDescription": "Det hver faktura og hvert tilbud har med seg.",
     "tabs": {
     "tabs": {
       "general": "Generelt",
       "general": "Generelt",
       "layout": "Oppsett",
       "layout": "Oppsett",
@@ -550,12 +554,6 @@
     "customFooter": "Egendefinert fakturabunnstykke",
     "customFooter": "Egendefinert fakturabunnstykke",
     "footerPlaceholder": "Takk for handelen!",
     "footerPlaceholder": "Takk for handelen!",
     "footerHint": "Denne teksten vises nederst pa hver faktura",
     "footerHint": "Denne teksten vises nederst pa hver faktura",
-    "partsMarkupTitle": "Delepåslag",
-    "partsMarkupDescription": "Påslag på innkjøpspris når deler legges til. Kun internt — kunden ser aldri kostnad eller påslag, bare sluttpris.",
-    "defaultMarkupPercent": "Standard påslag %",
-    "defaultMarkupPercentHint": "Nye manuelt lagt til deler fylles ut med dette påslaget. La stå på 0 for å skrive priser manuelt.",
-    "markupAppliesToInventory": "Bruk påslag på lagerdeler",
-    "markupAppliesToInventoryHint": "Når aktivert, bruker deler valgt fra lager også kostnad + standardpåslag (overstyrer lagerets salgspris).",
     "saveInvoice": "Lagre fakturainnstillinger",
     "saveInvoice": "Lagre fakturainnstillinger",
     "saved": "Fakturainnstillinger lagret",
     "saved": "Fakturainnstillinger lagret",
     "layoutDescription": "Tilpass rekkefølgen og synligheten av seksjoner i fakturaer og tilbud.",
     "layoutDescription": "Tilpass rekkefølgen og synligheten av seksjoner i fakturaer og tilbud.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Tilbud",
     "layoutDocQuote": "Tilbud",
     "saveLayout": "Lagre oppsett",
     "saveLayout": "Lagre oppsett",
     "resetLayout": "Tilbakestill",
     "resetLayout": "Tilbakestill",
-    "sectionCustomers": "Kunder",
-    "assignCustomerNumbers": "Tildel kundenumre",
-    "assignCustomerNumbersHint": "{count} kunder mangler nummer. Tildel fortløpende numre (eksisterende beholdes).",
-    "customerNumbersAssigned": "Tildelte numre til {count} kunder",
-    "allCustomersNumbered": "Alle kunder har et nummer.",
-    "assignCustomerNumbersConfirmTitle": "Tildele kundenumre?",
-    "assignCustomerNumbersConfirmDescription": "De {count} kundene uten nummer får fortløpende numre i opprettelsesrekkefølge, fortsettende etter det høyeste eksisterende nummeret. Kunder som allerede har nummer endres ikke. Numrene kan redigeres etterpå.",
     "layoutColorsHint": "Farger, skrifter og brevhodestilen ligger under Maler.",
     "layoutColorsHint": "Farger, skrifter og brevhodestilen ligger under Maler.",
     "goToTemplates": "Åpne Maler",
     "goToTemplates": "Åpne Maler",
     "lockTitle": "Låsing av ferdige dokumenter",
     "lockTitle": "Låsing av ferdige dokumenter",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Dette låses: deler, arbeid, antall, priser, rabatt, avgift, dokumentnummer og dato. Sletting av dokumentet blokkeres også.",
     "lockWhatItDoes": "Dette låses: deler, arbeid, antall, priser, rabatt, avgift, dokumentnummer og dato. Sletting av dokumentet blokkeres også.",
     "lockWhatItAllows": "Dette virker fortsatt: registrere betalinger, kortbetaling på nett, statusendringer, sending og ny sending, vedlegg og interne notater. En låst faktura kan alltid betales.",
     "lockWhatItAllows": "Dette virker fortsatt: registrere betalinger, kortbetaling på nett, statusendringer, sending og ny sending, vedlegg og interne notater. En låst faktura kan alltid betales.",
     "lockUnlockNote": "Eiere og administratorer kan låse opp ett enkelt dokument fra dokumentets egen side når noe virkelig må rettes. Hver opplåsing loggføres.",
     "lockUnlockNote": "Eiere og administratorer kan låse opp ett enkelt dokument fra dokumentets egen side når noe virkelig må rettes. Hver opplåsing loggføres.",
+    "lockDetailsShow": "Hva låsing gjør",
+    "lockDetailsHide": "Skjul",
     "lockInvoicesLabel": "Lås fakturaer",
     "lockInvoicesLabel": "Lås fakturaer",
     "lockTriggerSent": "Når fakturaen er sendt",
     "lockTriggerSent": "Når fakturaen er sendt",
     "lockTriggerPaid": "Når fakturaen er betalt i sin helhet",
     "lockTriggerPaid": "Når fakturaen er betalt i sin helhet",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Legg til ny",
     "addNewTech": "+ Legg til ny",
     "defaultLaborRate": "Standard timesats",
     "defaultLaborRate": "Standard timesats",
     "laborRatePlaceholder": "750",
     "laborRatePlaceholder": "750",
+    "partsMarkupTitle": "Delepåslag",
+    "partsMarkupDescription": "Påslag på innkjøpspris når deler legges til. Kun internt. Kunden ser aldri kostnad eller påslag, bare sluttpris.",
+    "defaultMarkupPercent": "Standard påslag %",
+    "defaultMarkupPercentHint": "Nye manuelt lagt til deler fylles ut med dette påslaget. La stå på 0 for å skrive priser manuelt.",
+    "markupAppliesToInventory": "Bruk påslag på lagerdeler",
+    "markupAppliesToInventoryHint": "Når aktivert, bruker deler valgt fra lager også kostnad + standardpåslag (overstyrer lagerets salgspris).",
     "unitsTitle": "Enheter",
     "unitsTitle": "Enheter",
     "unitsDescription": "Velg mellom metrisk (km, liter) og imperial (miles, gallons).",
     "unitsDescription": "Velg mellom metrisk (km, liter) og imperial (miles, gallons).",
     "unitSystem": "Enhetssystem",
     "unitSystem": "Enhetssystem",

+ 4 - 0
messages/nl/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Verwijderen van klant mislukt",
     "deleteError": "Verwijderen van klant mislukt",
     "error": "Laden van klanten mislukt",
     "error": "Laden van klanten mislukt",
     "importCustomers": "Importeren",
     "importCustomers": "Importeren",
+    "assignNumbers": "Nummers toewijzen",
+    "assignNumbersTitle": "Klantnummers toewijzen?",
+    "assignNumbersDescription": "{count, plural, one {# klant heeft} other {# klanten hebben}} nog geen nummer. Elk krijgt het volgende nummer in de reeks, oudste eerst. Klanten die al een nummer hebben blijven ongewijzigd.",
+    "assignNumbersDone": "{count, plural, one {# klant} other {# klanten}} genummerd",
     "selectedCount": "{count} geselecteerd",
     "selectedCount": "{count} geselecteerd",
     "clearSelection": "Wissen",
     "clearSelection": "Wissen",
     "batchDelete": "Verwijderen ({count})",
     "batchDelete": "Verwijderen ({count})",

+ 12 - 13
messages/nl/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Factuurindeling",
     "title": "Factuurindeling",
     "description": "Configureer wat er op gegenereerde PDF-facturen verschijnt.",
     "description": "Configureer wat er op gegenereerde PDF-facturen verschijnt.",
+    "numberingTitle": "Nummering en termijnen",
+    "numberingDescription": "Hoe facturen en offertes worden genummerd, wanneer facturen vervallen en hoe lang offertes geldig blijven.",
+    "documentsTitle": "Documenten",
+    "documentsDescription": "Wat elke factuur en offerte met zich meebrengt.",
     "tabs": {
     "tabs": {
       "general": "Algemeen",
       "general": "Algemeen",
       "layout": "Layout",
       "layout": "Layout",
@@ -550,12 +554,6 @@
     "customFooter": "Aangepaste factuurvoettekst",
     "customFooter": "Aangepaste factuurvoettekst",
     "footerPlaceholder": "Bedankt voor uw vertrouwen!",
     "footerPlaceholder": "Bedankt voor uw vertrouwen!",
     "footerHint": "Deze tekst verschijnt onderaan elke factuur",
     "footerHint": "Deze tekst verschijnt onderaan elke factuur",
-    "partsMarkupTitle": "Onderdelenmarge",
-    "partsMarkupDescription": "Marge toegepast op inkoopprijs bij het toevoegen van onderdelen. Alleen intern — de klant ziet nooit de inkoopprijs of de marge, alleen de eindprijs.",
-    "defaultMarkupPercent": "Standaardmarge %",
-    "defaultMarkupPercentHint": "Nieuwe handmatig toegevoegde onderdelen worden voorgevuld met deze marge. Laat op 0 staan om prijzen handmatig in te voeren.",
-    "markupAppliesToInventory": "Marge toepassen op voorraadonderdelen",
-    "markupAppliesToInventoryHint": "Indien ingeschakeld gebruiken onderdelen uit de voorraad ook inkoop + standaardmarge (overschrijft de verkoopprijs van de voorraad).",
     "saveInvoice": "Factuurinstellingen opslaan",
     "saveInvoice": "Factuurinstellingen opslaan",
     "saved": "Factuurinstellingen opgeslagen",
     "saved": "Factuurinstellingen opgeslagen",
     "layoutDescription": "Pas de volgorde en zichtbaarheid van secties in uw facturen en offertes aan.",
     "layoutDescription": "Pas de volgorde en zichtbaarheid van secties in uw facturen en offertes aan.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Offerte",
     "layoutDocQuote": "Offerte",
     "saveLayout": "Layout opslaan",
     "saveLayout": "Layout opslaan",
     "resetLayout": "Herstellen",
     "resetLayout": "Herstellen",
-    "sectionCustomers": "Klanten",
-    "assignCustomerNumbers": "Klantnummers toewijzen",
-    "assignCustomerNumbersHint": "{count} klanten hebben nog geen nummer. Wijs opeenvolgende nummers toe (bestaande blijven behouden).",
-    "customerNumbersAssigned": "Nummers toegewezen aan {count} klanten",
-    "allCustomersNumbered": "Alle klanten hebben een nummer.",
-    "assignCustomerNumbersConfirmTitle": "Klantnummers toewijzen?",
-    "assignCustomerNumbersConfirmDescription": "De {count} klanten zonder nummer krijgen opeenvolgende nummers in volgorde van aanmaak, doorlopend na het hoogste bestaande nummer. Klanten met een nummer worden niet gewijzigd. Nummers zijn achteraf te bewerken.",
     "layoutColorsHint": "Kleuren, lettertypen en de briefhoofdstijl staan bij Sjablonen.",
     "layoutColorsHint": "Kleuren, lettertypen en de briefhoofdstijl staan bij Sjablonen.",
     "goToTemplates": "Sjablonen openen",
     "goToTemplates": "Sjablonen openen",
     "lockTitle": "Afgeronde documenten vergrendelen",
     "lockTitle": "Afgeronde documenten vergrendelen",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Wat wordt vergrendeld: onderdelen, arbeid, aantallen, prijzen, korting, btw, documentnummer en datum. Verwijderen wordt ook geblokkeerd.",
     "lockWhatItDoes": "Wat wordt vergrendeld: onderdelen, arbeid, aantallen, prijzen, korting, btw, documentnummer en datum. Verwijderen wordt ook geblokkeerd.",
     "lockWhatItAllows": "Wat blijft werken: betalingen vastleggen, online kaartbetaling, statuswijzigingen, versturen en opnieuw versturen, bijlagen en interne notities. Een vergrendelde factuur blijft altijd betaalbaar.",
     "lockWhatItAllows": "Wat blijft werken: betalingen vastleggen, online kaartbetaling, statuswijzigingen, versturen en opnieuw versturen, bijlagen en interne notities. Een vergrendelde factuur blijft altijd betaalbaar.",
     "lockUnlockNote": "Eigenaren en beheerders kunnen één document ontgrendelen vanaf de pagina van dat document wanneer er echt iets gecorrigeerd moet worden. Elke ontgrendeling komt in het auditlogboek.",
     "lockUnlockNote": "Eigenaren en beheerders kunnen één document ontgrendelen vanaf de pagina van dat document wanneer er echt iets gecorrigeerd moet worden. Elke ontgrendeling komt in het auditlogboek.",
+    "lockDetailsShow": "Wat vergrendelen doet",
+    "lockDetailsHide": "Verbergen",
     "lockInvoicesLabel": "Facturen vergrendelen",
     "lockInvoicesLabel": "Facturen vergrendelen",
     "lockTriggerSent": "Zodra de factuur is verstuurd",
     "lockTriggerSent": "Zodra de factuur is verstuurd",
     "lockTriggerPaid": "Zodra de factuur volledig is betaald",
     "lockTriggerPaid": "Zodra de factuur volledig is betaald",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Nieuw toevoegen",
     "addNewTech": "+ Nieuw toevoegen",
     "defaultLaborRate": "Standaard uurtarief",
     "defaultLaborRate": "Standaard uurtarief",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
+    "partsMarkupTitle": "Onderdelenmarge",
+    "partsMarkupDescription": "Marge toegepast op inkoopprijs bij het toevoegen van onderdelen. Alleen intern. De klant ziet nooit de inkoopprijs of de marge, alleen de eindprijs.",
+    "defaultMarkupPercent": "Standaardmarge %",
+    "defaultMarkupPercentHint": "Nieuwe handmatig toegevoegde onderdelen worden voorgevuld met deze marge. Laat op 0 staan om prijzen handmatig in te voeren.",
+    "markupAppliesToInventory": "Marge toepassen op voorraadonderdelen",
+    "markupAppliesToInventoryHint": "Indien ingeschakeld gebruiken onderdelen uit de voorraad ook inkoop + standaardmarge (overschrijft de verkoopprijs van de voorraad).",
     "unitsTitle": "Eenheden",
     "unitsTitle": "Eenheden",
     "unitsDescription": "Kies tussen metrisch (km, liters) en imperiaal (mijlen, gallons).",
     "unitsDescription": "Kies tussen metrisch (km, liters) en imperiaal (mijlen, gallons).",
     "unitSystem": "Eenhedensysteem",
     "unitSystem": "Eenhedensysteem",

+ 4 - 0
messages/pl/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Nie udało się usunąć klienta",
     "deleteError": "Nie udało się usunąć klienta",
     "error": "Nie udało się załadować klientów",
     "error": "Nie udało się załadować klientów",
     "importCustomers": "Importuj",
     "importCustomers": "Importuj",
+    "assignNumbers": "Nadaj numery",
+    "assignNumbersTitle": "Nadać numery klientów?",
+    "assignNumbersDescription": "{count, plural, one {# klient nie ma} few {# klientów nie ma} many {# klientów nie ma} other {# klientów nie ma}} jeszcze numeru. Każdy otrzyma kolejny numer w sekwencji, najstarsi jako pierwsi. Klienci, którzy już mają numer, pozostaną bez zmian.",
+    "assignNumbersDone": "Nadano numery: {count}",
     "selectedCount": "{count} zaznaczono",
     "selectedCount": "{count} zaznaczono",
     "clearSelection": "Wyczyść",
     "clearSelection": "Wyczyść",
     "batchDelete": "Usuń ({count})",
     "batchDelete": "Usuń ({count})",

+ 12 - 13
messages/pl/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Układ faktury",
     "title": "Układ faktury",
     "description": "Skonfiguruj, co pojawi się na generowanych fakturach PDF.",
     "description": "Skonfiguruj, co pojawi się na generowanych fakturach PDF.",
+    "numberingTitle": "Numeracja i terminy",
+    "numberingDescription": "Jak numerowane są faktury i wyceny, kiedy faktury stają się wymagalne i jak długo wyceny są ważne.",
+    "documentsTitle": "Dokumenty",
+    "documentsDescription": "Co zawiera każda faktura i wycena.",
     "tabs": {
     "tabs": {
       "general": "Ogólne",
       "general": "Ogólne",
       "layout": "Układ",
       "layout": "Układ",
@@ -550,12 +554,6 @@
     "customFooter": "Niestandardowa stopka faktury",
     "customFooter": "Niestandardowa stopka faktury",
     "footerPlaceholder": "Dziękujemy za zamowienie!",
     "footerPlaceholder": "Dziękujemy za zamowienie!",
     "footerHint": "Ten tekst pojawi się na dole każdej faktury",
     "footerHint": "Ten tekst pojawi się na dole każdej faktury",
-    "partsMarkupTitle": "Marża na Części",
-    "partsMarkupDescription": "Marża doliczana do ceny zakupu przy dodawaniu części. Tylko wewnętrznie — klient nigdy nie widzi kosztu ani marży, tylko cenę końcową.",
-    "defaultMarkupPercent": "Domyślna Marża %",
-    "defaultMarkupPercentHint": "Nowe ręcznie dodane części są wstępnie wypełniane tą marżą. Pozostaw 0, aby wprowadzać ceny ręcznie.",
-    "markupAppliesToInventory": "Zastosuj marżę do części z magazynu",
-    "markupAppliesToInventoryHint": "Gdy włączone, części wybrane z magazynu również używają kosztu + domyślnej marży (zastępuje cenę sprzedaży z magazynu).",
     "saveInvoice": "Zapisz ustawienia faktury",
     "saveInvoice": "Zapisz ustawienia faktury",
     "saved": "Ustawienia faktury zapisane",
     "saved": "Ustawienia faktury zapisane",
     "layoutDescription": "Dostosuj kolejność i widoczność sekcji w fakturach i wycenach.",
     "layoutDescription": "Dostosuj kolejność i widoczność sekcji w fakturach i wycenach.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Wycena",
     "layoutDocQuote": "Wycena",
     "saveLayout": "Zapisz układ",
     "saveLayout": "Zapisz układ",
     "resetLayout": "Resetuj",
     "resetLayout": "Resetuj",
-    "sectionCustomers": "Klienci",
-    "assignCustomerNumbers": "Przydziel numery klientów",
-    "assignCustomerNumbersHint": "{count} klientów nie ma jeszcze numeru. Przydziel im kolejne numery (istniejące pozostają).",
-    "customerNumbersAssigned": "Przydzielono numery {count} klientom",
-    "allCustomersNumbered": "Wszyscy klienci mają numer.",
-    "assignCustomerNumbersConfirmTitle": "Przydzielić numery klientów?",
-    "assignCustomerNumbersConfirmDescription": "{count} klientów bez numeru otrzyma kolejne numery w kolejności utworzenia, kontynuując od najwyższego istniejącego numeru. Klienci z numerem nie zostaną zmienieni. Numery można później edytować.",
     "layoutColorsHint": "Kolory, czcionki i styl nagłówka są w Szablonach.",
     "layoutColorsHint": "Kolory, czcionki i styl nagłówka są w Szablonach.",
     "goToTemplates": "Otwórz Szablony",
     "goToTemplates": "Otwórz Szablony",
     "lockTitle": "Blokowanie zakończonych dokumentów",
     "lockTitle": "Blokowanie zakończonych dokumentów",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Blokowane są: części, robocizna, ilości, ceny, rabat, podatek, numer i data dokumentu. Usunięcie dokumentu również jest zablokowane.",
     "lockWhatItDoes": "Blokowane są: części, robocizna, ilości, ceny, rabat, podatek, numer i data dokumentu. Usunięcie dokumentu również jest zablokowane.",
     "lockWhatItAllows": "Nadal działa: rejestrowanie płatności, płatność kartą online, zmiany statusu, wysyłka i ponowna wysyłka, załączniki i notatki wewnętrzne. Zablokowaną fakturę zawsze można opłacić.",
     "lockWhatItAllows": "Nadal działa: rejestrowanie płatności, płatność kartą online, zmiany statusu, wysyłka i ponowna wysyłka, załączniki i notatki wewnętrzne. Zablokowaną fakturę zawsze można opłacić.",
     "lockUnlockNote": "Właściciele i administratorzy mogą odblokować pojedynczy dokument z jego własnej strony, gdy naprawdę wymaga poprawki. Każde odblokowanie trafia do dziennika audytu.",
     "lockUnlockNote": "Właściciele i administratorzy mogą odblokować pojedynczy dokument z jego własnej strony, gdy naprawdę wymaga poprawki. Każde odblokowanie trafia do dziennika audytu.",
+    "lockDetailsShow": "Co robi blokada",
+    "lockDetailsHide": "Ukryj",
     "lockInvoicesLabel": "Blokuj faktury",
     "lockInvoicesLabel": "Blokuj faktury",
     "lockTriggerSent": "Po wysłaniu faktury",
     "lockTriggerSent": "Po wysłaniu faktury",
     "lockTriggerPaid": "Po opłaceniu faktury w całości",
     "lockTriggerPaid": "Po opłaceniu faktury w całości",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Dodaj nowego",
     "addNewTech": "+ Dodaj nowego",
     "defaultLaborRate": "Domyślna stawka za godzinę",
     "defaultLaborRate": "Domyślna stawka za godzinę",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
+    "partsMarkupTitle": "Marża na Części",
+    "partsMarkupDescription": "Marża doliczana do ceny zakupu przy dodawaniu części. Tylko wewnętrznie. Klient nigdy nie widzi kosztu ani marży, tylko cenę końcową.",
+    "defaultMarkupPercent": "Domyślna Marża %",
+    "defaultMarkupPercentHint": "Nowe ręcznie dodane części są wstępnie wypełniane tą marżą. Pozostaw 0, aby wprowadzać ceny ręcznie.",
+    "markupAppliesToInventory": "Zastosuj marżę do części z magazynu",
+    "markupAppliesToInventoryHint": "Gdy włączone, części wybrane z magazynu również używają kosztu + domyślnej marży (zastępuje cenę sprzedaży z magazynu).",
     "unitsTitle": "Jednostki",
     "unitsTitle": "Jednostki",
     "unitsDescription": "Wybierz między metrycznym (km, litry) a imperialnym (mile, galony).",
     "unitsDescription": "Wybierz między metrycznym (km, litry) a imperialnym (mile, galony).",
     "unitSystem": "System jednostek",
     "unitSystem": "System jednostek",

+ 4 - 0
messages/pt-BR/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Falha ao excluir cliente",
     "deleteError": "Falha ao excluir cliente",
     "error": "Falha ao carregar clientes",
     "error": "Falha ao carregar clientes",
     "importCustomers": "Importar",
     "importCustomers": "Importar",
+    "assignNumbers": "Atribuir números",
+    "assignNumbersTitle": "Atribuir números de cliente?",
+    "assignNumbersDescription": "{count, plural, one {# cliente ainda não tem} other {# clientes ainda não têm}} número. Cada um recebe o próximo número da sequência, do mais antigo ao mais recente. Clientes que já têm número não são alterados.",
+    "assignNumbersDone": "{count, plural, one {# cliente numerado} other {# clientes numerados}}",
     "selectedCount": "{count} selecionado(s)",
     "selectedCount": "{count} selecionado(s)",
     "clearSelection": "Limpar",
     "clearSelection": "Limpar",
     "batchDelete": "Excluir ({count})",
     "batchDelete": "Excluir ({count})",

+ 12 - 13
messages/pt-BR/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Layout da fatura",
     "title": "Layout da fatura",
     "description": "Configure o que aparece nas faturas PDF geradas.",
     "description": "Configure o que aparece nas faturas PDF geradas.",
+    "numberingTitle": "Numeração e prazos",
+    "numberingDescription": "Como faturas e orçamentos são numerados, quando as faturas vencem e por quanto tempo os orçamentos ficam válidos.",
+    "documentsTitle": "Documentos",
+    "documentsDescription": "O que toda fatura e orçamento leva consigo.",
     "tabs": {
     "tabs": {
       "general": "Geral",
       "general": "Geral",
       "layout": "Layout",
       "layout": "Layout",
@@ -550,12 +554,6 @@
     "customFooter": "Rodapé personalizado da fatura",
     "customFooter": "Rodapé personalizado da fatura",
     "footerPlaceholder": "Obrigado pela preferência!",
     "footerPlaceholder": "Obrigado pela preferência!",
     "footerHint": "Este texto aparece no final de cada fatura",
     "footerHint": "Este texto aparece no final de cada fatura",
-    "partsMarkupTitle": "Margem de Peças",
-    "partsMarkupDescription": "Margem aplicada ao custo do fornecedor ao adicionar peças. Apenas interno — o cliente nunca vê o custo ou a margem, somente o preço final.",
-    "defaultMarkupPercent": "Margem Padrão %",
-    "defaultMarkupPercentHint": "Novas peças adicionadas manualmente são pré-preenchidas com esta margem. Deixe em 0 para inserir preços manualmente.",
-    "markupAppliesToInventory": "Aplicar margem a peças do estoque",
-    "markupAppliesToInventoryHint": "Quando ativado, peças selecionadas do estoque também usam custo + margem padrão (substitui o preço de venda do estoque).",
     "saveInvoice": "Salvar configurações da fatura",
     "saveInvoice": "Salvar configurações da fatura",
     "saved": "Configurações da fatura salvas",
     "saved": "Configurações da fatura salvas",
     "layoutDescription": "Personalize a ordem e visibilidade das seções em suas faturas e orçamentos.",
     "layoutDescription": "Personalize a ordem e visibilidade das seções em suas faturas e orçamentos.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Orçamento",
     "layoutDocQuote": "Orçamento",
     "saveLayout": "Salvar layout",
     "saveLayout": "Salvar layout",
     "resetLayout": "Redefinir",
     "resetLayout": "Redefinir",
-    "sectionCustomers": "Clientes",
-    "assignCustomerNumbers": "Atribuir números de cliente",
-    "assignCustomerNumbersHint": "{count} clientes ainda não têm número. Atribuir números sequenciais (os existentes são mantidos).",
-    "customerNumbersAssigned": "Números atribuídos a {count} clientes",
-    "allCustomersNumbered": "Todos os clientes têm número.",
-    "assignCustomerNumbersConfirmTitle": "Atribuir números de cliente?",
-    "assignCustomerNumbersConfirmDescription": "Os {count} clientes sem número receberão números sequenciais na ordem de criação, continuando após o maior número existente. Clientes que já têm número não são alterados. Os números podem ser editados depois.",
     "layoutColorsHint": "Cores, fontes e o estilo do timbre ficam em Modelos.",
     "layoutColorsHint": "Cores, fontes e o estilo do timbre ficam em Modelos.",
     "goToTemplates": "Abrir Modelos",
     "goToTemplates": "Abrir Modelos",
     "lockTitle": "Bloqueio de documentos concluídos",
     "lockTitle": "Bloqueio de documentos concluídos",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "O que é bloqueado: peças, mão de obra, quantidades, preços, desconto, imposto, número do documento e data. Excluir o documento também é bloqueado.",
     "lockWhatItDoes": "O que é bloqueado: peças, mão de obra, quantidades, preços, desconto, imposto, número do documento e data. Excluir o documento também é bloqueado.",
     "lockWhatItAllows": "O que continua funcionando: registrar pagamentos, pagamento online com cartão, mudanças de status, envio e reenvio, anexos e notas internas. Uma fatura bloqueada sempre pode ser paga.",
     "lockWhatItAllows": "O que continua funcionando: registrar pagamentos, pagamento online com cartão, mudanças de status, envio e reenvio, anexos e notas internas. Uma fatura bloqueada sempre pode ser paga.",
     "lockUnlockNote": "Proprietários e administradores podem desbloquear um documento específico na página dele quando algo realmente precisa ser corrigido. Todo desbloqueio fica no log de auditoria.",
     "lockUnlockNote": "Proprietários e administradores podem desbloquear um documento específico na página dele quando algo realmente precisa ser corrigido. Todo desbloqueio fica no log de auditoria.",
+    "lockDetailsShow": "O que o bloqueio faz",
+    "lockDetailsHide": "Ocultar",
     "lockInvoicesLabel": "Bloquear faturas",
     "lockInvoicesLabel": "Bloquear faturas",
     "lockTriggerSent": "Assim que a fatura for enviada",
     "lockTriggerSent": "Assim que a fatura for enviada",
     "lockTriggerPaid": "Assim que a fatura for paga integralmente",
     "lockTriggerPaid": "Assim que a fatura for paga integralmente",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Adicionar novo",
     "addNewTech": "+ Adicionar novo",
     "defaultLaborRate": "Taxa de mao de obra padrão (por hora)",
     "defaultLaborRate": "Taxa de mao de obra padrão (por hora)",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
+    "partsMarkupTitle": "Margem de Peças",
+    "partsMarkupDescription": "Margem aplicada ao custo do fornecedor ao adicionar peças. Apenas interno. O cliente nunca vê o custo ou a margem, somente o preço final.",
+    "defaultMarkupPercent": "Margem Padrão %",
+    "defaultMarkupPercentHint": "Novas peças adicionadas manualmente são pré-preenchidas com esta margem. Deixe em 0 para inserir preços manualmente.",
+    "markupAppliesToInventory": "Aplicar margem a peças do estoque",
+    "markupAppliesToInventoryHint": "Quando ativado, peças selecionadas do estoque também usam custo + margem padrão (substitui o preço de venda do estoque).",
     "unitsTitle": "Unidades",
     "unitsTitle": "Unidades",
     "unitsDescription": "Escolha entre metrico (km, litros) e imperial (milhas, galoes).",
     "unitsDescription": "Escolha entre metrico (km, litros) e imperial (milhas, galoes).",
     "unitSystem": "Sistema de unidades",
     "unitSystem": "Sistema de unidades",

+ 4 - 0
messages/ru/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Не удалось удалить клиента",
     "deleteError": "Не удалось удалить клиента",
     "error": "Не удалось загрузить клиентов",
     "error": "Не удалось загрузить клиентов",
     "importCustomers": "Импорт",
     "importCustomers": "Импорт",
+    "assignNumbers": "Присвоить номера",
+    "assignNumbersTitle": "Присвоить номера клиентам?",
+    "assignNumbersDescription": "{count, plural, one {# клиент ещё не имеет} few {# клиента ещё не имеют} many {# клиентов ещё не имеют} other {# клиентов ещё не имеют}} номера. Каждый получит следующий номер по порядку, начиная с самых старых. Клиенты с номером не затрагиваются.",
+    "assignNumbersDone": "Присвоено номеров: {count}",
     "selectedCount": "{count} выбрано",
     "selectedCount": "{count} выбрано",
     "clearSelection": "Очистить",
     "clearSelection": "Очистить",
     "batchDelete": "Удалить ({count})",
     "batchDelete": "Удалить ({count})",

+ 12 - 13
messages/ru/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Счета и предложения",
     "title": "Счета и предложения",
     "description": "Настройте параметры счетов, макет и пользовательские поля.",
     "description": "Настройте параметры счетов, макет и пользовательские поля.",
+    "numberingTitle": "Нумерация и сроки",
+    "numberingDescription": "Как нумеруются счета и предложения, когда наступает срок оплаты счетов и как долго действуют предложения.",
+    "documentsTitle": "Документы",
+    "documentsDescription": "Что сопровождает каждый счёт и предложение.",
     "tabs": {
     "tabs": {
       "general": "Общие",
       "general": "Общие",
       "layout": "Макет",
       "layout": "Макет",
@@ -550,12 +554,6 @@
     "customFooter": "Пользовательский подвал счёта",
     "customFooter": "Пользовательский подвал счёта",
     "footerPlaceholder": "Спасибо за выбор нашего сервиса!",
     "footerPlaceholder": "Спасибо за выбор нашего сервиса!",
     "footerHint": "Этот текст отображается внизу каждого счёта",
     "footerHint": "Этот текст отображается внизу каждого счёта",
-    "partsMarkupTitle": "Наценка на запчасти",
-    "partsMarkupDescription": "Наценка к закупочной цене при добавлении запчастей. Только внутреннее — клиент никогда не видит себестоимость или наценку, только итоговую цену.",
-    "defaultMarkupPercent": "Наценка по умолчанию %",
-    "defaultMarkupPercentHint": "Новые вручную добавленные запчасти заполняются этой наценкой. Оставьте 0, чтобы вводить цены вручную.",
-    "markupAppliesToInventory": "Применить наценку к запчастям со склада",
-    "markupAppliesToInventoryHint": "Если включено, запчасти, выбранные со склада, также используют себестоимость + наценку по умолчанию (переопределяет цену продажи со склада).",
     "saveInvoice": "Сохранить настройки счёта",
     "saveInvoice": "Сохранить настройки счёта",
     "saved": "Настройки счёта сохранены",
     "saved": "Настройки счёта сохранены",
     "layoutDescription": "Настройте порядок и видимость разделов в счетах и предложениях.",
     "layoutDescription": "Настройте порядок и видимость разделов в счетах и предложениях.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Предложение",
     "layoutDocQuote": "Предложение",
     "saveLayout": "Сохранить макет",
     "saveLayout": "Сохранить макет",
     "resetLayout": "Сбросить по умолчанию",
     "resetLayout": "Сбросить по умолчанию",
-    "sectionCustomers": "Клиенты",
-    "assignCustomerNumbers": "Присвоить номера клиентам",
-    "assignCustomerNumbersHint": "У {count} клиентов ещё нет номера. Присвоить им последовательные номера (существующие сохраняются).",
-    "customerNumbersAssigned": "Номера присвоены {count} клиентам",
-    "allCustomersNumbered": "У всех клиентов есть номер.",
-    "assignCustomerNumbersConfirmTitle": "Присвоить номера клиентам?",
-    "assignCustomerNumbersConfirmDescription": "{count} клиентов без номера получат последовательные номера в порядке создания, продолжая после наибольшего существующего номера. Клиенты с номером не изменяются. Номера можно редактировать позже.",
     "layoutColorsHint": "Цвета, шрифты и стиль бланка находятся в Шаблонах.",
     "layoutColorsHint": "Цвета, шрифты и стиль бланка находятся в Шаблонах.",
     "goToTemplates": "Открыть шаблоны",
     "goToTemplates": "Открыть шаблоны",
     "lockTitle": "Блокировка завершённых документов",
     "lockTitle": "Блокировка завершённых документов",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Блокируются: запчасти, работы, количество, цены, скидка, налог, номер документа и дата. Удаление документа также запрещено.",
     "lockWhatItDoes": "Блокируются: запчасти, работы, количество, цены, скидка, налог, номер документа и дата. Удаление документа также запрещено.",
     "lockWhatItAllows": "Продолжает работать: внесение оплат, онлайн-оплата картой, смена статуса, отправка и повторная отправка, вложения и внутренние заметки. Заблокированный счёт всегда можно оплатить.",
     "lockWhatItAllows": "Продолжает работать: внесение оплат, онлайн-оплата картой, смена статуса, отправка и повторная отправка, вложения и внутренние заметки. Заблокированный счёт всегда можно оплатить.",
     "lockUnlockNote": "Владельцы и администраторы могут разблокировать отдельный документ на его странице, если что-то действительно нужно исправить. Каждая разблокировка записывается в журнал аудита.",
     "lockUnlockNote": "Владельцы и администраторы могут разблокировать отдельный документ на его странице, если что-то действительно нужно исправить. Каждая разблокировка записывается в журнал аудита.",
+    "lockDetailsShow": "Что делает блокировка",
+    "lockDetailsHide": "Скрыть",
     "lockInvoicesLabel": "Блокировать счета",
     "lockInvoicesLabel": "Блокировать счета",
     "lockTriggerSent": "После отправки счёта",
     "lockTriggerSent": "После отправки счёта",
     "lockTriggerPaid": "После полной оплаты счёта",
     "lockTriggerPaid": "После полной оплаты счёта",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Добавить нового",
     "addNewTech": "+ Добавить нового",
     "defaultLaborRate": "Ставка за работу по умолчанию (в час)",
     "defaultLaborRate": "Ставка за работу по умолчанию (в час)",
     "laborRatePlaceholder": "75.00",
     "laborRatePlaceholder": "75.00",
+    "partsMarkupTitle": "Наценка на запчасти",
+    "partsMarkupDescription": "Наценка к закупочной цене при добавлении запчастей. Только внутреннее. Клиент никогда не видит себестоимость или наценку, только итоговую цену.",
+    "defaultMarkupPercent": "Наценка по умолчанию %",
+    "defaultMarkupPercentHint": "Новые вручную добавленные запчасти заполняются этой наценкой. Оставьте 0, чтобы вводить цены вручную.",
+    "markupAppliesToInventory": "Применить наценку к запчастям со склада",
+    "markupAppliesToInventoryHint": "Если включено, запчасти, выбранные со склада, также используют себестоимость + наценку по умолчанию (переопределяет цену продажи со склада).",
     "unitsTitle": "Единицы измерения",
     "unitsTitle": "Единицы измерения",
     "unitsDescription": "Выберите между метрической (км, литры) и имперской (мили, галлоны) системой.",
     "unitsDescription": "Выберите между метрической (км, литры) и имперской (мили, галлоны) системой.",
     "unitSystem": "Система единиц",
     "unitSystem": "Система единиц",

+ 4 - 0
messages/tr/customers.json

@@ -17,6 +17,10 @@
     "deleteError": "Müşteri silinemedi",
     "deleteError": "Müşteri silinemedi",
     "error": "Müşteriler yüklenemedi",
     "error": "Müşteriler yüklenemedi",
     "importCustomers": "İçe Aktar",
     "importCustomers": "İçe Aktar",
+    "assignNumbers": "Numara ata",
+    "assignNumbersTitle": "Müşteri numaraları atansın mı?",
+    "assignNumbersDescription": "{count, plural, one {# müşterinin} other {# müşterinin}} henüz numarası yok. Her biri sıradaki numarayı alır, en eski müşteriden başlayarak. Zaten numarası olan müşterilere dokunulmaz.",
+    "assignNumbersDone": "{count} müşteriye numara verildi",
     "selectedCount": "{count} seçili",
     "selectedCount": "{count} seçili",
     "clearSelection": "Temizle",
     "clearSelection": "Temizle",
     "batchDelete": "Sil ({count})",
     "batchDelete": "Sil ({count})",

+ 12 - 13
messages/tr/settings.json

@@ -520,6 +520,10 @@
   "invoice": {
   "invoice": {
     "title": "Fatura Düzeni",
     "title": "Fatura Düzeni",
     "description": "Oluşturulan PDF faturalarda nelerin görüneceğini yapılandırın.",
     "description": "Oluşturulan PDF faturalarda nelerin görüneceğini yapılandırın.",
+    "numberingTitle": "Numaralandırma ve süreler",
+    "numberingDescription": "Faturaların ve tekliflerin nasıl numaralandırıldığı, faturaların ne zaman vadesinin dolduğu ve tekliflerin ne kadar süre geçerli kaldığı.",
+    "documentsTitle": "Belgeler",
+    "documentsDescription": "Her fatura ve teklifin yanında taşıdıkları.",
     "tabs": {
     "tabs": {
       "general": "Genel",
       "general": "Genel",
       "layout": "Düzen",
       "layout": "Düzen",
@@ -550,12 +554,6 @@
     "customFooter": "Özel Fatura Alt Bilgisi",
     "customFooter": "Özel Fatura Alt Bilgisi",
     "footerPlaceholder": "İşiniz için teşekkür ederiz!",
     "footerPlaceholder": "İşiniz için teşekkür ederiz!",
     "footerHint": "Bu metin her faturanın altında görünür",
     "footerHint": "Bu metin her faturanın altında görünür",
-    "partsMarkupTitle": "Parça Kâr Marjı",
-    "partsMarkupDescription": "Parça eklenirken tedarikçi maliyetine uygulanan kâr marjı. Yalnızca dahili — müşteri maliyeti veya marjı asla görmez, sadece son fiyatı.",
-    "defaultMarkupPercent": "Varsayılan Kâr Marjı %",
-    "defaultMarkupPercentHint": "Yeni elle eklenen parçalar bu marjla önceden doldurulur. Fiyatları elle girmek için 0 bırakın.",
-    "markupAppliesToInventory": "Stok parçalarına kâr marjı uygula",
-    "markupAppliesToInventoryHint": "Etkinleştirildiğinde, stoktan seçilen parçalar da maliyet + varsayılan marj kullanır (stoğun satış fiyatını geçersiz kılar).",
     "saveInvoice": "Fatura Ayarlarını Kaydet",
     "saveInvoice": "Fatura Ayarlarını Kaydet",
     "saved": "Fatura ayarları kaydedildi",
     "saved": "Fatura ayarları kaydedildi",
     "layoutDescription": "Fatura ve tekliflerinizdeki bölümlerin sırasını ve görünürlüğünü özelleştirin.",
     "layoutDescription": "Fatura ve tekliflerinizdeki bölümlerin sırasını ve görünürlüğünü özelleştirin.",
@@ -565,13 +563,6 @@
     "layoutDocQuote": "Teklif",
     "layoutDocQuote": "Teklif",
     "saveLayout": "Düzeni kaydet",
     "saveLayout": "Düzeni kaydet",
     "resetLayout": "Sıfırla",
     "resetLayout": "Sıfırla",
-    "sectionCustomers": "Müşteriler",
-    "assignCustomerNumbers": "Müşteri numaralarını ata",
-    "assignCustomerNumbersHint": "{count} müşterinin henüz numarası yok. Sıralı numaralar atayın (mevcutlar korunur).",
-    "customerNumbersAssigned": "{count} müşteriye numara atandı",
-    "allCustomersNumbered": "Tüm müşterilerin numarası var.",
-    "assignCustomerNumbersConfirmTitle": "Müşteri numaraları atansın mı?",
-    "assignCustomerNumbersConfirmDescription": "Numarası olmayan {count} müşteriye, oluşturulma sırasına göre ve mevcut en yüksek numaradan devam eden sıralı numaralar verilecek. Numarası olan müşteriler değişmez. Numaralar sonradan düzenlenebilir.",
     "layoutColorsHint": "Renkler, yazı tipleri ve antet stili Şablonlar’da bulunur.",
     "layoutColorsHint": "Renkler, yazı tipleri ve antet stili Şablonlar’da bulunur.",
     "goToTemplates": "Şablonları aç",
     "goToTemplates": "Şablonları aç",
     "lockTitle": "Tamamlanan belgeleri kilitleme",
     "lockTitle": "Tamamlanan belgeleri kilitleme",
@@ -579,6 +570,8 @@
     "lockWhatItDoes": "Kilitlenenler: parçalar, işçilik, miktarlar, fiyatlar, indirim, vergi, belge numarası ve tarihi. Belgeyi silmek de engellenir.",
     "lockWhatItDoes": "Kilitlenenler: parçalar, işçilik, miktarlar, fiyatlar, indirim, vergi, belge numarası ve tarihi. Belgeyi silmek de engellenir.",
     "lockWhatItAllows": "Çalışmaya devam edenler: ödeme kaydetme, çevrim içi kartla ödeme, durum değişiklikleri, gönderme ve yeniden gönderme, ekler ve dahili notlar. Kilitli bir fatura her zaman ödenebilir.",
     "lockWhatItAllows": "Çalışmaya devam edenler: ödeme kaydetme, çevrim içi kartla ödeme, durum değişiklikleri, gönderme ve yeniden gönderme, ekler ve dahili notlar. Kilitli bir fatura her zaman ödenebilir.",
     "lockUnlockNote": "Sahipler ve yöneticiler, gerçekten düzeltilmesi gereken bir şey olduğunda belgenin kendi sayfasından tek bir belgenin kilidini açabilir. Her kilit açma denetim günlüğüne yazılır.",
     "lockUnlockNote": "Sahipler ve yöneticiler, gerçekten düzeltilmesi gereken bir şey olduğunda belgenin kendi sayfasından tek bir belgenin kilidini açabilir. Her kilit açma denetim günlüğüne yazılır.",
+    "lockDetailsShow": "Kilitleme ne yapar",
+    "lockDetailsHide": "Gizle",
     "lockInvoicesLabel": "Faturaları kilitle",
     "lockInvoicesLabel": "Faturaları kilitle",
     "lockTriggerSent": "Fatura gönderildiğinde",
     "lockTriggerSent": "Fatura gönderildiğinde",
     "lockTriggerPaid": "Fatura tamamen ödendiğinde",
     "lockTriggerPaid": "Fatura tamamen ödendiğinde",
@@ -1052,6 +1045,12 @@
     "addNewTech": "+ Yeni ekle",
     "addNewTech": "+ Yeni ekle",
     "defaultLaborRate": "Varsayılan İşçilik Ücreti (saat başı)",
     "defaultLaborRate": "Varsayılan İşçilik Ücreti (saat başı)",
     "laborRatePlaceholder": "75,00",
     "laborRatePlaceholder": "75,00",
+    "partsMarkupTitle": "Parça Kâr Marjı",
+    "partsMarkupDescription": "Parça eklenirken tedarikçi maliyetine uygulanan kâr marjı. Yalnızca dahili. Müşteri maliyeti veya marjı asla görmez, sadece son fiyatı.",
+    "defaultMarkupPercent": "Varsayılan Kâr Marjı %",
+    "defaultMarkupPercentHint": "Yeni elle eklenen parçalar bu marjla önceden doldurulur. Fiyatları elle girmek için 0 bırakın.",
+    "markupAppliesToInventory": "Stok parçalarına kâr marjı uygula",
+    "markupAppliesToInventoryHint": "Etkinleştirildiğinde, stoktan seçilen parçalar da maliyet + varsayılan marj kullanır (stoğun satış fiyatını geçersiz kılar).",
     "unitsTitle": "Birimler",
     "unitsTitle": "Birimler",
     "unitsDescription": "Metrik (km, litre) ve emperyal (mil, galon) birimler arasında seçim yapın.",
     "unitsDescription": "Metrik (km, litre) ve emperyal (mil, galon) birimler arasında seçim yapın.",
     "unitSystem": "Birim Sistemi",
     "unitSystem": "Birim Sistemi",

+ 55 - 1
src/app/(authenticated)/customers/customers-client.tsx

@@ -38,7 +38,11 @@ import { useConfirm } from '@/components/confirm-dialog'
 import { CustomerForm } from '@/features/customers/Components/CustomerForm'
 import { CustomerForm } from '@/features/customers/Components/CustomerForm'
 import { ImportWizard } from '@/features/import/Components/ImportWizard'
 import { ImportWizard } from '@/features/import/Components/ImportWizard'
 import { Checkbox } from '@/components/ui/checkbox'
 import { Checkbox } from '@/components/ui/checkbox'
-import { deleteCustomer, deleteCustomers } from '@/features/customers/Actions/customerActions'
+import {
+  backfillCustomerNumbers,
+  deleteCustomer,
+  deleteCustomers,
+} from '@/features/customers/Actions/customerActions'
 import { toast } from 'sonner'
 import { toast } from 'sonner'
 import {
 import {
   ArrowDown,
   ArrowDown,
@@ -46,6 +50,7 @@ import {
   ArrowUpDown,
   ArrowUpDown,
   Car,
   Car,
   ExternalLink,
   ExternalLink,
+  Hash,
   Loader2,
   Loader2,
   MoreVertical,
   MoreVertical,
   Pencil,
   Pencil,
@@ -83,11 +88,14 @@ export function CustomersClient({
   search,
   search,
   sortBy,
   sortBy,
   sortOrder,
   sortOrder,
+  unnumbered: initialUnnumbered = 0,
 }: {
 }: {
   data: PaginatedData
   data: PaginatedData
   search: string
   search: string
   sortBy: string
   sortBy: string
   sortOrder: 'asc' | 'desc'
   sortOrder: 'asc' | 'desc'
+  /** Customers with no number yet; the assign button shows while there are any. */
+  unnumbered?: number
 }) {
 }) {
   const t = useTranslations('customers.list')
   const t = useTranslations('customers.list')
   const tc = useTranslations('common')
   const tc = useTranslations('common')
@@ -98,6 +106,12 @@ export function CustomersClient({
   const [isPending, startTransition] = useTransition()
   const [isPending, startTransition] = useTransition()
   const tableNav = useTableKeyboardNav()
   const tableNav = useTableKeyboardNav()
   useRememberedSort('customers')
   useRememberedSort('customers')
+
+  // Customers imported or created before numbering may have none. The
+  // button lives here, beside the list it changes, and goes away once every
+  // customer has a number.
+  const [unnumbered, setUnnumbered] = useState(initialUnnumbered)
+  const [assigning, setAssigning] = useState(false)
   const [showForm, setShowForm] = useState(false)
   const [showForm, setShowForm] = useState(false)
   const [showImport, setShowImport] = useState(false)
   const [showImport, setShowImport] = useState(false)
   const [editCustomer, setEditCustomer] = useState<Customer | null>(null)
   const [editCustomer, setEditCustomer] = useState<Customer | null>(null)
@@ -215,6 +229,25 @@ export function CustomersClient({
     setIsDeleting(false)
     setIsDeleting(false)
   }
   }
 
 
+  const handleAssignNumbers = async () => {
+    const ok = await confirm({
+      title: t('assignNumbersTitle'),
+      description: t('assignNumbersDescription', { count: unnumbered }),
+      confirmLabel: t('assignNumbers'),
+    })
+    if (!ok) return
+    setAssigning(true)
+    const result = await backfillCustomerNumbers()
+    setAssigning(false)
+    if (result.success && result.data) {
+      setUnnumbered(0)
+      toast.success(t('assignNumbersDone', { count: result.data.assigned }))
+      router.refresh()
+    } else {
+      toast.error(result.success ? t('error') : result.error || t('error'))
+    }
+  }
+
   return (
   return (
     <div className="flex min-h-0 flex-1 flex-col gap-4">
     <div className="flex min-h-0 flex-1 flex-col gap-4">
       {/* Toolbar */}
       {/* Toolbar */}
@@ -254,6 +287,27 @@ export function CustomersClient({
               {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
               {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
             </div>
             </div>
             <div className="flex shrink-0 items-center gap-2">
             <div className="flex shrink-0 items-center gap-2">
+              {unnumbered > 0 && (
+                <Button
+                  size="sm"
+                  variant="outline"
+                  onClick={handleAssignNumbers}
+                  disabled={assigning}
+                  aria-label={t('assignNumbers')}
+                  title={t('assignNumbersDescription', { count: unnumbered })}
+                  className="h-9 w-9 p-0 md:h-8 md:w-auto md:px-3"
+                >
+                  {assigning ? (
+                    <Loader2 className="h-4 w-4 animate-spin md:mr-1 md:h-3.5 md:w-3.5" />
+                  ) : (
+                    <Hash className="h-4 w-4 md:mr-1 md:h-3.5 md:w-3.5" />
+                  )}
+                  <span className="hidden md:inline">{t('assignNumbers')}</span>
+                  <span className="hidden rounded-full bg-primary/10 px-1.5 text-[11px] font-semibold tabular-nums text-primary md:ml-1.5 md:inline">
+                    {unnumbered}
+                  </span>
+                </Button>
+              )}
               <Button
               <Button
                 size="sm"
                 size="sm"
                 variant="outline"
                 variant="outline"

+ 13 - 7
src/app/(authenticated)/customers/page.tsx

@@ -4,6 +4,7 @@ import { getCustomersPaginated } from '@/features/customers/Actions/customerActi
 import { CustomersClient } from './customers-client'
 import { CustomersClient } from './customers-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
 import { ListPage } from '@/components/list-page'
 import { ListPage } from '@/components/list-page'
+import { countUnnumberedCustomers } from '@/features/customers/Actions/customerActions'
 
 
 export default async function CustomersPage({
 export default async function CustomersPage({
   searchParams,
   searchParams,
@@ -21,13 +22,17 @@ export default async function CustomersPage({
     sortBy: undefined,
     sortBy: undefined,
     sortOrder: 'desc',
     sortOrder: 'desc',
   })
   })
-  const result = await getCustomersPaginated({
-    page: params.page ? parseInt(params.page) : 1,
-    pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
-    search: params.search,
-    sortBy: sort.sortBy,
-    sortOrder: sort.sortOrder,
-  })
+  const [result, unnumberedResult] = await Promise.all([
+    getCustomersPaginated({
+      page: params.page ? parseInt(params.page) : 1,
+      pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
+      search: params.search,
+      sortBy: sort.sortBy,
+      sortOrder: sort.sortOrder,
+    }),
+    countUnnumberedCustomers(),
+  ])
+  const unnumbered = unnumberedResult.success ? (unnumberedResult.data ?? 0) : 0
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
     const t = await getTranslations('customers.list')
     const t = await getTranslations('customers.list')
@@ -50,6 +55,7 @@ export default async function CustomersPage({
           search={params.search || ''}
           search={params.search || ''}
           sortBy={sort.sortBy || ''}
           sortBy={sort.sortBy || ''}
           sortOrder={sort.sortOrder}
           sortOrder={sort.sortOrder}
+          unnumbered={unnumbered}
         />
         />
       </ListPage>
       </ListPage>
     </>
     </>

+ 214 - 263
src/app/(authenticated)/settings/invoice/invoice-settings.tsx

@@ -7,7 +7,6 @@ import { useTranslations } from 'next-intl'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Label } from '@/components/ui/label'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
-import { Separator } from '@/components/ui/separator'
 import { Switch } from '@/components/ui/switch'
 import { Switch } from '@/components/ui/switch'
 import {
 import {
   Select,
   Select,
@@ -19,10 +18,9 @@ import {
 import { Textarea } from '@/components/ui/textarea'
 import { Textarea } from '@/components/ui/textarea'
 import { toast } from 'sonner'
 import { toast } from 'sonner'
 import { setSettings } from '@/features/settings/Actions/settingsActions'
 import { setSettings } from '@/features/settings/Actions/settingsActions'
-import { backfillCustomerNumbers } from '@/features/customers/Actions/customerActions'
 import { freezeUnfrozenInvoices } from '@/features/invoices/Actions/legacyInvoiceActions'
 import { freezeUnfrozenInvoices } from '@/features/invoices/Actions/legacyInvoiceActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
-import { FileText, Loader2, Lock, Save } from 'lucide-react'
+import { ChevronDown, ChevronUp, FileText, Hash, Loader2, Lock, Save } from 'lucide-react'
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from '../read-only-guard'
 import { ReadOnlyBanner, SaveButton, ReadOnlyWrapper } from '../read-only-guard'
 import { cn } from '@/lib/utils'
 import { cn } from '@/lib/utils'
 import { useConfirm } from '@/components/confirm-dialog'
 import { useConfirm } from '@/components/confirm-dialog'
@@ -51,7 +49,6 @@ interface InvoiceSettingsProps {
   settings: Record<string, string>
   settings: Record<string, string>
   /** Fills the layout preview with this workshop's own letterhead. */
   /** Fills the layout preview with this workshop's own letterhead. */
   workshop?: { name?: string; address?: string; phone?: string; email?: string; slogan?: string }
   workshop?: { name?: string; address?: string; phone?: string; email?: string; slogan?: string }
-  unnumberedCustomers?: number
   /** Invoices that reached a customer before issuing existed, still unfrozen. */
   /** Invoices that reached a customer before issuing existed, still unfrozen. */
   unfrozenInvoices?: number
   unfrozenInvoices?: number
   initialInvoiceLayout?: InvoiceLayoutConfig
   initialInvoiceLayout?: InvoiceLayoutConfig
@@ -61,16 +58,59 @@ interface InvoiceSettingsProps {
   telegramEnabled?: boolean
   telegramEnabled?: boolean
 }
 }
 
 
+/** One labelled control with its one-line explanation beneath. */
+function Field({
+  id,
+  label,
+  hint,
+  children,
+}: {
+  id: string
+  label: string
+  hint?: React.ReactNode
+  children: React.ReactNode
+}) {
+  return (
+    <div className="space-y-1.5">
+      <Label htmlFor={id}>{label}</Label>
+      {children}
+      {hint && <p className="text-xs leading-relaxed text-muted-foreground">{hint}</p>}
+    </div>
+  )
+}
+
+/** A switch with its name and explanation on the left, the way the tax page does it. */
+function SwitchRow({
+  id,
+  label,
+  hint,
+  checked,
+  onCheckedChange,
+}: {
+  id: string
+  label: string
+  hint: string
+  checked: boolean
+  onCheckedChange: (checked: boolean) => void
+}) {
+  return (
+    <div className="flex items-start justify-between gap-4">
+      <div className="space-y-1">
+        <Label htmlFor={id}>{label}</Label>
+        <p className="text-xs leading-relaxed text-muted-foreground">{hint}</p>
+      </div>
+      <Switch id={id} checked={checked} onCheckedChange={onCheckedChange} className="mt-0.5" />
+    </div>
+  )
+}
+
 export function InvoiceSettings({
 export function InvoiceSettings({
   settings,
   settings,
-  workshop,
-  unnumberedCustomers = 0,
   unfrozenInvoices = 0,
   unfrozenInvoices = 0,
   initialInvoiceLayout,
   initialInvoiceLayout,
   initialQuoteLayout,
   initialQuoteLayout,
   customFields,
   customFields,
   customFieldsEnabled,
   customFieldsEnabled,
-  telegramEnabled = false,
 }: InvoiceSettingsProps) {
 }: InvoiceSettingsProps) {
   const router = useRouter()
   const router = useRouter()
   const searchParams = useSearchParams()
   const searchParams = useSearchParams()
@@ -92,7 +132,6 @@ export function InvoiceSettings({
     [router, searchParams]
     [router, searchParams]
   )
   )
 
 
-  // General tab state
   const [invoicePrefix, setInvoicePrefix] = useState(
   const [invoicePrefix, setInvoicePrefix] = useState(
     settings[SETTING_KEYS.INVOICE_PREFIX] ?? '{year}-'
     settings[SETTING_KEYS.INVOICE_PREFIX] ?? '{year}-'
   )
   )
@@ -105,12 +144,6 @@ export function InvoiceSettings({
   )
   )
   const [dueDays, setDueDays] = useState(settings[SETTING_KEYS.INVOICE_DUE_DAYS] || '14')
   const [dueDays, setDueDays] = useState(settings[SETTING_KEYS.INVOICE_DUE_DAYS] || '14')
   const [footerNote, setFooterNote] = useState(settings[SETTING_KEYS.INVOICE_FOOTER_NOTE] || '')
   const [footerNote, setFooterNote] = useState(settings[SETTING_KEYS.INVOICE_FOOTER_NOTE] || '')
-  const [defaultMarkupPercent, setDefaultMarkupPercent] = useState(
-    settings[SETTING_KEYS.PARTS_DEFAULT_MARKUP_PERCENT] || '0'
-  )
-  const [markupAppliesToInventory, setMarkupAppliesToInventory] = useState(
-    settings[SETTING_KEYS.PARTS_MARKUP_APPLIES_TO_INVENTORY] === 'true'
-  )
   const [invoiceLockEnabled, setInvoiceLockEnabled] = useState(
   const [invoiceLockEnabled, setInvoiceLockEnabled] = useState(
     settings[SETTING_KEYS.INVOICE_LOCK_ENABLED] === 'true'
     settings[SETTING_KEYS.INVOICE_LOCK_ENABLED] === 'true'
   )
   )
@@ -124,6 +157,9 @@ export function InvoiceSettings({
     settings[SETTING_KEYS.QUOTE_LOCK_TRIGGER] || 'accepted'
     settings[SETTING_KEYS.QUOTE_LOCK_TRIGGER] || 'accepted'
   )
   )
   const [attachPdf, setAttachPdf] = useState(settings[SETTING_KEYS.EMAIL_ATTACH_PDF] !== 'false')
   const [attachPdf, setAttachPdf] = useState(settings[SETTING_KEYS.EMAIL_ATTACH_PDF] !== 'false')
+  // The three paragraphs on what a lock freezes are worth reading once, not
+  // every time somebody comes to change a due date. Folded away by default.
+  const [lockDetailsOpen, setLockDetailsOpen] = useState(false)
 
 
   const handleSaveGeneral = async () => {
   const handleSaveGeneral = async () => {
     setSaving(true)
     setSaving(true)
@@ -134,8 +170,6 @@ export function InvoiceSettings({
       [SETTING_KEYS.INVOICE_START_NUMBER]: invoiceStartNumber,
       [SETTING_KEYS.INVOICE_START_NUMBER]: invoiceStartNumber,
       [SETTING_KEYS.INVOICE_DUE_DAYS]: dueDays,
       [SETTING_KEYS.INVOICE_DUE_DAYS]: dueDays,
       [SETTING_KEYS.INVOICE_FOOTER_NOTE]: footerNote,
       [SETTING_KEYS.INVOICE_FOOTER_NOTE]: footerNote,
-      [SETTING_KEYS.PARTS_DEFAULT_MARKUP_PERCENT]: defaultMarkupPercent,
-      [SETTING_KEYS.PARTS_MARKUP_APPLIES_TO_INVENTORY]: markupAppliesToInventory ? 'true' : 'false',
       [SETTING_KEYS.INVOICE_LOCK_ENABLED]: invoiceLockEnabled ? 'true' : 'false',
       [SETTING_KEYS.INVOICE_LOCK_ENABLED]: invoiceLockEnabled ? 'true' : 'false',
       [SETTING_KEYS.INVOICE_LOCK_TRIGGER]: invoiceLockTrigger,
       [SETTING_KEYS.INVOICE_LOCK_TRIGGER]: invoiceLockTrigger,
       [SETTING_KEYS.QUOTE_LOCK_ENABLED]: quoteLockEnabled ? 'true' : 'false',
       [SETTING_KEYS.QUOTE_LOCK_ENABLED]: quoteLockEnabled ? 'true' : 'false',
@@ -148,8 +182,6 @@ export function InvoiceSettings({
   }
   }
 
 
   const confirm = useConfirm()
   const confirm = useConfirm()
-  const [assigning, setAssigning] = useState(false)
-  const [unnumbered, setUnnumbered] = useState(unnumberedCustomers)
 
 
   // Invoices sent before this version could lock what they print. Locked in
   // Invoices sent before this version could lock what they print. Locked in
   // batches, one request each, so a workshop with thousands of them sees
   // batches, one request each, so a workshop with thousands of them sees
@@ -187,24 +219,15 @@ export function InvoiceSettings({
       setFreezing(null)
       setFreezing(null)
     }
     }
   }
   }
-  const handleAssignCustomerNumbers = async () => {
-    const ok = await confirm({
-      title: t('invoice.assignCustomerNumbersConfirmTitle'),
-      description: t('invoice.assignCustomerNumbersConfirmDescription', { count: unnumbered }),
-      confirmLabel: t('invoice.assignCustomerNumbers'),
+
+  const year = String(new Date().getFullYear())
+  const numberHint = (prefix: string, preview: string) =>
+    t.rich('invoice.invoiceNumberFormatHint', {
+      code: (chunks) => <code className="rounded bg-muted px-1">{chunks}</code>,
+      bold: (chunks) => <span className="font-medium">{chunks}</span>,
+      year: '{year}',
+      preview: prefix.replace(/\{year\}/g, year) + preview,
     })
     })
-    if (!ok) return
-    setAssigning(true)
-    const result = await backfillCustomerNumbers()
-    setAssigning(false)
-    if (result.success && result.data) {
-      setUnnumbered(0)
-      toast.success(t('invoice.customerNumbersAssigned', { count: result.data.assigned }))
-      router.refresh()
-    } else {
-      toast.error(result.error || t('templates.failedSave'))
-    }
-  }
 
 
   return (
   return (
     <div className="space-y-6">
     <div className="space-y-6">
@@ -216,21 +239,20 @@ export function InvoiceSettings({
         </p>
         </p>
       </div>
       </div>
 
 
-      {/* Tab Buttons */}
-      <div className="flex gap-1 rounded-lg border bg-muted p-1">
-        <button
-          type="button"
-          onClick={() => setTab('general')}
-          className={cn(
-            'flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors',
-            tab === 'general'
-              ? 'bg-background text-foreground shadow-sm'
-              : 'text-muted-foreground hover:text-foreground'
-          )}
-        >
-          {t('invoice.tabs.general')}
-        </button>
-        {customFieldsEnabled && (
+      {customFieldsEnabled && (
+        <div className="flex gap-1 rounded-lg border bg-muted p-1">
+          <button
+            type="button"
+            onClick={() => setTab('general')}
+            className={cn(
+              'flex-1 rounded-md px-4 py-2 text-sm font-medium transition-colors',
+              tab === 'general'
+                ? 'bg-background text-foreground shadow-sm'
+                : 'text-muted-foreground hover:text-foreground'
+            )}
+          >
+            {t('invoice.tabs.general')}
+          </button>
           <button
           <button
             type="button"
             type="button"
             onClick={() => setTab('customFields')}
             onClick={() => setTab('customFields')}
@@ -243,68 +265,106 @@ export function InvoiceSettings({
           >
           >
             {t('invoice.tabs.customFields')}
             {t('invoice.tabs.customFields')}
           </button>
           </button>
-        )}
-      </div>
+        </div>
+      )}
 
 
       {tab === 'general' ? (
       {tab === 'general' ? (
         <ReadOnlyWrapper>
         <ReadOnlyWrapper>
-          <AppCard icon={FileText} title={t('invoice.tabs.general')} contentClassName="space-y-6">
-            <div className="space-y-3">
-              <h3 className="text-sm font-semibold">{t('invoice.sectionInvoices')}</h3>
-              <div className="grid gap-4 sm:grid-cols-2">
-                <div className="space-y-2">
-                  <Label htmlFor="invoicePrefix">{t('invoice.invoiceNumberFormat')}</Label>
-                  <Input
+          <div className="space-y-6">
+            <AppCard
+              icon={Hash}
+              title={t('invoice.numberingTitle')}
+              description={t('invoice.numberingDescription')}
+            >
+              <div className="grid gap-x-8 gap-y-5 md:grid-cols-2">
+                <div className="space-y-4">
+                  <h3 className="text-sm font-semibold">{t('invoice.sectionInvoices')}</h3>
+                  <Field
                     id="invoicePrefix"
                     id="invoicePrefix"
-                    placeholder="{year}-"
-                    value={invoicePrefix}
-                    onChange={(e) => setInvoicePrefix(e.target.value)}
-                  />
-                  <p className="text-xs text-muted-foreground">
-                    {t.rich('invoice.invoiceNumberFormatHint', {
-                      code: (chunks) => <code className="rounded bg-muted px-1">{chunks}</code>,
-                      bold: (chunks) => <span className="font-medium">{chunks}</span>,
-                      year: '{year}',
-                      preview:
-                        invoicePrefix.replace(/\{year\}/g, String(new Date().getFullYear())) +
-                        (invoiceStartNumber || '1001'),
-                    })}
-                  </p>
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor="invoiceStartNumber">{t('invoice.nextInvoiceNumber')}</Label>
-                  <Input
+                    label={t('invoice.invoiceNumberFormat')}
+                    hint={numberHint(invoicePrefix, invoiceStartNumber || '1001')}
+                  >
+                    <Input
+                      id="invoicePrefix"
+                      placeholder="{year}-"
+                      value={invoicePrefix}
+                      onChange={(e) => setInvoicePrefix(e.target.value)}
+                    />
+                  </Field>
+                  <Field
                     id="invoiceStartNumber"
                     id="invoiceStartNumber"
-                    type="number"
-                    min="1"
-                    placeholder={t('invoice.nextInvoiceNumberPlaceholder')}
-                    value={invoiceStartNumber}
-                    onChange={(e) => setInvoiceStartNumber(e.target.value)}
-                    className="w-32"
-                  />
-                  <p className="text-xs text-muted-foreground">
-                    {t('invoice.nextInvoiceNumberHint', {
+                    label={t('invoice.nextInvoiceNumber')}
+                    hint={t('invoice.nextInvoiceNumberHint', {
                       example: invoicePrefix + (invoiceStartNumber || '...'),
                       example: invoicePrefix + (invoiceStartNumber || '...'),
                     })}
                     })}
-                  </p>
+                  >
+                    <Input
+                      id="invoiceStartNumber"
+                      type="number"
+                      min="1"
+                      placeholder={t('invoice.nextInvoiceNumberPlaceholder')}
+                      value={invoiceStartNumber}
+                      onChange={(e) => setInvoiceStartNumber(e.target.value)}
+                      className="w-32"
+                    />
+                  </Field>
+                  <Field id="dueDays" label={t('invoice.dueDays')} hint={t('invoice.dueDaysHint')}>
+                    <Input
+                      id="dueDays"
+                      type="number"
+                      min="0"
+                      placeholder="14"
+                      value={dueDays}
+                      onChange={(e) => setDueDays(e.target.value)}
+                      className="w-24"
+                    />
+                  </Field>
                 </div>
                 </div>
-                <div className="space-y-2">
-                  <Label htmlFor="dueDays">{t('invoice.dueDays')}</Label>
-                  <Input
-                    id="dueDays"
-                    type="number"
-                    min="0"
-                    placeholder="14"
-                    value={dueDays}
-                    onChange={(e) => setDueDays(e.target.value)}
-                    className="w-24"
-                  />
-                  <p className="text-xs text-muted-foreground">{t('invoice.dueDaysHint')}</p>
+
+                <div className="space-y-4">
+                  <h3 className="text-sm font-semibold">{t('invoice.sectionQuotes')}</h3>
+                  <Field
+                    id="quotePrefix"
+                    label={t('invoice.quoteNumberFormat')}
+                    hint={numberHint(quotePrefix, '1001')}
+                  >
+                    <Input
+                      id="quotePrefix"
+                      placeholder="QT-"
+                      value={quotePrefix}
+                      onChange={(e) => setQuotePrefix(e.target.value)}
+                    />
+                  </Field>
+                  <Field
+                    id="quoteValidDays"
+                    label={t('invoice.quoteValidDays')}
+                    hint={t('invoice.quoteValidDaysHint')}
+                  >
+                    <Input
+                      id="quoteValidDays"
+                      type="number"
+                      min="0"
+                      placeholder="30"
+                      value={quoteValidDays}
+                      onChange={(e) => setQuoteValidDays(e.target.value)}
+                      className="w-24"
+                    />
+                  </Field>
                 </div>
                 </div>
               </div>
               </div>
+            </AppCard>
 
 
-              <div className="space-y-2">
-                <Label htmlFor="footerNote">{t('invoice.customFooter')}</Label>
+            <AppCard
+              icon={FileText}
+              title={t('invoice.documentsTitle')}
+              description={t('invoice.documentsDescription')}
+              contentClassName="space-y-5"
+            >
+              <Field
+                id="footerNote"
+                label={t('invoice.customFooter')}
+                hint={t('invoice.footerHint')}
+              >
                 <Textarea
                 <Textarea
                   id="footerNote"
                   id="footerNote"
                   placeholder={t('invoice.footerPlaceholder')}
                   placeholder={t('invoice.footerPlaceholder')}
@@ -312,108 +372,24 @@ export function InvoiceSettings({
                   value={footerNote}
                   value={footerNote}
                   onChange={(e) => setFooterNote(e.target.value)}
                   onChange={(e) => setFooterNote(e.target.value)}
                 />
                 />
-                <p className="text-xs text-muted-foreground">{t('invoice.footerHint')}</p>
-              </div>
-            </div>
-
-            <Separator />
-
-            <div className="space-y-3">
-              <h3 className="text-sm font-semibold">{t('invoice.sectionQuotes')}</h3>
-              <div className="grid gap-4 sm:grid-cols-2">
-                <div className="space-y-2">
-                  <Label htmlFor="quotePrefix">{t('invoice.quoteNumberFormat')}</Label>
-                  <Input
-                    id="quotePrefix"
-                    placeholder="QT-"
-                    value={quotePrefix}
-                    onChange={(e) => setQuotePrefix(e.target.value)}
-                  />
-                  <p className="text-xs text-muted-foreground">
-                    {t.rich('invoice.quoteNumberFormatHint', {
-                      code: (chunks) => <code className="rounded bg-muted px-1">{chunks}</code>,
-                      bold: (chunks) => <span className="font-medium">{chunks}</span>,
-                      year: '{year}',
-                      preview:
-                        quotePrefix.replace(/\{year\}/g, String(new Date().getFullYear())) + '1001',
-                    })}
-                  </p>
-                </div>
-                <div className="space-y-2">
-                  <Label htmlFor="quoteValidDays">{t('invoice.quoteValidDays')}</Label>
-                  <Input
-                    id="quoteValidDays"
-                    type="number"
-                    min="0"
-                    placeholder="30"
-                    value={quoteValidDays}
-                    onChange={(e) => setQuoteValidDays(e.target.value)}
-                    className="w-32"
-                  />
-                  <p className="text-xs text-muted-foreground">{t('invoice.quoteValidDaysHint')}</p>
-                </div>
-              </div>
-            </div>
-
-            <Separator />
-
-            <div className="space-y-3">
-              <h3 className="text-sm font-semibold">{t('invoice.sectionSending')}</h3>
-              <Label
-                htmlFor="attachPdf"
-                className="flex items-center justify-between gap-3 font-normal"
-              >
-                <span className="text-sm">{t('invoice.attachPdfLabel')}</span>
-                <Switch id="attachPdf" checked={attachPdf} onCheckedChange={setAttachPdf} />
-              </Label>
-              <p className="text-xs text-muted-foreground">{t('invoice.attachPdfHint')}</p>
-            </div>
-
-            <Separator />
-
-            <div className="space-y-3">
-              <h3 className="text-sm font-semibold">{t('invoice.sectionCustomers')}</h3>
-              <div className="flex items-center justify-between gap-4">
-                <p className="text-xs text-muted-foreground">
-                  {unnumbered > 0
-                    ? t('invoice.assignCustomerNumbersHint', { count: unnumbered })
-                    : t('invoice.allCustomersNumbered')}
-                </p>
-                <Button
-                  type="button"
-                  variant="outline"
-                  size="sm"
-                  disabled={assigning || unnumbered === 0}
-                  onClick={handleAssignCustomerNumbers}
-                >
-                  {assigning && <Loader2 className="mr-2 h-3.5 w-3.5 animate-spin" />}
-                  {t('invoice.assignCustomerNumbers')}
-                </Button>
-              </div>
-            </div>
-
-            <Separator />
-
-            <div className="space-y-3">
-              <div>
-                <h3 className="flex items-center gap-2 text-sm font-semibold">
-                  <Lock className="h-3.5 w-3.5" />
-                  {t('invoice.lockTitle')}
-                </h3>
-                <p className="text-xs text-muted-foreground">{t('invoice.lockDescription')}</p>
-              </div>
-
-              <div className="rounded-md border border-dashed p-3">
-                <p className="text-xs text-muted-foreground">{t('invoice.lockWhatItDoes')}</p>
-                <p className="mt-1.5 text-xs text-muted-foreground">
-                  {t('invoice.lockWhatItAllows')}
-                </p>
-                <p className="mt-1.5 text-xs text-muted-foreground">
-                  {t('invoice.lockUnlockNote')}
-                </p>
-              </div>
+              </Field>
+              <SwitchRow
+                id="attachPdf"
+                label={t('invoice.attachPdfLabel')}
+                hint={t('invoice.attachPdfHint')}
+                checked={attachPdf}
+                onCheckedChange={setAttachPdf}
+              />
+            </AppCard>
 
 
-              <div className="grid gap-4 sm:grid-cols-2">
+            <AppCard
+              icon={Lock}
+              title={t('invoice.lockTitle')}
+              description={t('invoice.lockDescription')}
+              contentClassName="space-y-5"
+              footer={unfrozen === 0 ? t('invoice.freezeNone') : undefined}
+            >
+              <div className="grid gap-x-8 gap-y-5 md:grid-cols-2">
                 <div className="space-y-2">
                 <div className="space-y-2">
                   <Label
                   <Label
                     htmlFor="invoiceLockEnabled"
                     htmlFor="invoiceLockEnabled"
@@ -439,7 +415,7 @@ export function InvoiceSettings({
                       <SelectItem value="paid">{t('invoice.lockTriggerPaid')}</SelectItem>
                       <SelectItem value="paid">{t('invoice.lockTriggerPaid')}</SelectItem>
                     </SelectContent>
                     </SelectContent>
                   </Select>
                   </Select>
-                  <p className="text-xs text-muted-foreground">
+                  <p className="text-xs leading-relaxed text-muted-foreground">
                     {invoiceLockTrigger === 'sent'
                     {invoiceLockTrigger === 'sent'
                       ? t('invoice.lockTriggerSentHint')
                       ? t('invoice.lockTriggerSentHint')
                       : t('invoice.lockTriggerPaidHint')}
                       : t('invoice.lockTriggerPaidHint')}
@@ -473,7 +449,7 @@ export function InvoiceSettings({
                       </SelectItem>
                       </SelectItem>
                     </SelectContent>
                     </SelectContent>
                   </Select>
                   </Select>
-                  <p className="text-xs text-muted-foreground">
+                  <p className="text-xs leading-relaxed text-muted-foreground">
                     {quoteLockTrigger === 'sent'
                     {quoteLockTrigger === 'sent'
                       ? t('invoice.quoteLockTriggerSentHint')
                       ? t('invoice.quoteLockTriggerSentHint')
                       : t('invoice.quoteLockTriggerAcceptedHint')}
                       : t('invoice.quoteLockTriggerAcceptedHint')}
@@ -481,15 +457,36 @@ export function InvoiceSettings({
                 </div>
                 </div>
               </div>
               </div>
 
 
-              <div className="rounded-md border p-3">
-                <h4 className="text-sm font-medium">{t('invoice.freezeTitle')}</h4>
-                <div className="mt-1.5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
-                  <p className="text-xs text-muted-foreground">
-                    {unfrozen > 0
-                      ? t('invoice.freezeBody', { count: unfrozen })
-                      : t('invoice.freezeNone')}
-                  </p>
-                  {unfrozen > 0 && (
+              <div>
+                <button
+                  type="button"
+                  onClick={() => setLockDetailsOpen((open) => !open)}
+                  aria-expanded={lockDetailsOpen}
+                  className="inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
+                >
+                  {lockDetailsOpen ? t('invoice.lockDetailsHide') : t('invoice.lockDetailsShow')}
+                  {lockDetailsOpen ? (
+                    <ChevronUp className="h-3.5 w-3.5" />
+                  ) : (
+                    <ChevronDown className="h-3.5 w-3.5" />
+                  )}
+                </button>
+                {lockDetailsOpen && (
+                  <div className="mt-2 space-y-1.5 rounded-md border border-dashed p-3 text-xs leading-relaxed text-muted-foreground">
+                    <p>{t('invoice.lockWhatItDoes')}</p>
+                    <p>{t('invoice.lockWhatItAllows')}</p>
+                    <p>{t('invoice.lockUnlockNote')}</p>
+                  </div>
+                )}
+              </div>
+
+              {unfrozen > 0 && (
+                <div className="rounded-md border p-3">
+                  <h4 className="text-sm font-medium">{t('invoice.freezeTitle')}</h4>
+                  <div className="mt-1.5 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
+                    <p className="text-xs leading-relaxed text-muted-foreground">
+                      {t('invoice.freezeBody', { count: unfrozen })}
+                    </p>
                     <Button
                     <Button
                       type="button"
                       type="button"
                       variant="outline"
                       variant="outline"
@@ -506,58 +503,12 @@ export function InvoiceSettings({
                           })
                           })
                         : t('invoice.freezeButton')}
                         : t('invoice.freezeButton')}
                     </Button>
                     </Button>
-                  )}
+                  </div>
                 </div>
                 </div>
-              </div>
-            </div>
-
-            <Separator />
-
-            <div className="space-y-3">
-              <div>
-                <h3 className="text-sm font-semibold">{t('invoice.partsMarkupTitle')}</h3>
-                <p className="text-xs text-muted-foreground">
-                  {t('invoice.partsMarkupDescription')}
-                </p>
-              </div>
-              <div className="grid gap-4 sm:grid-cols-2">
-                <div className="space-y-2">
-                  <Label htmlFor="defaultMarkupPercent">{t('invoice.defaultMarkupPercent')}</Label>
-                  <Input
-                    id="defaultMarkupPercent"
-                    type="number"
-                    min="0"
-                    step="0.1"
-                    placeholder="0"
-                    value={defaultMarkupPercent}
-                    onChange={(e) => setDefaultMarkupPercent(e.target.value)}
-                    className="w-32"
-                  />
-                  <p className="text-xs text-muted-foreground">
-                    {t('invoice.defaultMarkupPercentHint')}
-                  </p>
-                </div>
-                <div className="space-y-2">
-                  <Label
-                    htmlFor="markupAppliesToInventory"
-                    className="flex items-center justify-between gap-3"
-                  >
-                    <span>{t('invoice.markupAppliesToInventory')}</span>
-                    <Switch
-                      id="markupAppliesToInventory"
-                      checked={markupAppliesToInventory}
-                      onCheckedChange={setMarkupAppliesToInventory}
-                    />
-                  </Label>
-                  <p className="text-xs text-muted-foreground">
-                    {t('invoice.markupAppliesToInventoryHint')}
-                  </p>
-                </div>
-              </div>
-            </div>
+              )}
+            </AppCard>
 
 
             <SaveButton>
             <SaveButton>
-              <Separator />
               <div className="flex items-center gap-3">
               <div className="flex items-center gap-3">
                 <Button onClick={handleSaveGeneral} disabled={saving}>
                 <Button onClick={handleSaveGeneral} disabled={saving}>
                   {saving ? (
                   {saving ? (
@@ -569,7 +520,7 @@ export function InvoiceSettings({
                 </Button>
                 </Button>
               </div>
               </div>
             </SaveButton>
             </SaveButton>
-          </AppCard>
+          </div>
         </ReadOnlyWrapper>
         </ReadOnlyWrapper>
       ) : (
       ) : (
         <CustomFieldsManager
         <CustomFieldsManager

+ 0 - 4
src/app/(authenticated)/settings/invoice/page.tsx

@@ -28,9 +28,6 @@ export default async function InvoiceSettingsPage() {
   ])
   ])
 
 
   const settings = result.success && result.data ? result.data : {}
   const settings = result.success && result.data ? result.data : {}
-  const unnumberedCustomers = await db.customer.count({
-    where: { organizationId: data.organizationId, customerNumber: null },
-  })
   const customFields =
   const customFields =
     customFieldsResult.success && customFieldsResult.data ? customFieldsResult.data : []
     customFieldsResult.success && customFieldsResult.data ? customFieldsResult.data : []
   const unfrozenResult = await countUnfrozenInvoices()
   const unfrozenResult = await countUnfrozenInvoices()
@@ -61,7 +58,6 @@ export default async function InvoiceSettingsPage() {
     <InvoiceSettings
     <InvoiceSettings
       settings={settings}
       settings={settings}
       workshop={workshop}
       workshop={workshop}
-      unnumberedCustomers={unnumberedCustomers}
       unfrozenInvoices={unfrozenInvoices}
       unfrozenInvoices={unfrozenInvoices}
       initialInvoiceLayout={invoiceLayoutResult.success ? invoiceLayoutResult.data : undefined}
       initialInvoiceLayout={invoiceLayoutResult.success ? invoiceLayoutResult.data : undefined}
       initialQuoteLayout={quoteLayoutResult.success ? quoteLayoutResult.data : undefined}
       initialQuoteLayout={quoteLayoutResult.success ? quoteLayoutResult.data : undefined}

+ 57 - 1
src/app/(authenticated)/settings/workshop/workshop-settings.tsx

@@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Label } from '@/components/ui/label'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { Separator } from '@/components/ui/separator'
 import { Separator } from '@/components/ui/separator'
+import { Switch } from '@/components/ui/switch'
 import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
 import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
 import {
 import {
   Command,
   Command,
@@ -33,7 +34,7 @@ import { toast } from 'sonner'
 import { setSettings } from '@/features/settings/Actions/settingsActions'
 import { setSettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { assignTechToUnassignedWorkOrders } from '@/features/workboard/Actions/technicianActions'
 import { assignTechToUnassignedWorkOrders } from '@/features/workboard/Actions/technicianActions'
-import { Loader2, Ruler, Save, Wrench, Check, ChevronsUpDown, Plus } from 'lucide-react'
+import { Loader2, Percent, Ruler, Save, Wrench, Check, ChevronsUpDown, Plus } from 'lucide-react'
 import {
 import {
   Select,
   Select,
   SelectContent,
   SelectContent,
@@ -90,6 +91,12 @@ export function WorkshopSettings({
   const [workDayEnd, setWorkDayEnd] = useState(
   const [workDayEnd, setWorkDayEnd] = useState(
     settings[SETTING_KEYS.WORKBOARD_WORK_DAY_END] || '15:00'
     settings[SETTING_KEYS.WORKBOARD_WORK_DAY_END] || '15:00'
   )
   )
+  const [defaultMarkupPercent, setDefaultMarkupPercent] = useState(
+    settings[SETTING_KEYS.PARTS_DEFAULT_MARKUP_PERCENT] || '0'
+  )
+  const [markupAppliesToInventory, setMarkupAppliesToInventory] = useState(
+    settings[SETTING_KEYS.PARTS_MARKUP_APPLIES_TO_INVENTORY] === 'true'
+  )
 
 
   const selectedTechName = technicians.find((t) => t.id === defaultTechnicianId)?.name || ''
   const selectedTechName = technicians.find((t) => t.id === defaultTechnicianId)?.name || ''
 
 
@@ -147,6 +154,8 @@ export function WorkshopSettings({
       [SETTING_KEYS.INVENTORY_DEFAULT_UNIT]: defaultUnit.trim(),
       [SETTING_KEYS.INVENTORY_DEFAULT_UNIT]: defaultUnit.trim(),
       [SETTING_KEYS.WORKBOARD_WORK_DAY_START]: workDayStart,
       [SETTING_KEYS.WORKBOARD_WORK_DAY_START]: workDayStart,
       [SETTING_KEYS.WORKBOARD_WORK_DAY_END]: workDayEnd,
       [SETTING_KEYS.WORKBOARD_WORK_DAY_END]: workDayEnd,
+      [SETTING_KEYS.PARTS_DEFAULT_MARKUP_PERCENT]: defaultMarkupPercent,
+      [SETTING_KEYS.PARTS_MARKUP_APPLIES_TO_INVENTORY]: markupAppliesToInventory ? 'true' : 'false',
     })
     })
     setSaving(false)
     setSaving(false)
     router.refresh()
     router.refresh()
@@ -317,6 +326,53 @@ export function WorkshopSettings({
 
 
           <Separator />
           <Separator />
 
 
+          {/* Pricing, not paperwork: the markup decides what a part costs the
+              customer, which is settled here on the job, long before an
+              invoice exists. */}
+          <div className="space-y-4">
+            <div className="flex flex-row items-center gap-3">
+              <Percent className="h-5 w-5 text-muted-foreground" />
+              <h3 className="text-lg font-semibold">{t('workshop.partsMarkupTitle')}</h3>
+            </div>
+            <p className="text-sm text-muted-foreground">{t('workshop.partsMarkupDescription')}</p>
+            <div className="grid gap-4 sm:grid-cols-2">
+              <div className="space-y-2">
+                <Label htmlFor="defaultMarkupPercent">{t('workshop.defaultMarkupPercent')}</Label>
+                <Input
+                  id="defaultMarkupPercent"
+                  type="number"
+                  min="0"
+                  step="0.1"
+                  placeholder="0"
+                  value={defaultMarkupPercent}
+                  onChange={(e) => setDefaultMarkupPercent(e.target.value)}
+                  className="w-32"
+                />
+                <p className="text-sm text-muted-foreground">
+                  {t('workshop.defaultMarkupPercentHint')}
+                </p>
+              </div>
+              <div className="space-y-2">
+                <Label
+                  htmlFor="markupAppliesToInventory"
+                  className="flex items-center justify-between gap-3"
+                >
+                  <span>{t('workshop.markupAppliesToInventory')}</span>
+                  <Switch
+                    id="markupAppliesToInventory"
+                    checked={markupAppliesToInventory}
+                    onCheckedChange={setMarkupAppliesToInventory}
+                  />
+                </Label>
+                <p className="text-sm text-muted-foreground">
+                  {t('workshop.markupAppliesToInventoryHint')}
+                </p>
+              </div>
+            </div>
+          </div>
+
+          <Separator />
+
           <div className="space-y-4">
           <div className="space-y-4">
             <div className="flex flex-row items-center gap-3">
             <div className="flex flex-row items-center gap-3">
               <Ruler className="h-5 w-5 text-muted-foreground" />
               <Ruler className="h-5 w-5 text-muted-foreground" />

+ 13 - 1
src/features/customers/Actions/customerActions.ts

@@ -239,6 +239,19 @@ export async function deleteCustomers(customerIds: string[]) {
   )
   )
 }
 }
 
 
+/** How many customers still have no number: what the list's assign button shows. */
+export async function countUnnumberedCustomers() {
+  return withAuth(
+    async ({ organizationId }) =>
+      db.customer.count({ where: { organizationId, customerNumber: null } }),
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS },
+      ],
+    }
+  )
+}
+
 /**
 /**
  * Assigns sequential numbers to every customer that has none, oldest first,
  * Assigns sequential numbers to every customer that has none, oldest first,
  * continuing after the highest existing numeric number (min 1001). Customers
  * continuing after the highest existing numeric number (min 1001). Customers
@@ -275,7 +288,6 @@ export async function backfillCustomerNumbers() {
       )
       )
 
 
       revalidatePath('/customers')
       revalidatePath('/customers')
-      revalidatePath('/settings/invoice')
       return { assigned: unnumbered.length }
       return { assigned: unnumbered.length }
     },
     },
     {
     {