Compare commits
3 Commits
2e5af64736
...
94dac61941
| Author | SHA1 | Date | |
|---|---|---|---|
| 94dac61941 | |||
| fb6caf84de | |||
|
|
e3e1df6ef9 |
@ -24,6 +24,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
const [type, setType] = useState<'contract' | 'invoice' | 'other'>('contract');
|
||||
const [contractType, setContractType] = useState<'contract' | 'gdpr' | 'abo'>('contract');
|
||||
const [userType, setUserType] = useState<'personal' | 'company' | 'both'>('personal');
|
||||
const [taxMode, setTaxMode] = useState<'standard' | 'reverse_charge' | 'both'>('both');
|
||||
const [description, setDescription] = useState<string>('');
|
||||
|
||||
const [editingMeta, setEditingMeta] = useState<{ id: string; version: number; state: string } | null>(null);
|
||||
@ -38,6 +39,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
setName('');
|
||||
setHtmlCode('');
|
||||
setDescription('');
|
||||
setTaxMode('both');
|
||||
setIsPreview(false);
|
||||
setEditingMeta(null);
|
||||
};
|
||||
@ -60,6 +62,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
setType(((tpl.type as any) || 'contract') as 'contract' | 'invoice' | 'other');
|
||||
setContractType(((tpl.contract_type as any) || 'contract') as 'contract' | 'gdpr' | 'abo');
|
||||
setUserType(((tpl.user_type as any) || 'both') as 'personal' | 'company' | 'both');
|
||||
setTaxMode(((tpl.tax_mode as any) || 'both') as 'standard' | 'reverse_charge' | 'both');
|
||||
setEditingMeta({
|
||||
id: editingTemplateId,
|
||||
version: Number(tpl.version || 1),
|
||||
@ -175,6 +178,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
contract_type: type === 'contract' ? contractType : null,
|
||||
lang,
|
||||
user_type: userType,
|
||||
tax_mode: type === 'invoice' ? taxMode : null,
|
||||
descriptionLength: description ? description.length : 0,
|
||||
htmlLength: html.length,
|
||||
});
|
||||
@ -193,6 +197,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
name,
|
||||
type,
|
||||
contract_type: type === 'contract' ? contractType : undefined,
|
||||
tax_mode: type === 'invoice' ? taxMode : undefined,
|
||||
lang,
|
||||
description: description || undefined,
|
||||
user_type: userType,
|
||||
@ -210,6 +215,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
name,
|
||||
type,
|
||||
contract_type: type === 'contract' ? contractType : undefined,
|
||||
tax_mode: type === 'invoice' ? taxMode : undefined,
|
||||
lang,
|
||||
description: description || undefined,
|
||||
user_type: userType,
|
||||
@ -237,7 +243,10 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
: type === 'invoice'
|
||||
? 'Invoice'
|
||||
: 'Other';
|
||||
setPublishConfirmMessage(`This will deactivate other active ${kind} templates that apply to the same user type and language.`)
|
||||
const scope = type === 'invoice'
|
||||
? 'same user type, tax mode and language'
|
||||
: 'same user type and language';
|
||||
setPublishConfirmMessage(`This will deactivate other active ${kind} templates that apply to the ${scope}.`)
|
||||
setPublishConfirmOpen(true)
|
||||
return
|
||||
}
|
||||
@ -323,6 +332,18 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
<option value="abo">ABO</option>
|
||||
</select>
|
||||
)}
|
||||
{type === 'invoice' && (
|
||||
<select
|
||||
value={taxMode}
|
||||
onChange={(e) => setTaxMode(e.target.value as 'standard' | 'reverse_charge' | 'both')}
|
||||
required
|
||||
className="w-full sm:w-56 rounded-lg border border-gray-300 bg-white px-4 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50 shadow"
|
||||
>
|
||||
<option value="both">Invoice mode: Both / Fallback</option>
|
||||
<option value="standard">Invoice mode: Standard VAT</option>
|
||||
<option value="reverse_charge">Invoice mode: Reverse Charge</option>
|
||||
</select>
|
||||
)}
|
||||
<select
|
||||
value={userType}
|
||||
onChange={(e) => setUserType(e.target.value as 'personal' | 'company' | 'both')}
|
||||
@ -358,6 +379,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900">
|
||||
<p className="font-semibold">{t('autofix.k221fa311')}</p>
|
||||
<p className="mt-1">{t('autofix.kb791958e')}</p>
|
||||
<p className="mt-1">Use the invoice mode dropdown to separate standard VAT and reverse-charge company templates. Keep "Both / Fallback" only for a shared default layout.</p>
|
||||
<p className="mt-1">Important: include <span className="font-semibold">itemsHtml</span>{t('autofix.k7a3a6ea3')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -19,6 +19,7 @@ type ContractTemplate = {
|
||||
type?: string;
|
||||
contract_type?: string | null;
|
||||
user_type?: string | null;
|
||||
tax_mode?: string | null;
|
||||
lang?: string | null;
|
||||
version: number;
|
||||
status: 'draft' | 'published' | 'archived' | string;
|
||||
@ -29,6 +30,7 @@ type ContractTemplate = {
|
||||
type TemplateFamilyKey = 'contract' | 'invoice' | 'other';
|
||||
type ContractTypeKey = 'contract' | 'gdpr' | 'abo';
|
||||
type AudienceKey = 'personal' | 'company' | 'both';
|
||||
type InvoiceTaxModeKey = 'standard' | 'reverse_charge' | 'both';
|
||||
|
||||
type TemplateLanguageColumn = {
|
||||
key: string;
|
||||
@ -91,6 +93,7 @@ type NormalizedTemplate = DocumentTemplate & {
|
||||
updated_at?: string;
|
||||
contractType?: string | null;
|
||||
userType?: string | null;
|
||||
taxMode?: string | null;
|
||||
};
|
||||
|
||||
const FAMILY_ORDER: TemplateFamilyKey[] = ['contract', 'invoice', 'other'];
|
||||
@ -175,6 +178,11 @@ function normalizeUserType(userType?: string | null): AudienceKey {
|
||||
return ['personal', 'company', 'both'].includes(value) ? (value as AudienceKey) : 'both';
|
||||
}
|
||||
|
||||
function normalizeInvoiceTaxMode(taxMode?: string | null): InvoiceTaxModeKey {
|
||||
const value = (taxMode || 'both').toLowerCase();
|
||||
return ['standard', 'reverse_charge', 'both'].includes(value) ? (value as InvoiceTaxModeKey) : 'both';
|
||||
}
|
||||
|
||||
function normalizeLanguage(lang?: string | null) {
|
||||
const value = (lang || '').toLowerCase();
|
||||
return value === 'de' || value === 'en' ? value : 'other';
|
||||
@ -217,6 +225,17 @@ function formatLanguageCode(lang?: string | null) {
|
||||
}
|
||||
}
|
||||
|
||||
function formatInvoiceTaxMode(taxMode?: string | null) {
|
||||
switch (normalizeInvoiceTaxMode(taxMode)) {
|
||||
case 'standard':
|
||||
return 'Standard VAT';
|
||||
case 'reverse_charge':
|
||||
return 'Reverse Charge';
|
||||
default:
|
||||
return 'Fallback / Both';
|
||||
}
|
||||
}
|
||||
|
||||
function formatContractType(contractType?: string | null) {
|
||||
switch (normalizeContractType(contractType)) {
|
||||
case 'abo':
|
||||
@ -272,6 +291,7 @@ function buildTrackKey(template: ContractTemplate) {
|
||||
family,
|
||||
normalizeContractType(template.contract_type),
|
||||
normalizeUserType(template.user_type),
|
||||
family === 'invoice' ? normalizeInvoiceTaxMode(template.tax_mode) : 'default',
|
||||
normalizeName(template.name),
|
||||
].join(':');
|
||||
}
|
||||
@ -310,7 +330,7 @@ function buildTrack(templates: ContractTemplate[]): TemplateTrack {
|
||||
|
||||
if (family === 'invoice') {
|
||||
title = names.length === 1 ? names[0] : 'Invoice Templates';
|
||||
subtitle = `${formatUserType(lead?.user_type)} • ${sortedTemplates.length} version${sortedTemplates.length === 1 ? '' : 's'}`;
|
||||
subtitle = `${formatUserType(lead?.user_type)} • ${formatInvoiceTaxMode(lead?.tax_mode)} • ${sortedTemplates.length} version${sortedTemplates.length === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
return {
|
||||
@ -520,11 +540,13 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
item.type || '',
|
||||
item.contract_type || '',
|
||||
item.user_type || '',
|
||||
item.tax_mode || '',
|
||||
item.lang || '',
|
||||
item.status,
|
||||
`v${item.version}`,
|
||||
formatContractType(item.contract_type),
|
||||
formatUserType(item.user_type),
|
||||
formatInvoiceTaxMode(item.tax_mode),
|
||||
formatLanguage(item.lang),
|
||||
]
|
||||
.join(' ')
|
||||
@ -614,6 +636,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
type: normalized.type,
|
||||
contract_type: normalized.contract_type ?? normalized.contractType ?? null,
|
||||
user_type: normalized.user_type ?? normalized.userType ?? null,
|
||||
tax_mode: normalized.tax_mode ?? normalized.taxMode ?? null,
|
||||
lang: normalized.lang ?? null,
|
||||
version: Number(normalized.version ?? 1),
|
||||
status: normalized.state === 'active' ? 'published' : 'draft',
|
||||
@ -627,7 +650,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
setItems((prev) => prev.length ? prev : [
|
||||
{ id: 'ex1', name: 'Standard Subscription Agreement', type: 'contract', contract_type: 'abo', user_type: 'personal', lang: 'de', version: 2, status: 'published', updatedAt: new Date().toISOString() },
|
||||
{ id: 'ex2', name: 'Standard Subscription Agreement', type: 'contract', contract_type: 'abo', user_type: 'personal', lang: 'de', version: 1, status: 'draft', updatedAt: new Date().toISOString() },
|
||||
{ id: 'ex3', name: 'Invoice Standard', type: 'invoice', user_type: 'both', lang: 'en', version: 3, status: 'published', updatedAt: new Date().toISOString() },
|
||||
{ id: 'ex3', name: 'Invoice Standard', type: 'invoice', user_type: 'both', tax_mode: 'both', lang: 'en', version: 3, status: 'published', updatedAt: new Date().toISOString() },
|
||||
]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
@ -665,11 +688,14 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
: tpl.type === 'invoice'
|
||||
? 'Invoice'
|
||||
: 'Other';
|
||||
const scope = tpl.type === 'invoice'
|
||||
? 'same user type, tax mode and language'
|
||||
: 'same user type and language';
|
||||
setPendingToggle({
|
||||
id,
|
||||
target,
|
||||
requiresConfirm: true,
|
||||
message: `This will deactivate other active ${kind} templates that apply to the same user type and language.`,
|
||||
message: `This will deactivate other active ${kind} templates that apply to the ${scope}.`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@ -734,6 +760,9 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
{track.family === 'invoice' && track.templates[0]?.user_type && (
|
||||
<Pill className="border-sky-200 bg-white/90 text-sky-800">{formatUserType(track.templates[0].user_type)}</Pill>
|
||||
)}
|
||||
{track.family === 'invoice' && (
|
||||
<Pill className="border-sky-200 bg-sky-100 text-sky-900">{formatInvoiceTaxMode(track.templates[0]?.tax_mode)}</Pill>
|
||||
)}
|
||||
{track.family === 'other' && track.templates[0]?.user_type && (
|
||||
<Pill className="border-slate-200 bg-slate-100 text-slate-700">{formatUserType(track.templates[0].user_type)}</Pill>
|
||||
)}
|
||||
@ -962,7 +991,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
||||
|
||||
{activeFamily.key === 'invoice' && (
|
||||
<div className="mt-4 rounded-2xl border border-sky-200 bg-sky-50 px-4 py-3 text-sm text-sky-900">
|
||||
Invoice templates can target Personal, Company, or Both. Keep one active version per language and user type so the invoice flow stays predictable.
|
||||
Invoice templates can target Personal, Company, or Both. Keep one active version per language, user type and invoice mode so the invoice flow stays predictable.
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ export type DocumentTemplate = {
|
||||
contract_type?: 'contract' | 'gdpr' | 'abo' | null | string;
|
||||
lang?: 'en' | 'de' | string;
|
||||
user_type?: 'personal' | 'company' | 'both' | string;
|
||||
tax_mode?: 'standard' | 'reverse_charge' | 'both' | null | string;
|
||||
state?: 'active' | 'inactive' | string;
|
||||
version?: number;
|
||||
previewUrl?: string | null;
|
||||
@ -226,6 +227,7 @@ export default function useContractManagement() {
|
||||
lang: 'en' | 'de' | string;
|
||||
description?: string;
|
||||
user_type?: 'personal' | 'company' | 'both';
|
||||
tax_mode?: 'standard' | 'reverse_charge' | 'both';
|
||||
}): Promise<DocumentTemplate> => {
|
||||
const fd = new FormData();
|
||||
const file = payload.file instanceof File ? payload.file : new File([payload.file], `${payload.name || 'template'}.html`, { type: 'text/html' });
|
||||
@ -235,6 +237,9 @@ export default function useContractManagement() {
|
||||
if (payload.type === 'contract' && payload.contract_type) {
|
||||
fd.append('contract_type', payload.contract_type);
|
||||
}
|
||||
if (payload.type === 'invoice' && payload.tax_mode) {
|
||||
fd.append('tax_mode', payload.tax_mode);
|
||||
}
|
||||
fd.append('lang', payload.lang);
|
||||
if (payload.description) fd.append('description', payload.description);
|
||||
fd.append('user_type', (payload.user_type ?? 'both'));
|
||||
@ -247,6 +252,7 @@ export default function useContractManagement() {
|
||||
willSendContractType: payload.type === 'contract' && Boolean(payload.contract_type),
|
||||
lang: payload.lang,
|
||||
user_type: payload.user_type ?? 'both',
|
||||
tax_mode: payload.type === 'invoice' ? (payload.tax_mode ?? 'both') : null,
|
||||
descriptionLength: payload.description ? payload.description.length : 0,
|
||||
file: typeof File !== 'undefined' && file instanceof File ? { name: file.name, type: file.type, size: file.size } : null,
|
||||
});
|
||||
@ -263,6 +269,7 @@ export default function useContractManagement() {
|
||||
lang?: 'en' | 'de' | string;
|
||||
description?: string;
|
||||
user_type?: 'personal' | 'company' | 'both';
|
||||
tax_mode?: 'standard' | 'reverse_charge' | 'both';
|
||||
}): Promise<DocumentTemplate> => {
|
||||
const fd = new FormData();
|
||||
if (payload.file) {
|
||||
@ -274,6 +281,9 @@ export default function useContractManagement() {
|
||||
if ((payload.type === 'contract' || payload.contract_type) && payload.contract_type) {
|
||||
fd.append('contract_type', payload.contract_type);
|
||||
}
|
||||
if ((payload.type === 'invoice' || payload.tax_mode) && payload.tax_mode) {
|
||||
fd.append('tax_mode', payload.tax_mode);
|
||||
}
|
||||
if (payload.lang) fd.append('lang', payload.lang);
|
||||
if (payload.description !== undefined) fd.append('description', payload.description);
|
||||
if (payload.user_type) fd.append('user_type', payload.user_type);
|
||||
@ -290,6 +300,7 @@ export default function useContractManagement() {
|
||||
lang?: 'en' | 'de' | string;
|
||||
description?: string;
|
||||
user_type?: 'personal' | 'company' | 'both';
|
||||
tax_mode?: 'standard' | 'reverse_charge' | 'both';
|
||||
state?: 'active' | 'inactive';
|
||||
}): Promise<DocumentTemplate> => {
|
||||
const fd = new FormData();
|
||||
@ -300,6 +311,7 @@ export default function useContractManagement() {
|
||||
if (payload.name !== undefined) fd.append('name', payload.name);
|
||||
if (payload.type !== undefined) fd.append('type', payload.type);
|
||||
if (payload.contract_type !== undefined) fd.append('contract_type', payload.contract_type);
|
||||
if (payload.tax_mode !== undefined) fd.append('tax_mode', payload.tax_mode);
|
||||
if (payload.lang !== undefined) fd.append('lang', payload.lang);
|
||||
if (payload.description !== undefined) fd.append('description', payload.description);
|
||||
if (payload.user_type !== undefined) fd.append('user_type', payload.user_type);
|
||||
@ -313,6 +325,7 @@ export default function useContractManagement() {
|
||||
contract_type: payload.contract_type,
|
||||
lang: payload.lang,
|
||||
user_type: payload.user_type,
|
||||
tax_mode: payload.tax_mode,
|
||||
state: payload.state,
|
||||
descriptionLength: payload.description ? payload.description.length : 0,
|
||||
file: typeof File !== 'undefined' && file instanceof File ? { name: file.name, type: file.type, size: file.size } : null,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user