feat: update contract and template management to streamline type handling and permissions
This commit is contained in:
parent
48c63a896f
commit
f94e4669f8
@ -18,7 +18,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
|||||||
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
const [statusMsg, setStatusMsg] = useState<string | null>(null);
|
||||||
|
|
||||||
const [lang, setLang] = useState<'en' | 'de'>('en');
|
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 [contractType, setContractType] = useState<'contract' | 'gdpr'>('contract');
|
||||||
const [userType, setUserType] = useState<'personal' | 'company' | 'both'>('personal');
|
const [userType, setUserType] = useState<'personal' | 'company' | 'both'>('personal');
|
||||||
const [description, setDescription] = useState<string>('');
|
const [description, setDescription] = useState<string>('');
|
||||||
@ -54,7 +54,7 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
|||||||
setHtmlCode(tpl.html || '');
|
setHtmlCode(tpl.html || '');
|
||||||
setDescription(((tpl as any)?.description as string) || ''); // FIX: DocumentTemplate may not declare `description`
|
setDescription(((tpl as any)?.description as string) || ''); // FIX: DocumentTemplate may not declare `description`
|
||||||
setLang((tpl.lang as any) || 'en');
|
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');
|
setContractType(((tpl.contract_type as any) || 'contract') as 'contract' | 'gdpr');
|
||||||
setUserType(((tpl.user_type as any) || 'both') as 'personal' | 'company' | 'both');
|
setUserType(((tpl.user_type as any) || 'both') as 'personal' | 'company' | 'both');
|
||||||
setEditingMeta({
|
setEditingMeta({
|
||||||
@ -214,8 +214,12 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
|||||||
};
|
};
|
||||||
|
|
||||||
const save = async (publish: boolean) => {
|
const save = async (publish: boolean) => {
|
||||||
if (publish && type === 'contract') {
|
if (publish) {
|
||||||
const kind = contractType === 'gdpr' ? 'GDPR' : 'Contract';
|
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.`)
|
setPublishConfirmMessage(`This will deactivate other active ${kind} templates that apply to the same user type and language.`)
|
||||||
setPublishConfirmOpen(true)
|
setPublishConfirmOpen(true)
|
||||||
return
|
return
|
||||||
@ -283,12 +287,15 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
|||||||
<div className="flex flex-col sm:flex-row gap-4">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<select
|
<select
|
||||||
value={type}
|
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
|
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"
|
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="contract">Contract</option>
|
||||||
<option value="bill">Bill</option>
|
|
||||||
<option value="invoice">Invoice</option>
|
<option value="invoice">Invoice</option>
|
||||||
<option value="other">Other</option>
|
<option value="other">Other</option>
|
||||||
</select>
|
</select>
|
||||||
@ -303,16 +310,18 @@ export default function ContractEditor({ onSaved, editingTemplateId, onCancelEdi
|
|||||||
<option value="gdpr">GDPR</option>
|
<option value="gdpr">GDPR</option>
|
||||||
</select>
|
</select>
|
||||||
)}
|
)}
|
||||||
<select
|
{type !== 'invoice' && (
|
||||||
value={userType}
|
<select
|
||||||
onChange={(e) => setUserType(e.target.value as 'personal' | 'company' | 'both')}
|
value={userType}
|
||||||
required
|
onChange={(e) => setUserType(e.target.value as 'personal' | 'company' | 'both')}
|
||||||
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"
|
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="personal">Personal</option>
|
||||||
<option value="both">Both</option>
|
<option value="company">Company</option>
|
||||||
</select>
|
<option value="both">Both</option>
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
<select
|
<select
|
||||||
value={lang}
|
value={lang}
|
||||||
onChange={(e) => setLang(e.target.value as 'en' | 'de')}
|
onChange={(e) => setLang(e.target.value as 'en' | 'de')}
|
||||||
|
|||||||
@ -98,8 +98,12 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
|||||||
const target = current === 'published' ? 'inactive' : 'active';
|
const target = current === 'published' ? 'inactive' : 'active';
|
||||||
if (target === 'active') {
|
if (target === 'active') {
|
||||||
const tpl = items.find((i) => i.id === id);
|
const tpl = items.find((i) => i.id === id);
|
||||||
if (tpl?.type === 'contract') {
|
if (tpl) {
|
||||||
const kind = tpl.contract_type === 'gdpr' ? 'GDPR' : 'Contract';
|
const kind = tpl.type === 'contract'
|
||||||
|
? (tpl.contract_type === 'gdpr' ? 'GDPR' : 'Contract')
|
||||||
|
: tpl.type === 'invoice'
|
||||||
|
? 'Invoice'
|
||||||
|
: 'Other';
|
||||||
setPendingToggle({
|
setPendingToggle({
|
||||||
id,
|
id,
|
||||||
target,
|
target,
|
||||||
@ -137,7 +141,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<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">
|
<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 "Both". Provide templates for each language (en/de). If no active invoice template matches, backend falls back to a text-only invoice.
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<input
|
<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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{filtered.map((c) => (
|
{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 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} />
|
<StatusBadge status={c.status} />
|
||||||
{c.type && (
|
{c.type && (
|
||||||
<Pill className="bg-slate-50 text-slate-800 border-slate-200">
|
<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>
|
</Pill>
|
||||||
)}
|
)}
|
||||||
{c.type === 'contract' && (
|
{c.type === 'contract' && (
|
||||||
@ -171,7 +175,7 @@ export default function ContractTemplateList({ refreshKey = 0, onEdit }: Props)
|
|||||||
{c.contract_type === 'gdpr' ? 'GDPR' : 'Contract'}
|
{c.contract_type === 'gdpr' ? 'GDPR' : 'Contract'}
|
||||||
</Pill>
|
</Pill>
|
||||||
)}
|
)}
|
||||||
{c.user_type && (
|
{c.user_type && c.type !== 'invoice' && (
|
||||||
<Pill className="bg-emerald-50 text-emerald-800 border-emerald-200">
|
<Pill className="bg-emerald-50 text-emerald-800 border-emerald-200">
|
||||||
{c.user_type === 'personal' ? 'Personal' : c.user_type === 'company' ? 'Company' : 'Both'}
|
{c.user_type === 'personal' ? 'Personal' : c.user_type === 'company' ? 'Company' : 'Both'}
|
||||||
</Pill>
|
</Pill>
|
||||||
|
|||||||
@ -327,30 +327,7 @@ export default function SummaryPage() {
|
|||||||
>
|
>
|
||||||
Fill fields with logged in data
|
Fill fields with logged in data
|
||||||
</button>
|
</button>
|
||||||
<div className="mb-4 grid gap-3 sm:grid-cols-2">
|
{/* "For someone else" is disabled for now — only self-subscriptions */}
|
||||||
<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>
|
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
{/* inputs translated */}
|
{/* inputs translated */}
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -80,6 +80,7 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
: 'relative'
|
: 'relative'
|
||||||
|
|
||||||
const [hasReferralPerm, setHasReferralPerm] = useState(false)
|
const [hasReferralPerm, setHasReferralPerm] = useState(false)
|
||||||
|
const [hasSubscribePerm, setHasSubscribePerm] = useState(false)
|
||||||
const [canSeeDashboard, setCanSeeDashboard] = useState(false)
|
const [canSeeDashboard, setCanSeeDashboard] = useState(false)
|
||||||
const headerElRef = useRef<HTMLElement | null>(null)
|
const headerElRef = useRef<HTMLElement | null>(null)
|
||||||
|
|
||||||
@ -169,14 +170,20 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
console.log('ℹ️ Header: no user, clearing permission flag')
|
console.log('ℹ️ Header: no user, clearing permission flag')
|
||||||
if (!cancelled) setHasReferralPerm(false)
|
if (!cancelled) {
|
||||||
|
setHasReferralPerm(false)
|
||||||
|
setHasSubscribePerm(false)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const uid = (user as any)?.id ?? (user as any)?._id ?? (user as any)?.userId
|
const uid = (user as any)?.id ?? (user as any)?._id ?? (user as any)?.userId
|
||||||
if (!uid) {
|
if (!uid) {
|
||||||
console.warn('⚠️ Header: user id missing, cannot fetch permissions', user)
|
console.warn('⚠️ Header: user id missing, cannot fetch permissions', user)
|
||||||
if (!cancelled) setHasReferralPerm(false)
|
if (!cancelled) {
|
||||||
|
setHasReferralPerm(false)
|
||||||
|
setHasSubscribePerm(false)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,20 +223,31 @@ export default function Header({ setGlobalLoggingOut }: HeaderProps) {
|
|||||||
body
|
body
|
||||||
|
|
||||||
let can = false
|
let can = false
|
||||||
|
let canSub = false
|
||||||
if (Array.isArray(permsSrc)) {
|
if (Array.isArray(permsSrc)) {
|
||||||
// Could be array of strings or objects
|
// Could be array of strings or objects
|
||||||
can =
|
can =
|
||||||
permsSrc.includes?.('can_create_referrals') ||
|
permsSrc.includes?.('can_create_referrals') ||
|
||||||
permsSrc.some?.((p: any) => p?.name === 'can_create_referrals' || p?.key === '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') {
|
} else if (permsSrc && typeof permsSrc === 'object') {
|
||||||
can = !!permsSrc.can_create_referrals
|
can = !!permsSrc.can_create_referrals
|
||||||
|
canSub = !!permsSrc.can_subscribe
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('✅ Header: can_create_referrals =', can)
|
console.log('✅ Header: can_create_referrals =', can, 'can_subscribe =', canSub)
|
||||||
if (!cancelled) setHasReferralPerm(!!can)
|
if (!cancelled) {
|
||||||
|
setHasReferralPerm(!!can)
|
||||||
|
setHasSubscribePerm(!!canSub)
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ Header: fetch permissions error:', 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>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{DISPLAY_ABONEMENTS && (
|
{DISPLAY_ABONEMENTS && hasSubscribePerm && (
|
||||||
<button
|
<button
|
||||||
onClick={() => router.push('/coffee-abonnements')}
|
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"
|
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
|
Personal Matrix
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{DISPLAY_ABONEMENTS && (
|
{DISPLAY_ABONEMENTS && hasSubscribePerm && (
|
||||||
<button
|
<button
|
||||||
onClick={() => { router.push('/coffee-abonnements'); setMobileMenuOpen(false); }}
|
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"
|
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"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user