diff --git a/messages/de.json b/messages/de.json index ec9b268..9f2d718 100644 --- a/messages/de.json +++ b/messages/de.json @@ -970,6 +970,213 @@ "defaultTaxRateInvalid": "Standard-Steuersatz muss zwischen 0% und 100% liegen", "noticeTooLong": "Hinweistext darf maximal 1000 Zeichen lang sein" } + }, + "contracts": { + "title": "Vertragsvorlagen & Studio-Signatur", + "description": "Verwalten Sie rechtssichere Vertragsvorlagen, zweisprachige Klauseln, dynamische Platzhalter und Studio-Signaturdaten.", + "navLabel": "Verträge & Signatur", + "manage": "Vertragsvorlagen & Signatur verwalten", + "breadcrumbs": { + "settings": "Studio-Einstellungen", + "contracts": "Verträge & Signatur" + }, + "tabs": { + "tablistAria": "Vertragseinstellungen-Tabs", + "templates": "Vertragsvorlagen", + "signing": "Studio-Signaturprofil" + }, + "list": { + "title": "Vertragsvorlagen", + "description": "Wiederverwendbare Vertragsvorlagen mit dynamischen Platzhaltern für Kundenvereinbarungen.", + "createButton": "Neue Vorlage", + "filterAll": "Alle Projekttypen", + "filterByType": "Nach Projekttyp filtern", + "filterSearchPlaceholder": "Vorlagen durchsuchen...", + "activeCount": "{count, plural, =1 {1 aktive Vorlage} other {# aktive Vorlagen}}", + "archivedCount": "{count, plural, =1 {1 archivierte Vorlage} other {# archivierte Vorlagen}}", + "showArchived": "Archivierte anzeigen ({count})", + "hideArchived": "Archivierte ausblenden", + "emptyActive": "Noch keine aktiven Vertragsvorlagen vorhanden. Erstellen Sie Ihre erste Vertragsvorlage, um Angebote und Verträge schneller zu verfassen.", + "emptyArchived": "Keine archivierten Vertragsvorlagen.", + "table": { + "name": "Vorlagenname", + "slug": "Slug", + "projectType": "Projekttyp", + "status": "Status", + "actions": "Aktionen" + }, + "projectTypeNone": "Allgemein / Alle", + "projectTypes": { + "wedding": "Hochzeit", + "portrait": "Porträt", + "commercial": "Kommerziell", + "event": "Event", + "video": "Video", + "engagement": "Verlobung", + "editorial": "Editorial" + }, + "defaultBadge": "Standard", + "activeBadge": "Aktiv", + "archivedBadge": "Archiviert", + "editAction": "Vorlage bearbeiten", + "duplicateAction": "Duplizieren", + "archiveAction": "Vorlage archivieren", + "restoreAction": "Vorlage wiederherstellen", + "archiveDialog": { + "title": "Vertragsvorlage archivieren", + "description": "Sind Sie sicher, dass Sie \"{name}\" archivieren möchten? Bereits abgeschlossene Verträge bleiben unberührt, diese Vorlage steht jedoch für neue Verträge nicht mehr zur Verfügung.", + "confirmButton": "Vorlage archivieren", + "cancelButton": "Abbrechen" + } + }, + "modal": { + "createTitle": "Vertragsvorlage erstellen", + "editTitle": "Vertragsvorlage bearbeiten", + "duplicateTitle": "Vertragsvorlage duplizieren", + "description": "Legen Sie Vorlagendetails, Projekttyp-Zuordnung und zweisprachige Klauseln mit dynamischen Platzhaltern fest.", + "nameLabel": "Vorlagenname", + "namePlaceholder": "z. B. Vertrag über Hochzeitsfotografie", + "slugLabel": "Slug", + "slugPlaceholder": "z. B. vertrag-hochzeitsfotografie", + "slugHelp": "Nur Kleinbuchstaben, Ziffern und Bindestriche", + "projectTypeLabel": "Projekttyp", + "projectTypePlaceholder": "Projekttyp auswählen (optional)", + "projectTypeNone": "Keine (Allgemein)", + "descriptionLabel": "Beschreibung", + "descriptionPlaceholder": "Interne Beschreibung des Anwendungsbereichs", + "defaultToggleLabel": "Standardvorlage für diesen Projekttyp", + "defaultToggleHelp": "Wird bei der Vertragserstellung für diesen Projekttyp automatisch vorausgewählt", + "clauseEditorTitle": "Vertragsklauseln", + "clauseEditorHelp": "Klicken Sie auf einen Platzhalter unten, um ihn an der Cursorposition einzufügen.", + "tabEnglish": "Englische Klauseln (en)", + "tabGerman": "Deutsche Klauseln (de)", + "contentEnLabel": "Englische Vertragsklauseln", + "contentEnPlaceholder": "Vertragsklauseln auf Englisch eingeben (Markdown unterstützt)...", + "contentDeLabel": "Deutsche Vertragsklauseln", + "contentDePlaceholder": "Vertragsklauseln auf Deutsch eingeben (Markdown unterstützt)...", + "tokenInsertedAnnouncement": "Platzhalter {token} eingefügt", + "tokenPaletteTitle": "Platzhalter einfügen", + "previewTitle": "Live-Vorschau", + "previewBadge": "Beispieldaten-Vorschau", + "previewEmpty": "Noch keine Vertragsklauseln eingegeben. Geben Sie oben Text ein, um eine Live-Vorschau mit Beispieldaten zu sehen.", + "saveButton": "Vorlage speichern", + "savingButton": "Speichern...", + "cancelButton": "Abbrechen", + "errors": { + "nameRequired": "Vorlagenname ist erforderlich", + "nameTooLong": "Vorlagenname darf maximal 150 Zeichen lang sein", + "slugRequired": "Slug ist erforderlich", + "slugInvalid": "Slug darf nur aus Kleinbuchstaben und Ziffern mit einfachen Bindestrichen bestehen", + "slugTooLong": "Slug darf maximal 100 Zeichen lang sein", + "descriptionTooLong": "Beschreibung darf maximal 1.000 Zeichen lang sein", + "contentEnRequired": "Englische Vertragsklauseln sind erforderlich", + "contentDeRequired": "Deutsche Vertragsklauseln sind erforderlich" + } + }, + "signing": { + "title": "Studio-Signatur & Gegenzeichnungsprofil", + "description": "Konfigurieren Sie die zeichnungsberechtigte Person des Studios, offizielle Berufsbezeichnung, Anschrift und den Gegenzeichnungsprozess.", + "signatorySectionTitle": "Signaturprofil des Studios", + "signatoryNameLabel": "Rechtlicher Name des Unterzeichners", + "signatoryNamePlaceholder": "z. B. Elena Rostova", + "signatoryNameHelp": "Vollständiger Name auf Verträgen und in der elektronischen Signaturzeile", + "signatoryTitleLabel": "Berufsbezeichnung / Funktion", + "signatoryTitlePlaceholder": "z. B. Studioinhaberin & leitende Fotografin", + "signatoryTitleHelp": "Gesetzliche Funktionsbezeichnung unter der Unterschrift des Studios", + "modeSectionTitle": "Gegenzeichnungs-Modus", + "modeSectionHelp": "Legen Sie fest, wie Verträge vom Studio gegengezeichnet werden.", + "modes": { + "preSigned": { + "label": "Automatisch (Vorab gegengezeichnet)", + "description": "Die Signatur des Studios wird beim Versand oder bei Kundenunterzeichnung automatisch angebracht." + }, + "explicit": { + "label": "Explizit (Manuelle Gegenzeichnung)", + "description": "Der Studioinhaber muss nach der Unterschrift des Kunden manuell gegenzeichnen, um den Vertrag abzuschließen." + } + }, + "addressSectionTitle": "Studio-Geschäftsadresse & gesetzliche Kontaktdaten", + "addressSectionHelp": "Physische Geschäftsadresse und Kontaktdaten für gesetzliche Pflichtangaben (BGB § 126b, DDG § 5).", + "addressLine1Label": "Straße und Hausnummer", + "addressLine1Placeholder": "z. B. Kastanienallee 71", + "addressLine2Label": "Adresszusatz", + "addressLine2Placeholder": "z. B. Studio 4B (optional)", + "postalCodeLabel": "Postleitzahl", + "postalCodePlaceholder": "z. B. 10435", + "cityLabel": "Stadt", + "cityPlaceholder": "z. B. Berlin", + "countryCodeLabel": "Ländercode (ISO 3166-1 Alpha-2)", + "countryCodePlaceholder": "DE", + "phoneLabel": "Telefonnummer des Studios", + "phonePlaceholder": "z. B. +49 30 12345678", + "emailLabel": "Offizielle E-Mail-Adresse des Studios", + "emailPlaceholder": "z. B. studio@example.com", + "saveButton": "Signatur-Einstellungen speichern", + "savingButton": "Speichern...", + "successNotification": "Studio-Signatur-Einstellungen wurden erfolgreich aktualisiert.", + "errors": { + "signatoryNameTooLong": "Unterzeichnername darf maximal 200 Zeichen lang sein", + "signatoryTitleTooLong": "Berufsbezeichnung darf maximal 100 Zeichen lang sein", + "countryCodeInvalid": "Ländercode muss genau 2 Zeichen lang sein (z. B. DE)", + "emailInvalid": "Bitte geben Sie eine gültige E-Mail-Adresse ein" + } + }, + "tokens": { + "categories": { + "client": "Kundendaten", + "project": "Projekt & Auftrag", + "shoot": "Termin- & Eventlogistik", + "package": "Paket & Vergütung", + "studio": "Studio & Rechtsträger", + "contract": "Vertragsdaten" + }, + "client": { + "name": "Vollständiger Kundenname", + "company": "Kunden-Unternehmensname", + "email": "Kunden-E-Mail-Adresse", + "phone": "Kunden-Telefonnummer", + "address": "Kunden-Straße & Hausnummer", + "city": "Kunden-Stadt", + "postal_code": "Kunden-Postleitzahl", + "country": "Kunden-Land" + }, + "project": { + "title": "Projekttitel", + "type": "Projekttyp", + "price": "Projektpreis", + "currency": "Währung", + "notes": "Projektnotizen" + }, + "shoot": { + "date": "Shooting- / Eventdatum", + "time": "Shooting-Uhrzeit (Start & Ende)", + "timezone": "Zeitzone", + "location": "Shooting-Ort / Location", + "address": "Physische Adresse der Location" + }, + "package": { + "name": "Name des Leistungspakets", + "deliverables": "Übersicht enthaltener Leistungen", + "total": "Gesamtbetrag (inkl. Steuern)", + "subtotal": "Netto-Zwischensumme", + "tax": "Anfallender Steuerbetrag", + "addons": "Ausgewählte Zusatzleistungen" + }, + "studio": { + "name": "Name des Fotostudios", + "owner_name": "Name des Studioinhabers", + "email": "Offizielle E-Mail des Studios", + "phone": "Telefonnummer des Studios", + "address": "Physische Adresse des Studios", + "tax_number": "Steuernummer des Studios", + "vat_id": "USt-IdNr. des Studios" + }, + "contract": { + "number": "Vertragsnummer (CTR-YYYY-NNNN)", + "issue_date": "Vertragsausstellungsdatum", + "valid_until": "Vertragsgültigkeitsdatum" + } + } } }, "clients": { diff --git a/messages/en.json b/messages/en.json index bd05945..ffc5e0c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -970,6 +970,213 @@ "defaultTaxRateInvalid": "Default tax rate must be between 0% and 100%", "noticeTooLong": "Notice text cannot exceed 1000 characters" } + }, + "contracts": { + "title": "Contract Templates & Studio Signing", + "description": "Manage legally binding contract agreements, localized bilingual clauses, merge tokens, and studio signatory credentials.", + "navLabel": "Contracts & Signing", + "manage": "Manage Contract Templates & Signing", + "breadcrumbs": { + "settings": "Studio Settings", + "contracts": "Contracts & Signing" + }, + "tabs": { + "tablistAria": "Contract settings tabs", + "templates": "Contract Templates", + "signing": "Studio Signatory Profile" + }, + "list": { + "title": "Contract Templates", + "description": "Reusable contract agreements with dynamic merge tokens for client agreements.", + "createButton": "New Template", + "filterAll": "All Project Types", + "filterByType": "Filter by project type", + "filterSearchPlaceholder": "Search templates...", + "activeCount": "{count, plural, =1 {1 Active Template} other {# Active Templates}}", + "archivedCount": "{count, plural, =1 {1 Archived Template} other {# Archived Templates}}", + "showArchived": "Show Archived ({count})", + "hideArchived": "Hide Archived", + "emptyActive": "No active contract templates configured yet. Create your first contract template to speed up proposal and contract drafting.", + "emptyArchived": "No archived contract templates.", + "table": { + "name": "Template Name", + "slug": "Slug", + "projectType": "Project Type", + "status": "Status", + "actions": "Actions" + }, + "projectTypeNone": "General / All", + "projectTypes": { + "wedding": "Wedding", + "portrait": "Portrait", + "commercial": "Commercial", + "event": "Event", + "video": "Video", + "engagement": "Engagement", + "editorial": "Editorial" + }, + "defaultBadge": "Default", + "activeBadge": "Active", + "archivedBadge": "Archived", + "editAction": "Edit Template", + "duplicateAction": "Duplicate", + "archiveAction": "Archive Template", + "restoreAction": "Restore Template", + "archiveDialog": { + "title": "Archive Contract Template", + "description": "Are you sure you want to archive \"{name}\"? Existing executed contracts will remain untouched, but this template will no longer be available for new contract drafting.", + "confirmButton": "Archive Template", + "cancelButton": "Cancel" + } + }, + "modal": { + "createTitle": "Create Contract Template", + "editTitle": "Edit Contract Template", + "duplicateTitle": "Duplicate Contract Template", + "description": "Define template details, project type assignment, and localized clauses with dynamic merge tokens.", + "nameLabel": "Template Name", + "namePlaceholder": "e.g. Wedding Photography Agreement", + "slugLabel": "Slug", + "slugPlaceholder": "e.g. wedding-photography-agreement", + "slugHelp": "Lowercase letters, numbers, and hyphens only", + "projectTypeLabel": "Project Type", + "projectTypePlaceholder": "Select project type (optional)", + "projectTypeNone": "None (General)", + "descriptionLabel": "Description", + "descriptionPlaceholder": "Internal description of the agreement's scope or purpose", + "defaultToggleLabel": "Default template for this project type", + "defaultToggleHelp": "Automatically pre-selected when composing contracts for this project type", + "clauseEditorTitle": "Contract Clauses", + "clauseEditorHelp": "Click any merge token below to insert it at your cursor position.", + "tabEnglish": "English Clauses (en)", + "tabGerman": "German Clauses (de)", + "contentEnLabel": "English Contract Clauses", + "contentEnPlaceholder": "Enter contract agreement clauses in English (Markdown supported)...", + "contentDeLabel": "German Contract Clauses", + "contentDePlaceholder": "Vertragsklauseln auf Deutsch eingeben (Markdown wird unterstützt)...", + "tokenInsertedAnnouncement": "Token {token} inserted", + "tokenPaletteTitle": "Insert Merge Tokens", + "previewTitle": "Live Merge Preview", + "previewBadge": "Sample Data Preview", + "previewEmpty": "No contract clauses entered yet. Type clauses above to see a live preview interpolated with sample commission data.", + "saveButton": "Save Template", + "savingButton": "Saving...", + "cancelButton": "Cancel", + "errors": { + "nameRequired": "Template name is required", + "nameTooLong": "Template name must not exceed 150 characters", + "slugRequired": "Slug is required", + "slugInvalid": "Slug must consist of lowercase alphanumeric words separated by single hyphens", + "slugTooLong": "Slug must not exceed 100 characters", + "descriptionTooLong": "Description must not exceed 1,000 characters", + "contentEnRequired": "English contract clauses are required", + "contentDeRequired": "German contract clauses are required" + } + }, + "signing": { + "title": "Studio Signatory & Countersignature Profile", + "description": "Configure the studio's legal signatory, official business title, physical contact address, and countersignature workflow.", + "signatorySectionTitle": "Legal Signatory Profile", + "signatoryNameLabel": "Signatory Legal Name", + "signatoryNamePlaceholder": "e.g. Elena Rostova", + "signatoryNameHelp": "Full legal name displayed on contracts and electronic signature blocks", + "signatoryTitleLabel": "Professional Title / Role", + "signatoryTitlePlaceholder": "e.g. Studio Owner & Lead Photographer", + "signatoryTitleHelp": "Statutory role shown under studio signature line", + "modeSectionTitle": "Countersignature Mode", + "modeSectionHelp": "Choose how the studio countersigns contracts when issuing them to clients.", + "modes": { + "preSigned": { + "label": "Automatic (Pre-signed)", + "description": "Studio countersignature is automatically applied when the contract is issued or signed by the client." + }, + "explicit": { + "label": "Explicit (Manual Countersignature)", + "description": "Studio owner must manually sign after the client has signed to finalize execution." + } + }, + "addressSectionTitle": "Studio Business Address & Statutory Contact", + "addressSectionHelp": "Physical studio premises and contact information rendered in contract legal notices (BGB § 126b, DDG § 5).", + "addressLine1Label": "Street Address Line 1", + "addressLine1Placeholder": "e.g. Kastanienallee 71", + "addressLine2Label": "Street Address Line 2", + "addressLine2Placeholder": "e.g. Studio 4B (Optional)", + "postalCodeLabel": "Postal Code", + "postalCodePlaceholder": "e.g. 10435", + "cityLabel": "City", + "cityPlaceholder": "e.g. Berlin", + "countryCodeLabel": "Country Code (ISO 3166-1 alpha-2)", + "countryCodePlaceholder": "DE", + "phoneLabel": "Studio Telephone", + "phonePlaceholder": "e.g. +49 30 12345678", + "emailLabel": "Studio Official Email", + "emailPlaceholder": "e.g. studio@example.com", + "saveButton": "Save Signing Settings", + "savingButton": "Saving...", + "successNotification": "Studio signing settings updated successfully.", + "errors": { + "signatoryNameTooLong": "Signatory name must not exceed 200 characters", + "signatoryTitleTooLong": "Signatory title must not exceed 100 characters", + "countryCodeInvalid": "Country code must be exactly 2 characters (e.g. DE)", + "emailInvalid": "Please enter a valid email address" + } + }, + "tokens": { + "categories": { + "client": "Client Information", + "project": "Project & Commission", + "shoot": "Event & Shoot Logistics", + "package": "Package & Pricing", + "studio": "Studio & Legal Entity", + "contract": "Contract Metadata" + }, + "client": { + "name": "Full Client Name", + "company": "Client Company Name", + "email": "Client Email Address", + "phone": "Client Phone Number", + "address": "Client Street Address", + "city": "Client City", + "postal_code": "Client Postal Code", + "country": "Client Country" + }, + "project": { + "title": "Project Title", + "type": "Project Type", + "price": "Project Price", + "currency": "Currency", + "notes": "Project Scope Notes" + }, + "shoot": { + "date": "Shoot / Event Date", + "time": "Shoot Start & End Time", + "timezone": "Timezone", + "location": "Shoot Location / Venue", + "address": "Venue Physical Address" + }, + "package": { + "name": "Service Package Name", + "deliverables": "Included Deliverables Summary", + "total": "Package Total Price (incl. Tax)", + "subtotal": "Package Net Subtotal", + "tax": "Applicable VAT / Tax Amount", + "addons": "Selected Add-ons Summary" + }, + "studio": { + "name": "Studio Business Name", + "owner_name": "Studio Owner Name", + "email": "Studio Official Email", + "phone": "Studio Telephone", + "address": "Studio Physical Address", + "tax_number": "Studio Tax Number (Steuernummer)", + "vat_id": "Studio VAT ID (USt-IdNr.)" + }, + "contract": { + "number": "Contract Number (CTR-YYYY-NNNN)", + "issue_date": "Contract Issue Date", + "valid_until": "Contract Validity Expiry Date" + } + } } }, "clients": { diff --git a/src/app/[locale]/settings/contracts/page.tsx b/src/app/[locale]/settings/contracts/page.tsx new file mode 100644 index 0000000..7e5ef78 --- /dev/null +++ b/src/app/[locale]/settings/contracts/page.tsx @@ -0,0 +1,32 @@ +import { redirect } from 'next/navigation'; +import { requireOwner } from '@/server/auth/guards'; +import { listContractTemplates } from '@/server/contracts/template-repo'; +import { getStudioSigningProfile } from '@/server/studio/signing-settings'; +import { SettingsNav } from '@/components/settings/settings-nav'; +import { ContractsSettingsView } from '@/components/settings/contracts-settings-view'; + +export default async function ContractsSettingsPage({ + params, +}: { + params: Promise<{ locale: string }>; +}) { + const { locale } = await params; + + try { + await requireOwner(); + } catch { + redirect(`/${locale}/sign-in?callbackUrl=/${locale}/settings/contracts`); + } + + const [templates, signingProfile] = await Promise.all([ + listContractTemplates(), + getStudioSigningProfile(), + ]); + + return ( +
+ + +
+ ); +} diff --git a/src/app/[locale]/settings/page.tsx b/src/app/[locale]/settings/page.tsx index ce59ebf..ab5c8f2 100644 --- a/src/app/[locale]/settings/page.tsx +++ b/src/app/[locale]/settings/page.tsx @@ -1,6 +1,14 @@ import { useTranslations } from 'next-intl'; import { Link } from '@/i18n/navigation'; -import { Shield, Cloud, ClipboardList, CalendarClock, CalendarSync, Package } from 'lucide-react'; +import { + Shield, + Cloud, + ClipboardList, + CalendarClock, + CalendarSync, + Package, + FileText, +} from 'lucide-react'; export default function SettingsPage() { const t = useTranslations('pages.settings'); @@ -9,6 +17,7 @@ export default function SettingsPage() { const tInq = useTranslations('settings.inquiryForms'); const tAvail = useTranslations('settings.availability'); const tPkg = useTranslations('settings.packages'); + const tContract = useTranslations('settings.contracts'); return (
@@ -35,6 +44,25 @@ export default function SettingsPage() {
+ +
+
+
+
+ + {tContract('manage')} + +
+