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 [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')}
|
||||
|
||||
@ -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 "Both". 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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user