feat: update contract and template management to streamline type handling and permissions

This commit is contained in:
seaznCode 2026-03-11 22:35:24 +01:00
parent 48c63a896f
commit f94e4669f8
4 changed files with 62 additions and 54 deletions

View File

@ -18,7 +18,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
const [statusMsg, setStatusMsg] = useState<string | null>(null);
const [lang, setLang] = useState<'en' | 'de'>('en');
const [type, setType] = useState<'contract' | 'bill' | 'invoice' | 'other'>('contract');
const [type, setType] = useState<'contract' | 'invoice' | 'other'>('contract');
const [contractType, setContractType] = useState<'contract' | 'gdpr'>('contract');
const [userType, setUserType] = useState<'personal' | 'company' | 'both'>('personal');
const [description, setDescription] = useState<string>('');
@ -54,7 +54,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
setHtmlCode(tpl.html || '');
setDescription(((tpl as any)?.description as string) || ''); // FIX: DocumentTemplate may not declare `description`
setLang((tpl.lang as any) || 'en');
setType(((tpl.type as any) || 'contract') as 'contract' | 'bill' | 'invoice' | 'other');
setType(((tpl.type as any) || 'contract') as 'contract' | 'invoice' | 'other');
setContractType(((tpl.contract_type as any) || 'contract') as 'contract' | 'gdpr');
setUserType(((tpl.user_type as any) || 'both') as 'personal' | 'company' | 'both');
setEditingMeta({
@ -214,8 +214,12 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
};
const save = async (publish: boolean) => {
if (publish && type === 'contract') {
const kind = contractType === 'gdpr' ? 'GDPR' : 'Contract';
if (publish) {
let kind = type === 'contract'
? (contractType === 'gdpr' ? 'GDPR' : 'Contract')
: type === 'invoice'
? 'Invoice'
: 'Other';
setPublishConfirmMessage(`This will deactivate other active ${kind} templates that apply to the same user type and language.`)
setPublishConfirmOpen(true)
return
@ -283,12 +287,15 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
<div className="flex flex-col sm:flex-row gap-4">
<select
value={type}
onChange={(e) => setType(e.target.value as 'contract' | 'bill' | 'invoice' | 'other')}
onChange={(e) => {
const newType = e.target.value as 'contract' | 'invoice' | 'other';
setType(newType);
if (newType === 'invoice') setUserType('both');
}}
required
className="w-full sm:w-1/3 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="contract">Contract</option>
<option value="bill">Bill</option>
<option value="invoice">Invoice</option>
<option value="other">Other</option>
</select>
@ -303,16 +310,18 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
<option value="gdpr">GDPR</option>
</select>
)}
<select
value={userType}
onChange={(e) => setUserType(e.target.value as 'personal' | 'company' | 'both')}
required
className="w-full sm:w-40 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="personal">Personal</option>
<option value="company">Company</option>
<option value="both">Both</option>
</select>
{type !== 'invoice' && (
<select
value={userType}
onChange={(e) => setUserType(e.target.value as 'personal' | 'company' | 'both')}
required
className="w-full sm:w-40 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="personal">Personal</option>
<option value="company">Company</option>
<option value="both">Both</option>
</select>
)}
<select
value={lang}
onChange={(e) => setLang(e.target.value as 'en' | 'de')}

View File

@ -98,8 +98,12 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
const target = current === 'published' ? 'inactive' : 'active';
if (target === 'active') {
const tpl = items.find((i) => i.id === id);
if (tpl?.type === 'contract') {
const kind = tpl.contract_type === 'gdpr' ? 'GDPR' : 'Contract';
if (tpl) {
const kind = tpl.type === 'contract'
? (tpl.contract_type === 'gdpr' ? 'GDPR' : 'Contract')
: tpl.type === 'invoice'
? 'Invoice'
: 'Other';
setPendingToggle({
id,
target,
@ -137,7 +141,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
return (
<div className="space-y-4">
<div className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-900">
For invoice emails, provide active invoice templates for the language/user type combinations you need (en/de × personal/company/both). If no active invoice template matches, backend falls back to text-only email.
Invoice templates always use user type &quot;Both&quot;. Provide templates for each language (en/de). If no active invoice template matches, backend falls back to a text-only invoice.
</div>
<div className="flex gap-2 items-center">
<input
@ -158,12 +162,12 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filtered.map((c) => (
<div key={c.id} className="rounded-xl border border-gray-100 bg-white shadow-sm p-4 flex flex-col gap-2 hover:shadow-md transition">
<div className="flex items-center gap-2">
<p className="font-semibold text-lg text-gray-900 truncate">{c.name}</p>
<p className="font-semibold text-lg text-gray-900 truncate">{c.name}</p>
<div className="flex flex-wrap items-center gap-2">
<StatusBadge status={c.status} />
{c.type && (
<Pill className="bg-slate-50 text-slate-800 border-slate-200">
{c.type === 'contract' ? 'Contract' : c.type === 'bill' ? 'Bill' : c.type === 'invoice' ? 'Invoice' : 'Other'}
{c.type === 'contract' ? 'Contract' : c.type === 'invoice' ? 'Invoice' : 'Other'}
</Pill>
)}
{c.type === 'contract' && (
@ -171,7 +175,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
{c.contract_type === 'gdpr' ? 'GDPR' : 'Contract'}
</Pill>
)}
{c.user_type && (
{c.user_type && c.type !== 'invoice' && (
<Pill className="bg-emerald-50 text-emerald-800 border-emerald-200">
{c.user_type === 'personal' ? 'Personal' : c.user_type === 'company' ? 'Company' : 'Both'}
</Pill>

View File

@ -327,30 +327,7 @@ export default function SummaryPage() {
>
Fill fields with logged in data
</button>
<div className="mb-4 grid gap-3 sm:grid-cols-2">
<button
type="button"
onClick={() => setIsForSelf(true)}
className={`rounded-md border px-3 py-2 text-sm font-medium transition ${
isForSelf
? 'border-[#1C2B4A] bg-[#1C2B4A]/5 text-[#1C2B4A]'
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
For me
</button>
<button
type="button"
onClick={() => setIsForSelf(false)}
className={`rounded-md border px-3 py-2 text-sm font-medium transition ${
!isForSelf
? 'border-[#1C2B4A] bg-[#1C2B4A]/5 text-[#1C2B4A]'
: 'border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
For someone else
</button>
</div>
{/* "For someone else" is disabled for now — only self-subscriptions */}
<div className="grid gap-4 sm:grid-cols-2">
{/* inputs translated */}
<div>

View File

@ -80,6 +80,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
: 'relative'
const [hasReferralPerm, setHasReferralPerm] = useState(false)
const [hasSubscribePerm, setHasSubscribePerm] = useState(false)
const [canSeeDashboard, setCanSeeDashboard] = useState(false)
const headerElRef = useRef<HTMLElement | null>(null)
@ -169,14 +170,20 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
}
if (!user) {
console.log(' Header: no user, clearing permission flag')
if (!cancelled) setHasReferralPerm(false)
if (!cancelled) {
setHasReferralPerm(false)
setHasSubscribePerm(false)
}
return
}
const uid = (user as any)?.id ?? (user as any)?._id ?? (user as any)?.userId
if (!uid) {
console.warn('⚠️ Header: user id missing, cannot fetch permissions', user)
if (!cancelled) setHasReferralPerm(false)
if (!cancelled) {
setHasReferralPerm(false)
setHasSubscribePerm(false)
}
return
}
@ -216,20 +223,31 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
body
let can = false
let canSub = false
if (Array.isArray(permsSrc)) {
// Could be array of strings or objects
can =
permsSrc.includes?.('can_create_referrals') ||
permsSrc.some?.((p: any) => p?.name === 'can_create_referrals' || p?.key === 'can_create_referrals')
canSub =
permsSrc.includes?.('can_subscribe') ||
permsSrc.some?.((p: any) => p?.name === 'can_subscribe' || p?.key === 'can_subscribe')
} else if (permsSrc && typeof permsSrc === 'object') {
can = !!permsSrc.can_create_referrals
canSub = !!permsSrc.can_subscribe
}
console.log('✅ Header: can_create_referrals =', can)
if (!cancelled) setHasReferralPerm(!!can)
console.log('✅ Header: can_create_referrals =', can, 'can_subscribe =', canSub)
if (!cancelled) {
setHasReferralPerm(!!can)
setHasSubscribePerm(!!canSub)
}
} catch (e) {
console.error('❌ Header: fetch permissions error:', e)
if (!cancelled) setHasReferralPerm(false)
if (!cancelled) {
setHasReferralPerm(false)
setHasSubscribePerm(false)
}
}
}
@ -487,7 +505,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
</button>
)}
{DISPLAY_ABONEMENTS && (
{DISPLAY_ABONEMENTS && hasSubscribePerm && (
<button
onClick={() => router.push('/coffee-abonnements')}
className="text-sm/6 font-semibold text-gray-900 dark:text-white hover:text-indigo-600 dark:hover:text-indigo-400 transition-colors"
@ -719,7 +737,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
Personal Matrix
</button>
)}
{DISPLAY_ABONEMENTS && (
{DISPLAY_ABONEMENTS && hasSubscribePerm && (
<button
onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
className="block rounded-lg px-3 py-2 text-base/7 font-semibold text-gray-900 dark:text-white hover:bg-white/5 w-full text-left"