beautify: admin redesign

This commit is contained in:
DeathKaioken 2025-11-17 23:02:41 +01:00
parent 2439928eff
commit 805ed1fdf2
11 changed files with 595 additions and 485 deletions

View File

@ -132,29 +132,29 @@ export default function ContractEditor({ onSaved }: Props) {
}; };
return ( return (
<div className="space-y-3"> <div className="space-y-6">
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-4">
<input <input
type="text" type="text"
placeholder="Template name" placeholder="Template name"
value={name} value={name}
onChange={(e) => setName(e.target.value)} onChange={(e) => setName(e.target.value)}
className="w-full sm:w-1/2 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50" className="w-full sm:w-1/2 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"
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
type="button" type="button"
onClick={() => setIsPreview((v) => !v)} onClick={() => setIsPreview((v) => !v)}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm" className="inline-flex items-center rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-900 px-4 py-2 text-sm font-medium shadow transition"
> >
{isPreview ? 'Switch to Code' : 'Preview HTML'} {isPreview ? 'Switch to Code' : 'Preview HTML'}
</button> </button>
{isPreview && ( {isPreview && (
<button <button
type="button" type="button"
onClick={() => { const w = iframeRef.current?.contentWindow; w?.focus(); w?.print(); }} onClick={printPreview}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm" className="inline-flex items-center rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-700 px-4 py-2 text-sm font-medium shadow transition"
> >
Print Print
</button> </button>
@ -163,18 +163,18 @@ export default function ContractEditor({ onSaved }: Props) {
</div> </div>
{/* New metadata inputs */} {/* New metadata inputs */}
<div className="flex flex-col sm:flex-row gap-3"> <div className="flex flex-col sm:flex-row gap-4">
<input <input
type="text" type="text"
placeholder="Type (e.g., contract, nda, invoice)" placeholder="Type (e.g., contract, nda, invoice)"
value={type} value={type}
onChange={(e) => setType(e.target.value)} onChange={(e) => setType(e.target.value)}
className="w-full sm:w-1/3 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50" 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"
/> />
<select <select
value={lang} value={lang}
onChange={(e) => setLang(e.target.value as 'en' | 'de')} onChange={(e) => setLang(e.target.value as 'en' | 'de')}
className="w-full sm:w-32 rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50" className="w-full sm:w-32 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="en">English (en)</option> <option value="en">English (en)</option>
<option value="de">Deutsch (de)</option> <option value="de">Deutsch (de)</option>
@ -184,7 +184,7 @@ export default function ContractEditor({ onSaved }: Props) {
placeholder="Description (optional)" placeholder="Description (optional)"
value={description} value={description}
onChange={(e) => setDescription(e.target.value)} onChange={(e) => setDescription(e.target.value)}
className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50" className="w-full 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"
/> />
</div> </div>
</div> </div>
@ -194,33 +194,33 @@ export default function ContractEditor({ onSaved }: Props) {
value={htmlCode} value={htmlCode}
onChange={(e) => setHtmlCode(e.target.value)} onChange={(e) => setHtmlCode(e.target.value)}
placeholder="Paste your full HTML (or snippet) here…" placeholder="Paste your full HTML (or snippet) here…"
className="min-h-[240px] w-full rounded-md border border-gray-300 bg-white px-3 py-3 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 font-mono" className="min-h-[320px] w-full rounded-lg border border-gray-300 bg-white px-4 py-3 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500/50 font-mono shadow"
/> />
)} )}
{isPreview && ( {isPreview && (
<div className="rounded-md border border-gray-300 bg-white"> <div className="rounded-lg border border-gray-300 bg-white shadow">
<iframe <iframe
ref={iframeRef} ref={iframeRef}
title="Contract Preview" title="Contract Preview"
className="w-full rounded-md" className="w-full rounded-lg"
style={{ height: 1200, background: 'transparent' }} style={{ height: 1200, background: 'transparent' }}
/> />
</div> </div>
)} )}
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<button <button
onClick={() => save(false)} onClick={() => save(false)}
disabled={saving} disabled={saving}
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm disabled:opacity-60" className="inline-flex items-center rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-900 px-4 py-2 text-sm font-medium shadow disabled:opacity-60 transition"
> >
Create (inactive) Create (inactive)
</button> </button>
<button <button
onClick={() => save(true)} onClick={() => save(true)}
disabled={saving} disabled={saving}
className="inline-flex items-center rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-3 py-2 text-sm disabled:opacity-60" className="inline-flex items-center rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium shadow disabled:opacity-60 transition"
> >
Create & Activate Create & Activate
</button> </button>

View File

@ -17,11 +17,11 @@ type ContractTemplate = {
function StatusBadge({ status }: { status: string }) { function StatusBadge({ status }: { status: string }) {
const map: Record<string, string> = { const map: Record<string, string> = {
draft: 'bg-gray-100 text-gray-800', draft: 'bg-gray-100 text-gray-800 border border-gray-300',
published: 'bg-green-100 text-green-800', published: 'bg-green-100 text-green-800 border border-green-300',
archived: 'bg-yellow-100 text-yellow-800', archived: 'bg-yellow-100 text-yellow-800 border border-yellow-300',
}; };
const cls = map[status] || 'bg-blue-100 text-blue-800'; const cls = map[status] || 'bg-blue-100 text-blue-800 border border-blue-300';
return <span className={`px-2 py-0.5 rounded text-xs font-semibold ${cls}`}>{status}</span>; return <span className={`px-2 py-0.5 rounded text-xs font-semibold ${cls}`}>{status}</span>;
} }
@ -97,47 +97,48 @@ export default function ContractTemplateList({ refreshKey = 0 }: Props) {
}; };
return ( return (
<div className="space-y-3"> <div className="space-y-4">
<div className="flex gap-2"> <div className="flex gap-2 items-center">
<input <input
placeholder="Search…" placeholder="Search templates…"
value={q} value={q}
onChange={(e) => setQ(e.target.value)} onChange={(e) => setQ(e.target.value)}
className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 outline-none focus:ring-2 focus:ring-indigo-500/50" className="w-full 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"
/> />
<button <button
onClick={load} onClick={load}
disabled={loading} disabled={loading}
className="rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm disabled:opacity-60" className="rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-700 px-4 py-2 text-sm font-medium shadow disabled:opacity-60"
> >
{loading ? 'Loading…' : 'Refresh'} {loading ? 'Loading…' : 'Refresh'}
</button> </button>
</div> </div>
<ul className="divide-y divide-gray-200"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filtered.map((c) => ( {filtered.map((c) => (
<li key={c.id} className="py-3 flex items-start justify-between gap-3"> <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="min-w-0"> <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <p className="font-semibold text-lg text-gray-900 truncate">{c.name}</p>
<p className="font-medium text-sm text-gray-900 truncate">{c.name}</p> <StatusBadge status={c.status} />
<StatusBadge status={c.status} />
</div>
<p className="text-xs text-gray-600">Version {c.version}{c.updatedAt ? ` • Updated ${new Date(c.updatedAt).toLocaleString()}` : ''}</p>
</div> </div>
<div className="flex flex-wrap gap-2"> <p className="text-xs text-gray-500">Version {c.version}{c.updatedAt ? ` • Updated ${new Date(c.updatedAt).toLocaleString()}` : ''}</p>
<button onClick={() => onPreview(c.id)} className="px-2 py-1 text-xs rounded bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200">Preview</button> <div className="flex flex-wrap gap-2 mt-2">
<button onClick={() => onGenPdf(c.id)} className="px-2 py-1 text-xs rounded bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200">PDF</button> <button onClick={() => onPreview(c.id)} className="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200 transition">Preview</button>
<button onClick={() => onDownloadPdf(c.id)} className="px-2 py-1 text-xs rounded bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200">Download</button> <button onClick={() => onGenPdf(c.id)} className="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200 transition">PDF</button>
<button onClick={() => onToggleState(c.id, c.status)} className="px-2 py-1 text-xs rounded bg-indigo-600 hover:bg-indigo-500 text-white"> <button onClick={() => onDownloadPdf(c.id)} className="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200 transition">Download</button>
<button onClick={() => onToggleState(c.id, c.status)} className={`px-3 py-1 text-xs rounded-lg font-semibold transition
${c.status === 'published'
? 'bg-red-100 hover:bg-red-200 text-red-700 border border-red-200'
: 'bg-indigo-600 hover:bg-indigo-500 text-white border border-indigo-600'}`}>
{c.status === 'published' ? 'Deactivate' : 'Activate'} {c.status === 'published' ? 'Deactivate' : 'Activate'}
</button> </button>
</div> </div>
</li> </div>
))} ))}
{!filtered.length && ( {!filtered.length && (
<li className="py-6 text-sm text-gray-600">No contracts found.</li> <div className="col-span-full py-8 text-center text-sm text-gray-500">No contracts found.</div>
)} )}
</ul> </div>
</div> </div>
); );
} }

View File

@ -212,48 +212,48 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
}; };
return ( return (
<div className="space-y-4"> <div className="space-y-6">
{/* Header with Add New Stamp modal trigger */} {/* Header with Add New Stamp modal trigger */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<p className="text-sm text-gray-700">Manage your company stamps. One active at a time.</p> <p className="text-sm text-gray-700">Manage your company stamps. One active at a time.</p>
<button <button
type="button" type="button"
onClick={openModal} onClick={openModal}
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-3 py-2 text-sm shadow-sm" className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-semibold shadow transition"
> >
<svg width="16" height="16" fill="currentColor" className="opacity-90"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg> <svg width="18" height="18" fill="currentColor" className="opacity-90"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg>
Add New Stamp Add New Stamp
</button> </button>
</div> </div>
{/* Emphasized Active stamp */} {/* Emphasized Active stamp */}
{activeStamp && ( {activeStamp && (
<div className="relative rounded-2xl p-[1px] bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500"> <div className="relative rounded-2xl p-[2px] bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 shadow-lg">
<div className="rounded-2xl bg-white p-4 sm:p-5 flex items-center gap-4 sm:gap-6"> <div className="rounded-2xl bg-white p-5 flex items-center gap-6">
<div className="relative"> <div className="relative">
{activeStamp.base64 ? ( {activeStamp.base64 ? (
<img <img
src={toImgSrc(activeStamp)} src={toImgSrc(activeStamp)}
alt="Active stamp" alt="Active stamp"
className="h-20 w-20 sm:h-24 sm:w-24 object-contain rounded-xl ring-1 ring-gray-200 shadow-md" className="h-24 w-24 object-contain rounded-xl ring-2 ring-indigo-200 shadow"
/> />
) : ( ) : (
<div className="h-20 w-20 sm:h-24 sm:w-24 flex items-center justify-center rounded-xl ring-1 ring-gray-200 bg-gray-50 text-[10px] text-gray-500"> <div className="h-24 w-24 flex items-center justify-center rounded-xl ring-2 ring-gray-200 bg-gray-50 text-xs text-gray-500">
no image no image
</div> </div>
)} )}
<span className="absolute -top-2 -right-2 rounded-full bg-green-600 text-white text-[10px] px-2 py-0.5 shadow"> <span className="absolute -top-2 -right-2 rounded-full bg-green-600 text-white text-xs px-3 py-1 shadow font-bold">
Active Active
</span> </span>
</div> </div>
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm font-semibold text-gray-900 truncate">{activeStamp.label || activeStamp.id}</p> <p className="text-base font-semibold text-gray-900 truncate">{activeStamp.label || activeStamp.id}</p>
<p className="text-xs text-gray-600">This stamp is auto-applied to documents where applicable.</p> <p className="text-xs text-gray-500">Auto-applied to documents where applicable.</p>
</div> </div>
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-2">
<button <button
onClick={() => onDeactivate(activeStamp.id)} onClick={() => onDeactivate(activeStamp.id)}
className="text-xs px-3 py-2 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200 shadow-sm" className="text-xs px-4 py-2 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200 shadow transition"
> >
Deactivate Deactivate
</button> </button>
@ -266,33 +266,33 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{!!stamps.length && ( {!!stamps.length && (
<div className="mt-2"> <div className="mt-2">
<p className="text-sm font-medium text-gray-900 mb-2">Your Company Stamps</p> <p className="text-sm font-medium text-gray-900 mb-2">Your Company Stamps</p>
<ul className="space-y-2"> <ul className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{stamps.map((s) => { {stamps.map((s) => {
const src = toImgSrc(s); const src = toImgSrc(s);
const activeCls = s.active const activeCls = s.active
? 'border-green-300 bg-green-50' ? 'border-green-300 bg-green-50 shadow'
: 'border-gray-200 hover:border-gray-300 transition-colors'; : 'border-gray-200 hover:border-indigo-300 transition-colors';
return ( return (
<li <li
key={s.id} key={s.id}
className={`flex items-center justify-between gap-3 p-3 border rounded-xl ${activeCls}`} className={`flex items-center justify-between gap-3 p-4 border rounded-xl ${activeCls}`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
{s.base64 ? ( {s.base64 ? (
<img <img
src={src} src={src}
alt="Stamp" alt="Stamp"
className="h-12 w-12 object-contain rounded-lg ring-1 ring-gray-200 bg-white" className="h-14 w-14 object-contain rounded-lg ring-1 ring-gray-200 bg-white"
/> />
) : ( ) : (
<div className="h-12 w-12 flex items-center justify-center rounded-lg ring-1 ring-gray-200 bg-white text-[10px] text-gray-500"> <div className="h-14 w-14 flex items-center justify-center rounded-lg ring-1 ring-gray-200 bg-white text-xs text-gray-500">
no image no image
</div> </div>
)} )}
<div className="flex flex-col"> <div className="flex flex-col">
<span className="text-sm text-gray-900">{s.label || s.id}</span> <span className="text-sm text-gray-900">{s.label || s.id}</span>
{s.active && ( {s.active && (
<span className="text-[10px] mt-1 px-2 py-0.5 rounded bg-green-100 text-green-800 w-fit"> <span className="text-xs mt-1 px-2 py-0.5 rounded bg-green-100 text-green-800 w-fit font-semibold">
Active Active
</span> </span>
)} )}
@ -302,21 +302,21 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{s.active ? ( {s.active ? (
<button <button
onClick={() => onDeactivate(s.id)} onClick={() => onDeactivate(s.id)}
className="text-xs px-3 py-1.5 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200" className="text-xs px-3 py-2 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200 transition"
> >
Deactivate Deactivate
</button> </button>
) : ( ) : (
<button <button
onClick={() => onActivate(s.id)} onClick={() => onActivate(s.id)}
className="text-xs px-3 py-1.5 rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200" className="text-xs px-3 py-2 rounded-lg bg-indigo-50 hover:bg-indigo-100 text-indigo-700 border border-indigo-200 transition"
> >
Activate Activate
</button> </button>
)} )}
<button <button
onClick={() => onDelete(s.id, s.active, s.label ?? undefined)} onClick={() => onDelete(s.id, s.active, s.label ?? undefined)}
className="text-xs px-3 py-1.5 rounded-lg bg-red-50 hover:bg-red-100 text-red-700 border border-red-200" className="text-xs px-3 py-2 rounded-lg bg-red-50 hover:bg-red-100 text-red-700 border border-red-200 transition"
> >
Delete Delete
</button> </button>
@ -333,14 +333,14 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40" onClick={closeModal} /> <div className="absolute inset-0 bg-black/40" onClick={closeModal} />
<div className="absolute inset-0 flex items-center justify-center p-4"> <div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-lg rounded-2xl bg-white shadow-xl ring-1 ring-black/5"> <div className="w-full max-w-lg rounded-2xl bg-white shadow-2xl ring-1 ring-black/5">
<div className="p-5 border-b border-gray-100"> <div className="p-6 border-b border-gray-100">
<h3 className="text-base font-semibold text-gray-900">Add New Stamp</h3> <h3 className="text-lg font-bold text-indigo-700">Add New Stamp</h3>
<p className="mt-1 text-xs text-gray-600"> <p className="mt-1 text-xs text-gray-500">
Accepted types: PNG, JPEG, WebP, SVG. Max size: 5MB. Accepted types: PNG, JPEG, WebP, SVG. Max size: 5MB.
</p> </p>
</div> </div>
<div className="p-5 space-y-4"> <div className="p-6 space-y-4">
<div> <div>
<label className="block text-xs font-medium text-gray-700 mb-1">Label</label> <label className="block text-xs font-medium text-gray-700 mb-1">Label</label>
<input <input
@ -355,7 +355,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
<div <div
onDragOver={(e) => e.preventDefault()} onDragOver={(e) => e.preventDefault()}
onDrop={onDrop} onDrop={onDrop}
className="rounded-xl border-2 border-dashed border-gray-300 hover:border-indigo-400 transition-colors p-4 bg-gray-50" className="rounded-xl border-2 border-dashed border-indigo-300 hover:border-indigo-400 transition-colors p-4 bg-indigo-50"
> >
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{modalPreviewUrl ? ( {modalPreviewUrl ? (
@ -365,13 +365,13 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
className="h-20 w-20 object-contain rounded-lg ring-1 ring-gray-200 bg-white" className="h-20 w-20 object-contain rounded-lg ring-1 ring-gray-200 bg-white"
/> />
) : ( ) : (
<div className="h-20 w-20 flex items-center justify-center rounded-lg ring-1 ring-gray-200 bg-white text-[10px] text-gray-500"> <div className="h-20 w-20 flex items-center justify-center rounded-lg ring-1 ring-gray-200 bg-white text-xs text-gray-500">
No image No image
</div> </div>
)} )}
<div className="min-w-0"> <div className="min-w-0">
<p className="text-sm text-gray-900">Drag and drop your stamp here</p> <p className="text-sm text-gray-900">Drag and drop your stamp here</p>
<p className="text-xs text-gray-600">or click to browse</p> <p className="text-xs text-gray-500">or click to browse</p>
<div className="mt-2"> <div className="mt-2">
<label className="inline-block"> <label className="inline-block">
<input <input
@ -380,7 +380,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
onChange={onBrowse} onChange={onBrowse}
className="hidden" className="hidden"
/> />
<span className="cursor-pointer text-xs px-3 py-1.5 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200 inline-flex items-center gap-1"> <span className="cursor-pointer text-xs px-3 py-2 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200 inline-flex items-center gap-1">
<svg width="14" height="14" fill="currentColor"><path d="M12 10v2H2v-2H0v4h14v-4h-2zM7 0l4 4H9v5H5V4H3l4-4z"/></svg> <svg width="14" height="14" fill="currentColor"><path d="M12 10v2H2v-2H0v4h14v-4h-2zM7 0l4 4H9v5H5V4H3l4-4z"/></svg>
Choose file Choose file
</span> </span>
@ -393,11 +393,11 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
{modalError && <p className="text-xs text-red-600">{modalError}</p>} {modalError && <p className="text-xs text-red-600">{modalError}</p>}
</div> </div>
<div className="px-5 py-4 border-t border-gray-100 flex items-center justify-end gap-2"> <div className="px-6 py-4 border-t border-gray-100 flex items-center justify-end gap-2">
<button <button
type="button" type="button"
onClick={closeModal} onClick={closeModal}
className="text-sm px-3 py-2 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200" className="text-sm px-4 py-2 rounded-lg bg-white hover:bg-gray-50 text-gray-800 border border-gray-200 transition"
> >
Cancel Cancel
</button> </button>
@ -405,7 +405,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
type="button" type="button"
onClick={confirmUpload} onClick={confirmUpload}
disabled={modalUploading || !modalFile} disabled={modalUploading || !modalFile}
className="text-sm px-3 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white disabled:opacity-60" className="text-sm px-4 py-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white disabled:opacity-60 transition"
> >
{modalUploading ? 'Uploading…' : 'Upload'} {modalUploading ? 'Uploading…' : 'Upload'}
</button> </button>

View File

@ -8,15 +8,20 @@ import ContractTemplateList from './components/contractTemplateList';
import useAuthStore from '../../store/authStore'; import useAuthStore from '../../store/authStore';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
const NAV = [
{ key: 'stamp', label: 'Company Stamp', icon: <svg className="w-5 h-5" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg> },
{ key: 'templates', label: 'Templates', icon: <svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M4 6h16M4 12h16M4 18h16"/></svg> },
{ key: 'editor', label: 'Create Template', icon: <svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4 12.5-12.5z"/></svg> },
];
export default function ContractManagementPage() { export default function ContractManagementPage() {
const [refreshKey, setRefreshKey] = useState(0); const [refreshKey, setRefreshKey] = useState(0);
const user = useAuthStore((s) => s.user); const user = useAuthStore((s) => s.user);
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const router = useRouter(); const router = useRouter();
const [section, setSection] = useState('templates');
useEffect(() => { useEffect(() => { setMounted(true); }, []);
setMounted(true);
}, []);
// Only allow admin // Only allow admin
const isAdmin = const isAdmin =
@ -30,7 +35,7 @@ export default function ContractManagementPage() {
useEffect(() => { useEffect(() => {
if (mounted && !isAdmin) { if (mounted && !isAdmin) {
router.replace('/'); // or show a 403 page router.replace('/');
} }
}, [mounted, isAdmin, router]); }, [mounted, isAdmin, router]);
@ -41,33 +46,63 @@ export default function ContractManagementPage() {
return ( return (
<PageLayout> <PageLayout>
{/* Force white background for this page */} <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<div className="bg-white min-h-screen"> <div className="flex flex-col md:flex-row max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8 gap-8">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8"> {/* Sidebar Navigation */}
<div className="mb-8"> <nav className="md:w-56 w-full flex md:flex-col flex-row gap-2 md:gap-4">
<h1 className="text-2xl font-bold text-gray-900">Contract Management</h1> {NAV.map((item) => (
<p className="mt-1 text-sm text-gray-600"> <button
Create contract templates in HTML, upload the company stamp, and manage existing contracts. key={item.key}
</p> onClick={() => setSection(item.key)}
</div> className={`flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition
${section === item.key
? 'bg-blue-900 text-blue-50 shadow'
: 'bg-white text-blue-900 hover:bg-blue-50 hover:text-blue-900 border border-blue-200'}`}
>
{item.icon}
<span>{item.label}</span>
</button>
))}
</nav>
{/* Reordered vertical layout */} {/* Main Content */}
<div className="space-y-6"> <main className="flex-1 space-y-8">
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm"> <header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-4">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Company Stamp</h2> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Contract Management</h1>
<ContractUploadCompanyStamp onUploaded={bumpRefresh} /> <p className="text-lg text-blue-700">
</div> Manage contract templates, company stamp, and create new templates.
</p>
</header>
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm"> {/* Section Panels */}
<h2 className="text-lg font-semibold text-gray-900 mb-4">Contracts</h2> {section === 'stamp' && (
<ContractTemplateList refreshKey={refreshKey} /> <section className="rounded-2xl bg-white shadow-lg border border-gray-100 p-6">
</div> <h2 className="text-xl font-semibold text-blue-900 mb-4 flex items-center gap-2">
<svg className="w-6 h-6" fill="none" stroke="currentColor"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm"> Company Stamp
<h2 className="text-lg font-semibold text-gray-900 mb-4">Create Template</h2> </h2>
<ContractEditor onSaved={bumpRefresh} /> <ContractUploadCompanyStamp onUploaded={bumpRefresh} />
</div> </section>
</div> )}
{section === 'templates' && (
<section className="rounded-2xl bg-white shadow-lg border border-gray-100 p-6">
<h2 className="text-xl font-semibold text-blue-900 mb-4 flex items-center gap-2">
<svg className="w-6 h-6" fill="none" stroke="currentColor"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
Templates
</h2>
<ContractTemplateList refreshKey={refreshKey} />
</section>
)}
{section === 'editor' && (
<section className="rounded-2xl bg-white shadow-lg border border-gray-100 p-6">
<h2 className="text-xl font-semibold text-blue-900 mb-4 flex items-center gap-2">
<svg className="w-6 h-6" fill="none" stroke="currentColor"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
Create Template
</h2>
<ContractEditor onSaved={bumpRefresh} />
</section>
)}
</main>
</div> </div>
</div> </div>
</PageLayout> </PageLayout>

View File

@ -266,35 +266,34 @@ export default function MatrixDetailPage() {
{refreshing && ( {refreshing && (
<div className="fixed inset-0 z-[9999] flex items-center justify-center bg-white/50 backdrop-blur-sm transition-opacity"> <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-white/50 backdrop-blur-sm transition-opacity">
<div className="flex items-center gap-3 rounded-lg bg-white shadow-md border border-gray-200 px-4 py-3"> <div className="flex items-center gap-3 rounded-lg bg-white shadow-md border border-gray-200 px-4 py-3">
<span className="h-5 w-5 rounded-full border-2 border-indigo-600 border-b-transparent animate-spin" /> <span className="h-5 w-5 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
<span className="text-sm text-gray-700">Refreshing</span> <span className="text-sm text-gray-700">Refreshing</span>
</div> </div>
</div> </div>
)} )}
{/* Centered page container to avoid full-width stretch */} <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen w-full">
<div className="min-h-screen w-full bg-white"> <div className="mx-auto max-w-6xl px-2 sm:px-6 py-8">
<div className="mx-auto max-w-6xl px-4 sm:px-6 py-6"> {/* Header card */}
{/* Modern header card with action */} <header className="mb-8 rounded-2xl border border-gray-100 bg-white shadow-lg px-8 py-8 flex flex-col gap-4">
<div className="mb-6 rounded-xl border border-gray-200 bg-white shadow-sm overflow-hidden"> <div className="flex items-center justify-between">
<div className="px-5 py-4 flex items-start justify-between gap-4"> <div className="space-y-2">
<div className="space-y-1">
<button <button
onClick={() => router.push('/admin/matrix-management')} onClick={() => router.push('/admin/matrix-management')}
className="inline-flex items-center gap-2 text-sm text-gray-600 hover:text-gray-800" className="inline-flex items-center gap-2 text-sm text-blue-900 hover:text-blue-700"
> >
<ArrowLeftIcon className="h-4 w-4" /> <ArrowLeftIcon className="h-4 w-4" />
Back to matrices Back to matrices
</button> </button>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">{matrixName}</h1> <h1 className="text-3xl font-extrabold text-blue-900">{matrixName}</h1>
<p className="text-sm text-gray-600"> <p className="text-base text-blue-700">
Top node: <span className="font-medium text-gray-900">{topNodeEmail}</span> Top node: <span className="font-semibold text-blue-900">{topNodeEmail}</span>
</p> </p>
<div className="mt-2 flex flex-wrap items-center gap-2"> <div className="mt-2 flex flex-wrap items-center gap-2">
<span className="inline-flex items-center rounded-full bg-gray-100 border border-gray-200 px-2.5 py-1 text-[11px] text-gray-800"> <span className="inline-flex items-center rounded-full bg-gray-100 border border-gray-200 px-3 py-1 text-xs text-gray-800">
Children/node: 5 Children/node: 5
</span> </span>
<span className="inline-flex items-center rounded-full bg-indigo-50 border border-indigo-200 px-2.5 py-1 text-[11px] text-indigo-800"> <span className="inline-flex items-center rounded-full bg-blue-50 border border-blue-200 px-3 py-1 text-xs text-blue-900">
Max depth: {(!serverMaxDepth || serverMaxDepth <= 0) ? 'Unlimited' : serverMaxDepth} Max depth: {(!serverMaxDepth || serverMaxDepth <= 0) ? 'Unlimited' : serverMaxDepth}
</span> </span>
</div> </div>
@ -302,90 +301,89 @@ export default function MatrixDetailPage() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button <button
onClick={() => { setOpen(true) }} onClick={() => { setOpen(true) }}
className="inline-flex items-center gap-2 rounded-lg bg-blue-700 hover:bg-blue-600 text-white px-4 py-2 shadow-sm" className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
> >
<PlusIcon className="h-5 w-5" /> <PlusIcon className="h-5 w-5" />
Add users to matrix Add users to matrix
</button> </button>
</div> </div>
</div> </div>
</div> </header>
{/* banner for unlimited */} {/* Banner for unlimited */}
{isUnlimited && ( {isUnlimited && (
<div className="mb-4 rounded-md px-4 py-2 text-xs text-blue-900 bg-blue-50 border border-blue-200"> <div className="mb-4 rounded-md px-4 py-2 text-xs text-blue-900 bg-blue-50 border border-blue-200">
Large structure. Results are paginated by depth and count. Large structure. Results are paginated by depth and count.
</div> </div>
)} )}
{/* sticky depth controls */} {/* Sticky depth controls */}
<div className="sticky top-0 z-10 bg-white/85 backdrop-blur px-4 sm:px-6 py-3 border-b border-gray-100 flex items-center gap-3 rounded-md"> <div className="sticky top-0 z-10 bg-white/90 backdrop-blur px-6 py-4 border-b border-blue-100 flex flex-wrap items-center gap-4 rounded-xl mb-6 shadow">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="text-xs text-gray-600">From</label> <label className="text-xs text-gray-600">From</label>
<input type="number" min={0} value={depthA} onChange={e => setDepthA(Math.max(0, Number(e.target.value) || 0))} className="w-16 rounded border border-gray-300 px-2 py-1 text-xs" /> <input type="number" min={0} value={depthA} onChange={e => setDepthA(Math.max(0, Number(e.target.value) || 0))} className="w-16 rounded-lg border border-gray-300 px-2 py-1 text-xs" />
<label className="text-xs text-gray-600">to</label> <label className="text-xs text-gray-600">to</label>
<input type="number" min={depthA} value={depthB} onChange={e => setDepthB(Math.max(depthA, Number(e.target.value) || depthA))} className="w-16 rounded border border-gray-300 px-2 py-1 text-xs" /> <input type="number" min={depthA} value={depthB} onChange={e => setDepthB(Math.max(depthA, Number(e.target.value) || depthA))} className="w-16 rounded-lg border border-gray-300 px-2 py-1 text-xs" />
</div> </div>
<button <button
onClick={() => { setDepthA(Math.max(0, depthA - 5)); setDepthB(Math.max(5, depthB - 5)); }} onClick={() => { setDepthA(Math.max(0, depthA - 5)); setDepthB(Math.max(5, depthB - 5)); }}
className="inline-flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-300" className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-medium text-blue-900 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-300"
> >
Load previous 5 levels Load previous 5 levels
</button> </button>
<button <button
onClick={() => { setDepthA(depthA + 5); setDepthB(depthB + 5); }} onClick={() => { setDepthA(depthA + 5); setDepthB(depthB + 5); }}
className="inline-flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 px-2.5 py-1 text-xs font-medium text-blue-700 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-300" className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-50 px-3 py-1 text-xs font-medium text-blue-900 hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-300"
> >
Load next 5 levels Load next 5 levels
</button> </button>
{/* NEW: include root toggle */}
<label className="ml-3 inline-flex items-center gap-2 text-xs text-gray-700"> <label className="ml-3 inline-flex items-center gap-2 text-xs text-gray-700">
<input <input
type="checkbox" type="checkbox"
checked={includeRoot} checked={includeRoot}
onChange={e => setIncludeRoot(e.target.checked)} onChange={e => setIncludeRoot(e.target.checked)}
className="h-3 w-3 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" className="h-3 w-3 rounded border-gray-300 text-blue-900 focus:ring-blue-900"
/> />
Include root Include root
</label> </label>
<div className="ml-auto text-xs text-gray-600"> <div className="ml-auto text-xs text-gray-600">
Showing levels {depthA}{depthB} of {isUnlimited ? 'Unlimited' : serverMaxDepth} Showing levels {depthA}{depthB} of {isUnlimited ? 'Unlimited' : serverMaxDepth}
</div> </div>
<button onClick={exportCsv} className="ml-3 text-xs text-indigo-700 hover:text-indigo-900 underline">Export CSV (levels {depthA}{depthB})</button> <button onClick={exportCsv} className="ml-3 text-xs text-blue-900 hover:text-blue-700 underline">Export CSV (levels {depthA}{depthB})</button>
</div> </div>
{/* small stats */} {/* Small stats */}
<div className="mt-3 mb-4 grid grid-cols-1 sm:grid-cols-3 gap-3"> <div className="mb-8 grid grid-cols-1 sm:grid-cols-3 gap-6">
<div className="rounded-lg border border-gray-200 bg-white p-3"> <div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-[11px] text-gray-500">Users (current slice)</div> <div className="text-xs text-gray-500 mb-1">Users (current slice)</div>
<div className="text-base font-semibold text-gray-900">{usersInSlice.length}</div> <div className="text-xl font-semibold text-blue-900">{usersInSlice.length}</div>
</div> </div>
<div className="rounded-lg border border-gray-200 bg-white p-3"> <div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-[11px] text-gray-500">Total descendants</div> <div className="text-xs text-gray-500 mb-1">Total descendants</div>
<div className="text-base font-semibold text-gray-900">{isUnlimited ? 'All descendants so far' : totalDescendants}</div> <div className="text-xl font-semibold text-blue-900">{isUnlimited ? 'All descendants so far' : totalDescendants}</div>
</div> </div>
<div className="rounded-lg border border-gray-200 bg-white p-3"> <div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
<div className="text-[11px] text-gray-500">Active levels loaded</div> <div className="text-xs text-gray-500 mb-1">Active levels loaded</div>
<div className="text-base font-semibold text-gray-900">{depthA}{depthB}</div> <div className="text-xl font-semibold text-blue-900">{depthA}{depthB}</div>
</div> </div>
</div> </div>
{/* dynamic levels */} {/* Dynamic levels */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
{Array {Array
.from(byLevel.keys()) .from(byLevel.keys())
.filter(l => l >= depthA && l <= depthB) .filter(l => l >= depthA && l <= depthB)
.filter(l => includeRoot || l !== 0) // NEW: hide level 0 when unchecked .filter(l => includeRoot || l !== 0)
.sort((a,b)=>a-b) .sort((a,b)=>a-b)
.map(l => ( .map(l => (
<LevelSection key={l} level={l} /> <LevelSection key={l} level={l} />
))} ))}
</div> </div>
{/* jump to level */} {/* Jump to level */}
<div className="mt-4 mb-8"> <div className="mb-8">
<label className="text-xs text-gray-600 mr-2">Jump to level</label> <label className="text-xs text-gray-600 mr-2">Jump to level</label>
<select className="rounded border border-gray-300 px-2 py-1 text-xs" onChange={e => { <select className="rounded-lg border border-gray-300 px-3 py-1 text-xs" onChange={e => {
const lv = Number(e.target.value) || 0 const lv = Number(e.target.value) || 0
setDepthA(lv); setDepthB(Math.max(lv, lv + 5)) setDepthA(lv); setDepthB(Math.max(lv, lv + 5))
}}> }}>
@ -394,10 +392,10 @@ export default function MatrixDetailPage() {
</div> </div>
{/* Collapsible All users list by level */} {/* Collapsible All users list by level */}
<div className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden"> <div className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden mb-8">
<div className="px-5 py-4 border-b border-gray-100"> <div className="px-8 py-6 border-b border-gray-100">
<h2 className="text-base font-semibold text-gray-900">All users in this matrix</h2> <h2 className="text-xl font-semibold text-blue-900">All users in this matrix</h2>
<p className="text-xs text-gray-600">Grouped by levels (power of five structure).</p> <p className="text-xs text-blue-700">Grouped by levels (power of five structure).</p>
</div> </div>
<div className="divide-y divide-gray-100"> <div className="divide-y divide-gray-100">
{[0,1,2,3,4,5].map(lvl => { {[0,1,2,3,4,5].map(lvl => {
@ -414,15 +412,14 @@ export default function MatrixDetailPage() {
return globalMatch && levelMatch return globalMatch && levelMatch
}) })
return ( return (
<div key={lvl} className="px-5 py-4"> <div key={lvl} className="px-8 py-6">
<button <button
className="flex items-center justify-between w-full text-left" className="flex items-center justify-between w-full text-left"
onClick={() => { onClick={() => {
console.log('[MatrixDetailPage] toggle level', { level: lvl, to: !collapsedLevels[lvl] })
setCollapsedLevels(prev => ({ ...prev, [lvl]: !prev[lvl] })) setCollapsedLevels(prev => ({ ...prev, [lvl]: !prev[lvl] }))
}} }}
> >
<span className="mb-2 text-sm font-semibold text-gray-800 flex items-center gap-2"> <span className="mb-2 text-base font-semibold text-blue-900 flex items-center gap-2">
{lvl === 0 ? 'Level 0 (Top node)' : `Level ${lvl}`} {list.length} user(s) {lvl === 0 ? 'Level 0 (Top node)' : `Level ${lvl}`} {list.length} user(s)
</span> </span>
<span className="ml-2"> <span className="ml-2">
@ -438,34 +435,31 @@ export default function MatrixDetailPage() {
<> <>
<div className="flex justify-end mt-2 mb-3"> <div className="flex justify-end mt-2 mb-3">
<div className="relative w-64"> <div className="relative w-64">
<MagnifyingGlassIcon className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" /> <MagnifyingGlassIcon className="pointer-events-none absolute left-2 top-1/2 -translate-y-1/2 h-4 w-4 text-blue-300" />
<input <input
type="text" type="text"
value={levelSearch[lvl]} value={levelSearch[lvl]}
onChange={e => { onChange={e => setLevelSearch(prev => ({ ...prev, [lvl]: e.target.value }))}
console.log('[MatrixDetailPage] levelSearch', { level: lvl, q: e.target.value })
setLevelSearch(prev => ({ ...prev, [lvl]: e.target.value }))
}}
placeholder="Search in level..." placeholder="Search in level..."
className="pl-8 pr-2 py-1 rounded-md border border-gray-200 text-xs focus:ring-1 focus:ring-indigo-500 focus:border-transparent w-full" className="pl-8 pr-2 py-2 rounded-lg border border-gray-200 text-xs focus:ring-1 focus:ring-blue-900 focus:border-transparent w-full"
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredList.length === 0 && ( {filteredList.length === 0 && (
<div className="col-span-full text-xs text-gray-500 italic">No users found.</div> <div className="col-span-full text-xs text-gray-500 italic">No users found.</div>
)} )}
{filteredList.length > 0 && filteredList.map(u => ( {filteredList.length > 0 && filteredList.map(u => (
<div key={`${lvl}-${u.id}`} className="rounded-lg border border-gray-200 p-3 bg-gray-50"> <div key={`${lvl}-${u.id}`} className="rounded-lg border border-gray-100 p-4 bg-blue-50">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-sm font-medium text-gray-900">{u.name}</div> <div className="text-sm font-medium text-blue-900">{u.name}</div>
<span className={`text-[10px] font-semibold px-2 py-0.5 rounded-full ${ <span className={`text-[10px] font-semibold px-2 py-0.5 rounded-full ${
u.type === 'company' ? 'bg-indigo-100 text-indigo-800' : 'bg-blue-100 text-blue-800' u.type === 'company' ? 'bg-indigo-100 text-indigo-800' : 'bg-blue-100 text-blue-800'
}`}> }`}>
{u.type === 'company' ? 'Company' : 'Personal'} {u.type === 'company' ? 'Company' : 'Personal'}
</span> </span>
</div> </div>
<div className="mt-1 text-xs text-gray-600">{u.email}</div> <div className="mt-1 text-xs text-blue-700">{u.email}</div>
</div> </div>
))} ))}
</div> </div>

View File

@ -252,22 +252,24 @@ export default function MatrixManagementPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen w-full px-4 sm:px-6 py-8 bg-white"> <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen w-full">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
{/* Header + Create */} {/* Header + Create */}
<div className="flex items-center justify-between mb-6"> <header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<div> <div className="flex items-center justify-between">
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Matrix Management</h1> <div>
<p className="text-sm text-gray-600 mt-1">Manage matrices, see stats, and create new ones.</p> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Matrix Management</h1>
<p className="text-lg text-blue-700 mt-2">Manage matrices, see stats, and create new ones.</p>
</div>
<button
onClick={() => setCreateOpen(true)}
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
>
<PlusIcon className="h-5 w-5" />
Create Matrix
</button>
</div> </div>
<button </header>
onClick={() => setCreateOpen(true)}
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 shadow-sm"
>
<PlusIcon className="h-5 w-5" />
Create New Matrix
</button>
</div>
{/* Error banner for stats */} {/* Error banner for stats */}
{statsError && ( {statsError && (
@ -277,17 +279,17 @@ export default function MatrixManagementPage() {
)} )}
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 mb-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 mb-8">
<StatCard icon={CheckCircleIcon} label="Active Matrices" value={stats.active} color="bg-green-500" /> <StatCard icon={CheckCircleIcon} label="Active Matrices" value={stats.active} color="bg-green-500" />
<StatCard icon={ChartBarIcon} label="Total Matrices" value={stats.total} color="bg-indigo-500" /> <StatCard icon={ChartBarIcon} label="Total Matrices" value={stats.total} color="bg-blue-900" />
<StatCard icon={UsersIcon} label="Total Users Subscribed" value={stats.totalUsers} color="bg-amber-600" /> <StatCard icon={UsersIcon} label="Total Users Subscribed" value={stats.totalUsers} color="bg-amber-600" />
</div> </div>
{/* Filters */} {/* Filters */}
<div className="mb-6 flex items-center gap-3"> <div className="mb-6 flex flex-wrap items-center gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="text-sm text-gray-600">Policy</label> <label className="text-sm text-gray-600">Policy</label>
<select value={policyFilter} onChange={e => setPolicyFilter(e.target.value as any)} className="rounded border border-gray-300 px-2 py-1 text-sm"> <select value={policyFilter} onChange={e => setPolicyFilter(e.target.value as any)} className="rounded-lg border border-gray-300 px-3 py-2 text-sm bg-white shadow">
<option value="all">All</option> <option value="all">All</option>
<option value="unlimited">Unlimited</option> <option value="unlimited">Unlimited</option>
<option value="five">5-level</option> <option value="five">5-level</option>
@ -295,13 +297,13 @@ export default function MatrixManagementPage() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="text-sm text-gray-600">Sort by users</label> <label className="text-sm text-gray-600">Sort by users</label>
<select value={sortByUsers} onChange={e => setSortByUsers(e.target.value as any)} className="rounded border border-gray-300 px-2 py-1 text-sm"> <select value={sortByUsers} onChange={e => setSortByUsers(e.target.value as any)} className="rounded-lg border border-gray-300 px-3 py-2 text-sm bg-white shadow">
<option value="desc">Desc</option> <option value="desc">Desc</option>
<option value="asc">Asc</option> <option value="asc">Asc</option>
</select> </select>
</div> </div>
<div className="text-xs text-gray-500" title="Users count respects each matrixs max depth policy."> <div className="text-xs text-gray-500" title="Users count respects each matrixs max depth policy.">
Tooltip: Users count respects each matrixs max depth policy. Users count respects each matrixs max depth policy.
</div> </div>
</div> </div>
@ -313,12 +315,11 @@ export default function MatrixManagementPage() {
)} )}
{/* Matrix cards */} {/* Matrix cards */}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3"> <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-8">
{statsLoading ? ( {statsLoading ? (
// Simple skeleton
Array.from({ length: 3 }).map((_, i) => ( Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden"> <div key={i} className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden">
<div className="p-5 animate-pulse space-y-4"> <div className="p-6 animate-pulse space-y-4">
<div className="h-5 w-1/2 bg-gray-200 rounded" /> <div className="h-5 w-1/2 bg-gray-200 rounded" />
<div className="h-4 w-1/3 bg-gray-200 rounded" /> <div className="h-4 w-1/3 bg-gray-200 rounded" />
<div className="h-4 w-2/3 bg-gray-200 rounded" /> <div className="h-4 w-2/3 bg-gray-200 rounded" />
@ -330,14 +331,14 @@ export default function MatrixManagementPage() {
<div className="text-sm text-gray-600">No matrices found.</div> <div className="text-sm text-gray-600">No matrices found.</div>
) : ( ) : (
matricesView.map(m => ( matricesView.map(m => (
<article key={m.id} className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden"> <article key={m.id} className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden flex flex-col">
<div className="p-5"> <div className="p-6 flex-1 flex flex-col">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<h3 className="text-lg font-semibold text-gray-900">{m.name}</h3> <h3 className="text-xl font-semibold text-blue-900">{m.name}</h3>
<StatusBadge status={m.status} /> <StatusBadge status={m.status} />
</div> </div>
<div className="mt-2"> <div className="mt-2">
<span className="inline-flex items-center rounded-full bg-indigo-50 border border-indigo-200 px-2 py-0.5 text-[11px] text-indigo-800"> <span className="inline-flex items-center rounded-full bg-blue-50 border border-blue-200 px-2 py-0.5 text-xs text-blue-900">
Max depth: {(!m.policyMaxDepth || m.policyMaxDepth <= 0) ? 'Unlimited' : m.policyMaxDepth} Max depth: {(!m.policyMaxDepth || m.policyMaxDepth <= 0) ? 'Unlimited' : m.policyMaxDepth}
</span> </span>
</div> </div>
@ -361,18 +362,16 @@ export default function MatrixManagementPage() {
<div className="mt-5 flex items-center justify-between"> <div className="mt-5 flex items-center justify-between">
<button <button
onClick={() => toggleStatus(m.id)} onClick={() => toggleStatus(m.id)}
className={`rounded-md px-3 py-2 text-sm font-medium border ${ className={`rounded-lg px-4 py-2 text-sm font-medium border shadow transition
m.status === 'active' ${m.status === 'active'
? 'border-red-300 text-red-700 hover:bg-red-50' ? 'border-red-300 text-red-700 hover:bg-red-50'
: 'border-green-300 text-green-700 hover:bg-green-50' : 'border-green-300 text-green-700 hover:bg-green-50'}`}
}`}
> >
{m.status === 'active' ? 'Deactivate' : 'Activate'} {m.status === 'active' ? 'Deactivate' : 'Activate'}
</button> </button>
<button <button
className="text-sm font-medium text-indigo-600 hover:text-indigo-500" className="text-sm font-medium text-blue-900 hover:text-blue-700"
onClick={() => { onClick={() => {
// get default depth range from localStorage per matrix
const defA = Number(localStorage.getItem(`matrixDepthA:${m.id}`) ?? 0) const defA = Number(localStorage.getItem(`matrixDepthA:${m.id}`) ?? 0)
const defB = Number(localStorage.getItem(`matrixDepthB:${m.id}`) ?? 5) const defB = Number(localStorage.getItem(`matrixDepthB:${m.id}`) ?? 5)
const params = new URLSearchParams({ const params = new URLSearchParams({
@ -389,23 +388,22 @@ export default function MatrixManagementPage() {
View details View details
</button> </button>
</div> </div>
{/* Quick default AB editor */}
<div className="mt-3 flex items-center gap-2"> <div className="mt-3 flex items-center gap-2">
<span className="text-[11px] text-gray-500">Default depth slice:</span> <span className="text-xs text-gray-500">Default depth slice:</span>
<input <input
type="number" type="number"
min={0} min={0}
defaultValue={Number(localStorage.getItem(`matrixDepthA:${m.id}`) ?? 0)} defaultValue={Number(localStorage.getItem(`matrixDepthA:${m.id}`) ?? 0)}
onBlur={e => localStorage.setItem(`matrixDepthA:${m.id}`, String(Math.max(0, Number(e.target.value) || 0)))} onBlur={e => localStorage.setItem(`matrixDepthA:${m.id}`, String(Math.max(0, Number(e.target.value) || 0)))}
className="w-16 rounded border border-gray-300 px-2 py-1 text-[11px]" className="w-16 rounded-lg border border-gray-300 px-2 py-1 text-xs"
/> />
<span className="text-[11px] text-gray-500">to</span> <span className="text-xs text-gray-500">to</span>
<input <input
type="number" type="number"
min={0} min={0}
defaultValue={Number(localStorage.getItem(`matrixDepthB:${m.id}`) ?? 5)} defaultValue={Number(localStorage.getItem(`matrixDepthB:${m.id}`) ?? 5)}
onBlur={e => localStorage.setItem(`matrixDepthB:${m.id}`, String(Math.max(0, Number(e.target.value) || 0)))} onBlur={e => localStorage.setItem(`matrixDepthB:${m.id}`, String(Math.max(0, Number(e.target.value) || 0)))}
className="w-16 rounded border border-gray-300 px-2 py-1 text-[11px]" className="w-16 rounded-lg border border-gray-300 px-2 py-1 text-xs"
/> />
</div> </div>
</div> </div>
@ -420,9 +418,9 @@ export default function MatrixManagementPage() {
<div className="fixed inset-0 z-50"> <div className="fixed inset-0 z-50">
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => { setCreateOpen(false); resetForm() }} /> <div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={() => { setCreateOpen(false); resetForm() }} />
<div className="absolute inset-0 flex items-center justify-center p-4"> <div className="absolute inset-0 flex items-center justify-center p-4">
<div className="w-full max-w-md rounded-xl bg-white shadow-2xl ring-1 ring-black/10"> <div className="w-full max-w-md rounded-2xl bg-white shadow-2xl ring-1 ring-black/10">
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between"> <div className="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
<h4 className="text-base font-semibold text-gray-900">Create New Matrix</h4> <h4 className="text-lg font-semibold text-blue-900">Create Matrix</h4>
<button <button
onClick={() => { setCreateOpen(false); resetForm() }} onClick={() => { setCreateOpen(false); resetForm() }}
className="text-sm text-gray-500 hover:text-gray-700" className="text-sm text-gray-500 hover:text-gray-700"
@ -430,7 +428,7 @@ export default function MatrixManagementPage() {
Close Close
</button> </button>
</div> </div>
<form onSubmit={handleCreate} className="p-5 space-y-4"> <form onSubmit={handleCreate} className="p-6 space-y-5">
{/* Success banner */} {/* Success banner */}
{createSuccess && ( {createSuccess && (
<div className="rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700"> <div className="rounded-md border border-green-200 bg-green-50 px-3 py-2 text-sm text-green-700">
@ -451,7 +449,7 @@ export default function MatrixManagementPage() {
type="button" type="button"
onClick={confirmForce} onClick={confirmForce}
disabled={createLoading} disabled={createLoading}
className="rounded-md bg-amber-600 hover:bg-amber-500 text-white px-3 py-1.5 text-xs font-medium disabled:opacity-50" className="rounded-lg bg-amber-600 hover:bg-amber-500 text-white px-4 py-2 text-xs font-medium disabled:opacity-50"
> >
Replace (force) Replace (force)
</button> </button>
@ -459,7 +457,7 @@ export default function MatrixManagementPage() {
type="button" type="button"
onClick={() => setForcePrompt(null)} onClick={() => setForcePrompt(null)}
disabled={createLoading} disabled={createLoading}
className="rounded-md border border-amber-300 px-3 py-1.5 text-xs font-medium text-amber-800 hover:bg-amber-100 disabled:opacity-50" className="rounded-lg border border-amber-300 px-4 py-2 text-xs font-medium text-amber-800 hover:bg-amber-100 disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
@ -469,24 +467,24 @@ export default function MatrixManagementPage() {
{/* Form fields */} {/* Form fields */}
<div> <div>
<label className="block text-sm font-medium text-gray-800 mb-1">Matrix Name</label> <label className="block text-sm font-medium text-blue-900 mb-1">Matrix Name</label>
<input <input
type="text" type="text"
value={createName} value={createName}
onChange={e => setCreateName(e.target.value)} onChange={e => setCreateName(e.target.value)}
disabled={createLoading} disabled={createLoading}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:bg-gray-100" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent disabled:bg-gray-100"
placeholder="e.g., Platinum Matrix" placeholder="e.g., Platinum Matrix"
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-gray-800 mb-1">Top-node Email</label> <label className="block text-sm font-medium text-blue-900 mb-1">Top-node Email</label>
<input <input
type="email" type="email"
value={createEmail} value={createEmail}
onChange={e => setCreateEmail(e.target.value)} onChange={e => setCreateEmail(e.target.value)}
disabled={createLoading} disabled={createLoading}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent disabled:bg-gray-100" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent disabled:bg-gray-100"
placeholder="owner@example.com" placeholder="owner@example.com"
/> />
</div> </div>
@ -497,19 +495,19 @@ export default function MatrixManagementPage() {
</div> </div>
)} )}
<div className="pt-2 flex items-center justify-end gap-2"> <div className="pt-2 flex items-center justify-end gap-3">
<button <button
type="button" type="button"
onClick={() => { setCreateOpen(false); resetForm() }} onClick={() => { setCreateOpen(false); resetForm() }}
disabled={createLoading} disabled={createLoading}
className="rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50" className="rounded-lg border border-gray-300 px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 disabled:opacity-50"
> >
Cancel Cancel
</button> </button>
<button <button
type="submit" type="submit"
disabled={createLoading} disabled={createLoading}
className="rounded-md bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 text-sm font-medium disabled:opacity-50 inline-flex items-center gap-2" className="rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-sm font-semibold shadow disabled:opacity-50 inline-flex items-center gap-2"
> >
{createLoading && <span className="h-4 w-4 rounded-full border-2 border-white border-b-transparent animate-spin" />} {createLoading && <span className="h-4 w-4 rounded-full border-2 border-white border-b-transparent animate-spin" />}
{createLoading ? 'Creating...' : 'Create Matrix'} {createLoading ? 'Creating...' : 'Create Matrix'}

View File

@ -51,10 +51,10 @@ export default function AdminDashboardPage() {
if (!isClient) { if (!isClient) {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="min-h-screen flex items-center justify-center bg-blue-50">
<div className="text-center"> <div className="text-center">
<div className="h-12 w-12 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" /> <div className="h-12 w-12 rounded-full border-2 border-blue-900 border-b-transparent animate-spin mx-auto mb-4" />
<p className="text-gray-600">Loading...</p> <p className="text-blue-900">Loading...</p>
</div> </div>
</div> </div>
</PageLayout> </PageLayout>
@ -65,7 +65,7 @@ export default function AdminDashboardPage() {
if (!isAdmin) { if (!isAdmin) {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="min-h-screen flex items-center justify-center bg-blue-50">
<div className="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8"> <div className="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8">
<div className="text-center"> <div className="text-center">
<h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1> <h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1>
@ -79,19 +79,21 @@ export default function AdminDashboardPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen bg-gray-50"> <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
{/* Heading */} {/* Header */}
<div className="mb-8"> <header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1> <div>
<p className="text-sm sm:text-base text-gray-600 mt-2"> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Admin Dashboard</h1>
Manage all administrative features, user management, permissions, and global settings. <p className="text-lg text-blue-700 mt-2">
</p> Manage all administrative features, user management, permissions, and global settings.
</div> </p>
</div>
</header>
{/* Warning banner */} {/* Warning banner */}
<div className="rounded-lg border border-red-300 bg-red-50 text-red-700 px-5 py-4 flex gap-3 items-start text-sm mb-8"> <div className="rounded-2xl border border-red-300 bg-red-50 text-red-700 px-8 py-6 flex gap-3 items-start text-base mb-8 shadow">
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" /> <ExclamationTriangleIcon className="h-6 w-6 flex-shrink-0 text-red-500 mt-0.5" />
<div className="leading-relaxed"> <div className="leading-relaxed">
<p className="font-semibold mb-0.5"> <p className="font-semibold mb-0.5">
Warning: Settings and actions below this point can have consequences for the entire system! Warning: Settings and actions below this point can have consequences for the entire system!
@ -102,123 +104,151 @@ export default function AdminDashboardPage() {
</div> </div>
</div> </div>
{/* Management Shortcuts Card - full width */} {/* Stats Card */}
<div className="mb-8 grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-6">
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Total Users</div>
<div className="text-xl font-semibold text-blue-900">{displayStats.totalUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Admins</div>
<div className="text-xl font-semibold text-indigo-700">{displayStats.adminUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Active</div>
<div className="text-xl font-semibold text-green-700">{displayStats.activeUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Pending Verification</div>
<div className="text-xl font-semibold text-amber-700">{displayStats.verificationPending}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Personal</div>
<div className="text-xl font-semibold text-blue-700">{displayStats.personalUsers}</div>
</div>
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-xs text-gray-500">Company</div>
<div className="text-xl font-semibold text-purple-700">{displayStats.companyUsers}</div>
</div>
</div>
{/* Management Shortcuts Card */}
<div className="mb-8"> <div className="mb-8">
<div className="rounded-xl border border-gray-200 bg-[#f5f9ff] p-6 shadow-sm hover:shadow-md transition"> <div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
<div className="flex items-start gap-3 mb-5"> <div className="flex items-start gap-4 mb-6">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100"> <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100">
<Squares2X2Icon className="h-6 w-6 text-blue-600" /> <Squares2X2Icon className="h-7 w-7 text-blue-600" />
</div> </div>
<div> <div>
<h2 className="text-sm font-semibold text-[#0d315f]">Management Shortcuts</h2> <h2 className="text-lg font-semibold text-blue-900">Management Shortcuts</h2>
<p className="text-xs text-[#52739a] mt-0.5"> <p className="text-sm text-blue-700 mt-0.5">
Quick access to common admin modules. Quick access to common admin modules.
</p> </p>
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<button <button
type="button" type="button"
onClick={() => router.push('/admin/matrix-management')} onClick={() => router.push('/admin/matrix-management')}
className="group w-full flex items-center justify-between rounded-lg border border-blue-200 bg-white/70 hover:bg-white px-3 py-3 transition" className="group w-full flex items-center justify-between rounded-lg border border-blue-200 bg-blue-50 hover:bg-blue-100 px-4 py-4 transition"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-blue-50 border border-blue-100"> <span className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-blue-100 border border-blue-200">
<Squares2X2Icon className="h-5 w-5 text-blue-600" /> <Squares2X2Icon className="h-6 w-6 text-blue-600" />
</span> </span>
<div className="text-left"> <div className="text-left">
<div className="text-sm font-semibold text-[#0d315f]">Matrix Management</div> <div className="text-base font-semibold text-blue-900">Matrix Management</div>
<div className="text-[11px] text-[#52739a]">Configure matrices and users</div> <div className="text-xs text-blue-700">Configure matrices and users</div>
</div> </div>
</div> </div>
<ArrowRightIcon className="h-4 w-4 text-blue-600 opacity-70 group-hover:opacity-100" /> <ArrowRightIcon className="h-5 w-5 text-blue-600 opacity-70 group-hover:opacity-100" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => router.push('/admin/subscriptions')} onClick={() => router.push('/admin/subscriptions')}
className="group w-full flex items-center justify-between rounded-lg border border-amber-200 bg-white/70 hover:bg-white px-3 py-3 transition" className="group w-full flex items-center justify-between rounded-lg border border-amber-200 bg-amber-50 hover:bg-amber-100 px-4 py-4 transition"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-amber-50 border border-amber-100"> <span className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-amber-100 border border-amber-200">
<BanknotesIcon className="h-5 w-5 text-amber-600" /> <BanknotesIcon className="h-6 w-6 text-amber-600" />
</span> </span>
<div className="text-left"> <div className="text-left">
<div className="text-sm font-semibold text-[#6b4e16]">Coffee Subscription Management</div> <div className="text-base font-semibold text-amber-900">Coffee Subscription Management</div>
<div className="text-[11px] text-[#8a6c2b]">Plans, billing and renewals</div> <div className="text-xs text-amber-700">Plans, billing and renewals</div>
</div> </div>
</div> </div>
<ArrowRightIcon className="h-4 w-4 text-amber-600 opacity-70 group-hover:opacity-100" /> <ArrowRightIcon className="h-5 w-5 text-amber-600 opacity-70 group-hover:opacity-100" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => router.push('/admin/contract-management')} onClick={() => router.push('/admin/contract-management')}
className="group w-full flex items-center justify-between rounded-lg border border-indigo-200 bg-white/70 hover:bg-white px-3 py-3 transition" className="group w-full flex items-center justify-between rounded-lg border border-indigo-200 bg-indigo-50 hover:bg-indigo-100 px-4 py-4 transition"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-indigo-50 border border-indigo-100"> <span className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-indigo-100 border border-indigo-200">
<ClipboardDocumentListIcon className="h-5 w-5 text-indigo-600" /> <ClipboardDocumentListIcon className="h-6 w-6 text-indigo-600" />
</span> </span>
<div className="text-left"> <div className="text-left">
<div className="text-sm font-semibold text-[#1f2a5a]">Contract Management</div> <div className="text-base font-semibold text-indigo-900">Contract Management</div>
<div className="text-[11px] text-[#4b5a9a]">Templates, approvals, status</div> <div className="text-xs text-indigo-700">Templates, approvals, status</div>
</div> </div>
</div> </div>
<ArrowRightIcon className="h-4 w-4 text-indigo-600 opacity-70 group-hover:opacity-100" /> <ArrowRightIcon className="h-5 w-5 text-indigo-600 opacity-70 group-hover:opacity-100" />
</button> </button>
<button <button
type="button" type="button"
onClick={() => router.push('/admin/user-management')} onClick={() => router.push('/admin/user-management')}
className="group w-full flex items-center justify-between rounded-lg border border-gray-200 bg-white/70 hover:bg-white px-3 py-3 transition" className="group w-full flex items-center justify-between rounded-lg border border-gray-200 bg-gray-50 hover:bg-blue-50 px-4 py-4 transition"
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-gray-50 border border-gray-100"> <span className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-blue-100 border border-blue-200">
<UsersIcon className="h-5 w-5 text-blue-600" /> <UsersIcon className="h-6 w-6 text-blue-600" />
</span> </span>
<div className="text-left"> <div className="text-left">
<div className="text-sm font-semibold text-[#0d315f]">User Management</div> <div className="text-base font-semibold text-blue-900">User Management</div>
<div className="text-[11px] text-[#52739a]">Browse, search, and manage all users</div> <div className="text-xs text-blue-700">Browse, search, and manage all users</div>
</div> </div>
</div> </div>
<ArrowRightIcon className="h-4 w-4 text-blue-600 opacity-70 group-hover:opacity-100" /> <ArrowRightIcon className="h-5 w-5 text-blue-600 opacity-70 group-hover:opacity-100" />
</button> </button>
</div> </div>
</div> </div>
</div> </div>
{/* Server Status & Logs */} {/* Server Status & Logs */}
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md transition"> <div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
<div className="flex items-start gap-3 mb-6"> <div className="flex items-start gap-4 mb-6">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100"> <div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gray-100">
<ServerStackIcon className="h-6 w-6 text-gray-700" /> <ServerStackIcon className="h-7 w-7 text-gray-700" />
</div> </div>
<div> <div>
<h2 className="text-sm font-semibold text-gray-900"> <h2 className="text-lg font-semibold text-gray-900">
Server Status & Logs Server Status & Logs
</h2> </h2>
<p className="text-xs text-gray-500 mt-0.5"> <p className="text-sm text-gray-500 mt-0.5">
System health, resource usage & recent error insights. System health, resource usage & recent error insights.
</p> </p>
</div> </div>
</div> </div>
<div className="grid gap-6 lg:grid-cols-3"> <div className="grid gap-8 lg:grid-cols-3">
{/* Metrics */} {/* Metrics */}
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className={`h-2.5 w-2.5 rounded-full ${serverStats.status === 'Online' ? 'bg-emerald-500' : 'bg-red-500'}`} /> <span className={`h-2.5 w-2.5 rounded-full ${serverStats.status === 'Online' ? 'bg-emerald-500' : 'bg-red-500'}`} />
<p className="text-sm"> <p className="text-base">
<span className="font-semibold">Server Status:</span>{' '} <span className="font-semibold">Server Status:</span>{' '}
<span className={serverStats.status === 'Online' ? 'text-emerald-600 font-medium' : 'text-red-600 font-medium'}> <span className={serverStats.status === 'Online' ? 'text-emerald-600 font-medium' : 'text-red-600 font-medium'}>
{serverStats.status === 'Online' ? 'Server Online' : 'Offline'} {serverStats.status === 'Online' ? 'Server Online' : 'Offline'}
</span> </span>
</p> </p>
</div> </div>
<div className="text-xs space-y-1 text-gray-600"> <div className="text-sm space-y-1 text-gray-600">
<p><span className="font-medium text-gray-700">Uptime:</span> {serverStats.uptime}</p> <p><span className="font-medium text-gray-700">Uptime:</span> {serverStats.uptime}</p>
<p><span className="font-medium text-gray-700">CPU Usage:</span> {serverStats.cpu}</p> <p><span className="font-medium text-gray-700">CPU Usage:</span> {serverStats.cpu}</p>
<p><span className="font-medium text-gray-700">Memory Usage:</span> {serverStats.memory} GB</p> <p><span className="font-medium text-gray-700">Memory Usage:</span> {serverStats.memory} GB</p>
</div> </div>
<div className="flex items-center gap-2 text-xs text-gray-500"> <div className="flex items-center gap-2 text-sm text-gray-500">
<CpuChipIcon className="h-4 w-4" /> <CpuChipIcon className="h-4 w-4" />
<span>Autoscaled environment (mock)</span> <span>Autoscaled environment (mock)</span>
</div> </div>
@ -229,11 +259,11 @@ export default function AdminDashboardPage() {
{/* Logs */} {/* Logs */}
<div className="lg:col-span-2"> <div className="lg:col-span-2">
<h3 className="text-sm font-semibold text-gray-800 mb-3"> <h3 className="text-base font-semibold text-gray-800 mb-3">
Recent Error Logs Recent Error Logs
</h3> </h3>
{serverStats.recentErrors.length === 0 && ( {serverStats.recentErrors.length === 0 && (
<p className="text-xs text-gray-500 italic"> <p className="text-sm text-gray-500 italic">
No recent logs. No recent logs.
</p> </p>
)} )}
@ -242,12 +272,12 @@ export default function AdminDashboardPage() {
<div className="mt-6"> <div className="mt-6">
<button <button
type="button" type="button"
className="inline-flex items-center gap-1.5 rounded-md border border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-700 text-xs font-medium px-3 py-2 transition" className="inline-flex items-center gap-1.5 rounded-lg border border-gray-300 bg-gray-50 hover:bg-gray-100 text-gray-700 text-sm font-medium px-4 py-3 transition"
// TODO: navigate to logs / monitoring page // TODO: navigate to logs / monitoring page
onClick={() => {}} onClick={() => {}}
> >
View Full Logs View Full Logs
<ArrowRightIcon className="h-4 w-4" /> <ArrowRightIcon className="h-5 w-5" />
</button> </button>
</div> </div>
</div> </div>

View File

@ -55,51 +55,61 @@ export default function CreateSubscriptionPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen bg-gray-50"> <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
<div className="mb-6 flex items-center justify-between"> {/* Header */}
<div> <header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<h1 className="text-3xl font-bold text-gray-900">Create Subscription</h1> <div className="flex items-center justify-between">
<p className="text-sm sm:text-base text-gray-700 mt-2">Add a new product or subscription plan.</p> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Create Subscription</h1>
<p className="text-lg text-blue-700 mt-2">Add a new product or subscription plan.</p>
</div>
<Link href="/admin/subscriptions"
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>
Back to list
</Link>
</div> </div>
<Link href="/admin/subscriptions" className="inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-medium text-gray-700 ring-1 ring-inset ring-gray-300 hover:bg-gray-50"> </header>
Back to list
</Link>
</div>
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm"> <div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg">
<form onSubmit={onCreate} className="space-y-8"> <form onSubmit={onCreate} className="space-y-8">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2"> <div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
{/* Title */}
<div> <div>
<label htmlFor="title" className="block text-sm font-medium text-gray-900">Title</label> <label htmlFor="title" className="block text-sm font-medium text-blue-900">Title</label>
<input id="title" name="title" required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} /> <input id="title" name="title" required className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} />
</div> </div>
{/* Quantity */}
<div> <div>
<label htmlFor="quantity" className="block text-sm font-medium text-gray-900">Quantity</label> <label htmlFor="quantity" className="block text-sm font-medium text-blue-900">Quantity</label>
<input id="quantity" name="quantity" required min={1} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="Quantity" type="number" value={quantity} onChange={e => setQuantity(Number(e.target.value))} /> <input id="quantity" name="quantity" required min={1} className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="Quantity" type="number" value={quantity} onChange={e => setQuantity(Number(e.target.value))} />
</div> </div>
{/* Price */}
<div> <div>
<label htmlFor="price" className="block text-sm font-medium text-gray-900">Price</label> <label htmlFor="price" className="block text-sm font-medium text-blue-900">Price</label>
<input id="price" name="price" required min={0.01} step={0.01} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="Price" type="number" value={price} onChange={e => setPrice(Number(e.target.value))} /> <input id="price" name="price" required min={0.01} step={0.01} className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="Price" type="number" value={price} onChange={e => setPrice(Number(e.target.value))} />
</div> </div>
{/* Currency */}
<div> <div>
<label htmlFor="currency" className="block text-sm font-medium text-gray-900">Currency (e.g., EUR)</label> <label htmlFor="currency" className="block text-sm font-medium text-blue-900">Currency (e.g., EUR)</label>
<input id="currency" name="currency" required maxLength={3} pattern="[A-Za-z]{3}" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="EUR" value={currency} onChange={e => setCurrency(e.target.value.toUpperCase().slice(0,3))} /> <input id="currency" name="currency" required maxLength={3} pattern="[A-Za-z]{3}" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="EUR" value={currency} onChange={e => setCurrency(e.target.value.toUpperCase().slice(0,3))} />
</div> </div>
{/* Tax Rate */}
<div> <div>
<label htmlFor="tax_rate" className="block text-sm font-medium text-gray-900">Tax rate (%)</label> <label htmlFor="tax_rate" className="block text-sm font-medium text-blue-900">Tax rate (%)</label>
<input id="tax_rate" name="tax_rate" min={0} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="e.g. 19" type="number" step="0.01" value={taxRate ?? ''} onChange={e => setTaxRate(e.target.value === '' ? undefined : Number(e.target.value))} /> <input id="tax_rate" name="tax_rate" min={0} className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="e.g. 19" type="number" step="0.01" value={taxRate ?? ''} onChange={e => setTaxRate(e.target.value === '' ? undefined : Number(e.target.value))} />
</div> </div>
{/* Featured */}
<div className="flex items-center gap-2 mt-6"> <div className="flex items-center gap-2 mt-6">
<input id="featured" type="checkbox" className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500" checked={isFeatured} onChange={e => setIsFeatured(e.target.checked)} /> <input id="featured" type="checkbox" className="h-4 w-4 rounded border-gray-300 text-blue-900 focus:ring-blue-900" checked={isFeatured} onChange={e => setIsFeatured(e.target.checked)} />
<label htmlFor="featured" className="text-sm font-medium text-gray-900">Featured</label> <label htmlFor="featured" className="text-sm font-medium text-blue-900">Featured</label>
</div> </div>
{/* Billing Interval */}
<div> <div>
<label htmlFor="billing_interval" className="block text-sm font-medium text-gray-900">Billing interval</label> <label htmlFor="billing_interval" className="block text-sm font-medium text-blue-900">Billing interval</label>
<select id="billing_interval" name="billing_interval" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black" value={billingInterval} onChange={e => { <select id="billing_interval" name="billing_interval" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black" value={billingInterval} onChange={e => {
const v = e.target.value as any; const v = e.target.value as any;
setBillingInterval(v); setBillingInterval(v);
if (v) { if (v) {
@ -115,57 +125,77 @@ export default function CreateSubscriptionPage() {
<option value="year">Year</option> <option value="year">Year</option>
</select> </select>
</div> </div>
{/* Interval Count */}
<div> <div>
<label htmlFor="interval_count" className="block text-sm font-medium text-gray-900">Interval count</label> <label htmlFor="interval_count" className="block text-sm font-medium text-blue-900">Interval count</label>
<input id="interval_count" name="interval_count" min={1} className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400 disabled:bg-gray-100 disabled:text-gray-500" placeholder="e.g. 6 for 6 months (defaults to 1)" type="number" value={intervalCount ?? ''} onChange={e => setIntervalCount(e.target.value === '' ? undefined : Number(e.target.value))} disabled={!billingInterval} /> <input id="interval_count" name="interval_count" min={1} className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400 disabled:bg-gray-100 disabled:text-gray-500" placeholder="e.g. 6 for 6 months (defaults to 1)" type="number" value={intervalCount ?? ''} onChange={e => setIntervalCount(e.target.value === '' ? undefined : Number(e.target.value))} disabled={!billingInterval} />
</div> </div>
{/* SKU */}
<div> <div>
<label htmlFor="sku" className="block text-sm font-medium text-gray-900">SKU (optional)</label> <label htmlFor="sku" className="block text-sm font-medium text-blue-900">SKU (optional)</label>
<input id="sku" name="sku" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="SKU" value={sku} onChange={e => setSku(e.target.value)} /> <input id="sku" name="sku" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="SKU" value={sku} onChange={e => setSku(e.target.value)} />
</div> </div>
{/* Slug */}
<div> <div>
<label htmlFor="slug" className="block text-sm font-medium text-gray-900">Slug (optional)</label> <label htmlFor="slug" className="block text-sm font-medium text-blue-900">Slug (optional)</label>
<input id="slug" name="slug" className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" placeholder="slug" value={slug} onChange={e => setSlug(e.target.value)} /> <input id="slug" name="slug" className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" placeholder="slug" value={slug} onChange={e => setSlug(e.target.value)} />
</div> </div>
{/* Availability */}
<div> <div>
<label htmlFor="availability" className="block text-sm font-medium text-gray-900">Availability</label> <label htmlFor="availability" className="block text-sm font-medium text-blue-900">Availability</label>
<select id="availability" name="availability" required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black" value={state} onChange={e => setState(e.target.value as any)}> <select id="availability" name="availability" required className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black" value={state} onChange={e => setState(e.target.value as any)}>
<option value="available">Available</option> <option value="available">Available</option>
<option value="unavailable">Unavailable</option> <option value="unavailable">Unavailable</option>
</select> </select>
</div> </div>
</div> </div>
{/* Description */}
<div> <div>
<label htmlFor="description" className="block text-sm font-medium text-gray-900">Description</label> <label htmlFor="description" className="block text-sm font-medium text-blue-900">Description</label>
<textarea id="description" name="description" required className="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 px-3 py-2.5 text-black placeholder:text-gray-400" rows={3} placeholder="Describe the product" value={description} onChange={e => setDescription(e.target.value)} /> <textarea id="description" name="description" required className="mt-1 block w-full rounded-lg border-gray-300 shadow focus:border-blue-900 focus:ring-blue-900 px-4 py-3 text-black placeholder:text-gray-400" rows={3} placeholder="Describe the product" value={description} onChange={e => setDescription(e.target.value)} />
<p className="mt-1 text-xs text-gray-600">Shown to users in the shop and checkout.</p> <p className="mt-1 text-xs text-gray-600">Shown to users in the shop and checkout.</p>
</div> </div>
{/* Picture Upload */}
<div> <div>
<label className="block text-sm font-medium text-gray-900">Picture</label> <label className="block text-sm font-medium text-blue-900">Picture</label>
<div className="mt-2 flex max-w-xl justify-center rounded-lg border border-dashed border-gray-300 px-6 py-10 bg-gray-50"> <div
<div className="text-center"> className="mt-2 flex max-w-xl justify-center rounded-lg border border-dashed border-blue-300 px-6 py-10 bg-blue-50 cursor-pointer"
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-gray-500" /> onClick={() => document.getElementById('file-upload')?.click()}
<div className="mt-4 flex text-sm text-gray-700"> onDragOver={e => e.preventDefault()}
<label htmlFor="file-upload" className="relative cursor-pointer rounded-md bg-white font-medium text-indigo-600 focus-within:outline-none focus-within:ring-2 focus-within:ring-indigo-500 focus-within:ring-offset-2 hover:text-indigo-500 px-2 py-1 ring-1 ring-gray-200"> onDrop={e => {
<span>Upload a file</span> e.preventDefault();
<input id="file-upload" name="file-upload" type="file" accept="image/*" className="sr-only" onChange={e => setPictureFile(e.target.files?.[0])} /> if (e.dataTransfer.files?.[0]) setPictureFile(e.dataTransfer.files[0]);
</label> }}
<p className="pl-1">or drag and drop</p> >
<div className="text-center w-full">
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-blue-400" />
<div className="mt-4 text-sm text-blue-700">
<span>Drag and drop an image here</span>
</div> </div>
<p className="text-xs text-gray-600">PNG, JPG up to 10MB</p> <p className="text-xs text-blue-600 mt-2">PNG, JPG up to 10MB</p>
{pictureFile && (
<p className="mt-2 text-xs text-blue-900 font-medium">{pictureFile.name}</p>
)}
</div> </div>
<input
id="file-upload"
name="file-upload"
type="file"
accept="image/*"
className="hidden"
onChange={e => setPictureFile(e.target.files?.[0])}
/>
</div> </div>
</div> </div>
<div className="flex items-center justify-end gap-x-3"> {/* Actions */}
<Link href="/admin/subscriptions" className="text-sm font-medium text-gray-700 hover:text-gray-900"> <div className="flex items-center justify-end gap-x-4">
<Link href="/admin/subscriptions" className="text-sm font-medium text-blue-900 hover:text-blue-700">
Cancel Cancel
</Link> </Link>
<button type="submit" className="inline-flex justify-center rounded-md bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"> <button type="submit" className="inline-flex justify-center rounded-lg bg-blue-900 px-5 py-3 text-sm font-semibold text-blue-50 shadow hover:bg-blue-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-900 transition">
Create Create
</button> </button>
</div> </div>

View File

@ -37,34 +37,41 @@ export default function AdminSubscriptionsPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen bg-gray-50"> <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
<div className="mb-8 flex items-center justify-between"> {/* Header */}
<div> <header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<h1 className="text-3xl font-bold text-gray-900">Subscription Products</h1> <div className="flex items-center justify-between">
<p className="text-sm sm:text-base text-gray-700 mt-2">Manage all products and subscription plans.</p> <div>
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Subscription Products</h1>
<p className="text-lg text-blue-700 mt-2">Manage all products and subscription plans.</p>
</div>
<Link
href="/admin/subscriptions/createSubscription"
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg>
Create Subscription
</Link>
</div> </div>
<Link href="/admin/subscriptions/createSubscription" className="inline-flex items-center rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500"> </header>
Create Subscription
</Link>
</div>
{error && ( {error && (
<div className="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 ring-1 ring-inset ring-red-200">{error}</div> <div className="mb-4 rounded-md bg-red-50 p-3 text-sm text-red-700 ring-1 ring-inset ring-red-200">{error}</div>
)} )}
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{loading && ( {loading && (
<div className="col-span-full text-sm text-gray-700">Loading</div> <div className="col-span-full text-sm text-gray-700">Loading</div>
)} )}
{!loading && items.map(item => ( {!loading && items.map(item => (
<div key={item.id} className="rounded-xl border border-gray-200 bg-white p-5 shadow-sm"> <div key={item.id} className="rounded-2xl border border-gray-100 bg-white shadow-lg p-6 flex flex-col gap-3 hover:shadow-xl transition">
<div className="flex items-start justify-between gap-3"> <div className="flex items-start justify-between gap-3">
<h3 className="text-base font-semibold text-gray-900">{item.title}</h3> <h3 className="text-xl font-semibold text-blue-900">{item.title}</h3>
{availabilityBadge(!!item.state)} {availabilityBadge(!!item.state)}
</div> </div>
{item.pictureUrl && ( {item.pictureUrl && (
<img src={item.pictureUrl} alt={item.title} className="mt-3 w-full h-40 object-cover rounded-md ring-1 ring-gray-200" /> <img src={item.pictureUrl} alt={item.title} className="mt-3 w-full h-40 object-cover rounded-xl ring-1 ring-gray-200" />
)} )}
<p className="mt-3 text-sm text-gray-800 line-clamp-4">{item.description}</p> <p className="mt-3 text-sm text-gray-800 line-clamp-4">{item.description}</p>
<dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-2 text-sm"> <dl className="mt-4 grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
@ -87,17 +94,29 @@ export default function AdminSubscriptionsPage() {
) : null} ) : null}
</dl> </dl>
<div className="mt-4 flex gap-2"> <div className="mt-4 flex gap-2">
<button className="inline-flex items-center rounded-md bg-amber-50 px-3 py-1.5 text-xs font-medium text-amber-700 ring-1 ring-inset ring-amber-200 hover:bg-amber-100" onClick={async () => { await setProductState(item.id, !item.state); await load(); }}> <button
className={`inline-flex items-center rounded-lg px-4 py-2 text-xs font-medium shadow transition
${item.state
? 'bg-amber-50 text-amber-700 ring-1 ring-inset ring-amber-200 hover:bg-amber-100'
: 'bg-blue-900 text-blue-50 hover:bg-blue-800'}`}
onClick={async () => { await setProductState(item.id, !item.state); await load(); }}
>
{item.state ? 'Disable' : 'Enable'} {item.state ? 'Disable' : 'Enable'}
</button> </button>
<button className="inline-flex items-center rounded-md bg-red-50 px-3 py-1.5 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-200 hover:bg-red-100" onClick={async () => { await deleteProduct(item.id); await load(); }}> <button
className="inline-flex items-center rounded-lg bg-red-50 px-4 py-2 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-200 hover:bg-red-100 shadow transition"
onClick={async () => { await deleteProduct(item.id); await load(); }}
>
Delete Delete
</button> </button>
</div> </div>
</div> </div>
))} ))}
{!loading && !items.length && (
<div className="col-span-full py-8 text-center text-sm text-gray-500">No subscriptions found.</div>
)}
</div> </div>
</main> </div>
</div> </div>
</PageLayout> </PageLayout>
); );

View File

@ -138,10 +138,10 @@ export default function AdminUserManagementPage() {
if (!isClient) { if (!isClient) {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="min-h-screen flex items-center justify-center bg-blue-50">
<div className="text-center"> <div className="text-center">
<div className="h-12 w-12 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" /> <div className="h-12 w-12 rounded-full border-2 border-blue-900 border-b-transparent animate-spin mx-auto mb-4" />
<p className="text-gray-600">Loading...</p> <p className="text-blue-900">Loading...</p>
</div> </div>
</div> </div>
</PageLayout> </PageLayout>
@ -152,7 +152,7 @@ export default function AdminUserManagementPage() {
if (!isAdmin) { if (!isAdmin) {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="min-h-screen flex items-center justify-center bg-blue-50">
<div className="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8"> <div className="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8">
<div className="text-center"> <div className="text-center">
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" /> <ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" />
@ -251,48 +251,50 @@ export default function AdminUserManagementPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen bg-gray-50"> <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
{/* Title */} {/* Header */}
<div className="mb-8"> <header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<h1 className="text-3xl font-bold text-gray-900">User Management</h1> <div>
<p className="text-gray-600 mt-2"> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">User Management</h1>
Manage all users, view statistics, and handle verification. <p className="text-lg text-blue-700 mt-2">
</p> Manage all users, view statistics, and handle verification.
</div> </p>
</div>
</header>
{/* Statistic Section + Verify Button */} {/* Statistic Section + Verify Button */}
<div className="mb-8 flex flex-col sm:flex-row sm:items-center gap-4"> <div className="mb-8 flex flex-col sm:flex-row sm:items-center gap-6">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4 flex-1"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-6 flex-1">
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center"> <div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-[11px] text-gray-500">Total Users</div> <div className="text-xs text-gray-500">Total Users</div>
<div className="text-base font-semibold text-gray-900">{stats.total}</div> <div className="text-xl font-semibold text-blue-900">{stats.total}</div>
</div> </div>
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center"> <div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-[11px] text-gray-500">Admins</div> <div className="text-xs text-gray-500">Admins</div>
<div className="text-base font-semibold text-indigo-700">{stats.admins}</div> <div className="text-xl font-semibold text-indigo-700">{stats.admins}</div>
</div> </div>
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center"> <div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-[11px] text-gray-500">Personal</div> <div className="text-xs text-gray-500">Personal</div>
<div className="text-base font-semibold text-blue-700">{stats.personal}</div> <div className="text-xl font-semibold text-blue-700">{stats.personal}</div>
</div> </div>
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center"> <div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-[11px] text-gray-500">Company</div> <div className="text-xs text-gray-500">Company</div>
<div className="text-base font-semibold text-purple-700">{stats.company}</div> <div className="text-xl font-semibold text-purple-700">{stats.company}</div>
</div> </div>
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center"> <div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-[11px] text-gray-500">Active</div> <div className="text-xs text-gray-500">Active</div>
<div className="text-base font-semibold text-green-700">{stats.active}</div> <div className="text-xl font-semibold text-green-700">{stats.active}</div>
</div> </div>
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center"> <div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
<div className="text-[11px] text-gray-500">Pending</div> <div className="text-xs text-gray-500">Pending</div>
<div className="text-base font-semibold text-amber-700">{stats.pending}</div> <div className="text-xl font-semibold text-amber-700">{stats.pending}</div>
</div> </div>
</div> </div>
<div> <div>
<button <button
type="button" type="button"
className="inline-flex items-center gap-2 rounded-md bg-amber-50 hover:bg-amber-100 border border-amber-200 text-amber-700 text-sm font-medium px-4 py-2.5 transition" className="inline-flex items-center gap-2 rounded-lg bg-amber-100 hover:bg-amber-200 border border-amber-200 text-amber-800 text-base font-semibold px-5 py-3 shadow transition"
onClick={() => window.location.href = '/admin/user-verify'} onClick={() => window.location.href = '/admin/user-verify'}
> >
Go to User Verification Go to User Verification
@ -302,7 +304,7 @@ export default function AdminUserManagementPage() {
{/* Error Message */} {/* Error Message */}
{error && ( {error && (
<div className="rounded-lg border border-red-300 bg-red-50 text-red-700 px-5 py-4 flex gap-3 items-start mb-6"> <div className="rounded-xl border border-red-300 bg-red-50 text-red-700 px-6 py-5 flex gap-3 items-start mb-8 shadow">
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" /> <ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" />
<div> <div>
<p className="font-semibold">Error loading users</p> <p className="font-semibold">Error loading users</p>
@ -320,22 +322,22 @@ export default function AdminUserManagementPage() {
{/* Filter Card */} {/* Filter Card */}
<form <form
onSubmit={applyFilter} onSubmit={applyFilter}
className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 px-4 sm:px-6 py-5 flex flex-col gap-4 mb-6" className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 px-8 py-8 flex flex-col gap-6 mb-8"
> >
<h2 className="text-sm font-semibold text-[#0f2c55]"> <h2 className="text-lg font-semibold text-blue-900">
Search & Filter Users Search & Filter Users
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4"> <div className="grid grid-cols-1 md:grid-cols-5 gap-6">
{/* Search */} {/* Search */}
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="sr-only">Search</label> <label className="sr-only">Search</label>
<div className="relative"> <div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" /> <MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-blue-300" />
<input <input
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
placeholder="Email, name, company..." placeholder="Email, name, company..."
className="w-full rounded-md border border-gray-300 pl-10 pr-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 pl-10 pr-3 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
/> />
</div> </div>
</div> </div>
@ -344,7 +346,7 @@ export default function AdminUserManagementPage() {
<select <select
value={fType} value={fType}
onChange={e => setFType(e.target.value as any)} onChange={e => setFType(e.target.value as any)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
> >
<option value="all">All Types</option> <option value="all">All Types</option>
<option value="personal">Personal</option> <option value="personal">Personal</option>
@ -356,7 +358,7 @@ export default function AdminUserManagementPage() {
<select <select
value={fStatus} value={fStatus}
onChange={e => setFStatus(e.target.value as any)} onChange={e => setFStatus(e.target.value as any)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
> >
<option value="all">All Status</option> <option value="all">All Status</option>
{STATUSES.map(s => <option key={s} value={s}>{s[0].toUpperCase()+s.slice(1)}</option>)} {STATUSES.map(s => <option key={s} value={s}>{s[0].toUpperCase()+s.slice(1)}</option>)}
@ -367,26 +369,25 @@ export default function AdminUserManagementPage() {
<select <select
value={fRole} value={fRole}
onChange={e => setFRole(e.target.value as any)} onChange={e => setFRole(e.target.value as any)}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm text-gray-900 focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
> >
<option value="all">All Roles</option> <option value="all">All Roles</option>
{ROLES.map(r => <option key={r} value={r}>{r[0].toUpperCase()+r.slice(1)}</option>)} {ROLES.map(r => <option key={r} value={r}>{r[0].toUpperCase()+r.slice(1)}</option>)}
</select> </select>
</div> </div>
</div> </div>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-3">
{/* NEW: Export CSV (exports all filtered results) */}
<button <button
type="button" type="button"
onClick={exportCsv} onClick={exportCsv}
className="inline-flex items-center gap-2 rounded-md border border-gray-300 bg-white hover:bg-gray-50 text-gray-700 text-sm font-semibold px-5 py-2.5 shadow-sm transition" className="inline-flex items-center gap-2 rounded-lg border border-gray-300 bg-white hover:bg-gray-50 text-blue-900 text-sm font-semibold px-5 py-3 shadow transition"
title="Export all filtered users to CSV" title="Export all filtered users to CSV"
> >
Export all users as CSV Export all users as CSV
</button> </button>
<button <button
type="submit" type="submit"
className="inline-flex items-center gap-2 rounded-md bg-blue-600 hover:bg-blue-500 text-white text-sm font-semibold px-5 py-2.5 shadow transition" className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 text-sm font-semibold px-5 py-3 shadow transition"
> >
Filter Filter
</button> </button>
@ -394,9 +395,9 @@ export default function AdminUserManagementPage() {
</form> </form>
{/* Users Table */} {/* Users Table */}
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 overflow-hidden"> <div className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 overflow-hidden mb-8">
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between"> <div className="px-8 py-6 border-b border-gray-100 flex items-center justify-between">
<div className="text-sm font-semibold text-[#0f2c55]"> <div className="text-lg font-semibold text-blue-900">
All Users All Users
</div> </div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
@ -405,15 +406,15 @@ export default function AdminUserManagementPage() {
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-100 text-sm"> <table className="min-w-full divide-y divide-gray-100 text-sm">
<thead className="bg-gray-50 text-gray-600 font-medium"> <thead className="bg-blue-50 text-blue-900 font-medium">
<tr> <tr>
<th className="px-4 py-2 text-left">User</th> <th className="px-4 py-3 text-left">User</th>
<th className="px-4 py-2 text-left">Type</th> <th className="px-4 py-3 text-left">Type</th>
<th className="px-4 py-2 text-left">Status</th> <th className="px-4 py-3 text-left">Status</th>
<th className="px-4 py-2 text-left">Role</th> <th className="px-4 py-3 text-left">Role</th>
<th className="px-4 py-2 text-left">Created</th> <th className="px-4 py-3 text-left">Created</th>
<th className="px-4 py-2 text-left">Last Login</th> <th className="px-4 py-3 text-left">Last Login</th>
<th className="px-4 py-2 text-left">Actions</th> <th className="px-4 py-3 text-left">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-gray-100">
@ -421,8 +422,8 @@ export default function AdminUserManagementPage() {
<tr> <tr>
<td colSpan={7} className="px-4 py-10 text-center"> <td colSpan={7} className="px-4 py-10 text-center">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<div className="h-4 w-4 rounded-full border-2 border-blue-500 border-b-transparent animate-spin" /> <div className="h-4 w-4 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
<span className="text-sm text-gray-500">Loading users...</span> <span className="text-sm text-blue-900">Loading users...</span>
</div> </div>
</td> </td>
</tr> </tr>
@ -447,34 +448,34 @@ export default function AdminUserManagementPage() {
const lastLoginDate = u.last_login_at ? new Date(u.last_login_at).toLocaleDateString() : 'Never' const lastLoginDate = u.last_login_at ? new Date(u.last_login_at).toLocaleDateString() : 'Never'
return ( return (
<tr key={u.id} className="hover:bg-gray-50"> <tr key={u.id} className="hover:bg-blue-50">
<td className="px-4 py-3"> <td className="px-4 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-9 w-9 flex items-center justify-center rounded-full bg-gradient-to-br from-indigo-500 to-indigo-600 text-white text-xs font-semibold shadow"> <div className="h-9 w-9 flex items-center justify-center rounded-full bg-gradient-to-br from-blue-900 to-blue-700 text-white text-xs font-semibold shadow">
{initials} {initials}
</div> </div>
<div> <div>
<div className="font-medium text-gray-900 leading-tight"> <div className="font-medium text-blue-900 leading-tight">
{displayName} {displayName}
</div> </div>
<div className="text-[11px] text-gray-500"> <div className="text-[11px] text-blue-700">
{u.email} {u.email}
</div> </div>
</div> </div>
</div> </div>
</td> </td>
<td className="px-4 py-3">{typeBadge(u.user_type)}</td> <td className="px-4 py-4">{typeBadge(u.user_type)}</td>
<td className="px-4 py-3">{statusBadge(userStatus)}</td> <td className="px-4 py-4">{statusBadge(userStatus)}</td>
<td className="px-4 py-3">{roleBadge(u.role)}</td> <td className="px-4 py-4">{roleBadge(u.role)}</td>
<td className="px-4 py-3 text-gray-700">{createdDate}</td> <td className="px-4 py-4 text-blue-900">{createdDate}</td>
<td className="px-4 py-3 text-gray-500 italic"> <td className="px-4 py-4 text-blue-700 italic">
{lastLoginDate} {lastLoginDate}
</td> </td>
<td className="px-4 py-3"> <td className="px-4 py-4">
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => onEdit(u.id.toString())} onClick={() => onEdit(u.id.toString())}
className="inline-flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-700 px-2.5 py-1 text-xs font-medium transition" className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-900 px-3 py-2 text-xs font-medium transition"
> >
<PencilSquareIcon className="h-4 w-4" /> Edit <PencilSquareIcon className="h-4 w-4" /> Edit
</button> </button>
@ -485,7 +486,7 @@ export default function AdminUserManagementPage() {
})} })}
{current.length === 0 && ( {current.length === 0 && (
<tr> <tr>
<td colSpan={7} className="px-4 py-10 text-center text-sm text-gray-500"> <td colSpan={7} className="px-4 py-10 text-center text-sm text-blue-700">
No users match current filters. No users match current filters.
</td> </td>
</tr> </tr>
@ -494,22 +495,22 @@ export default function AdminUserManagementPage() {
</table> </table>
</div> </div>
{/* Pagination */} {/* Pagination */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 px-5 py-4 bg-gray-50 border-t border-gray-100"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 px-8 py-6 bg-blue-50 border-t border-blue-100">
<div className="text-xs text-gray-600"> <div className="text-xs text-blue-700">
Page {page} of {totalPages} ({filtered.length} total users) Page {page} of {totalPages} ({filtered.length} total users)
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
disabled={page===1} disabled={page===1}
onClick={() => setPage(p => Math.max(1,p-1))} onClick={() => setPage(p => Math.max(1,p-1))}
className="px-3 py-1.5 text-xs font-medium rounded-md border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed" className="px-4 py-2 text-xs font-medium rounded-lg border border-gray-300 bg-white hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed"
> >
Previous Previous
</button> </button>
<button <button
disabled={page===totalPages} disabled={page===totalPages}
onClick={() => setPage(p => Math.min(totalPages,p+1))} onClick={() => setPage(p => Math.min(totalPages,p+1))}
className="px-3 py-1.5 text-xs font-medium rounded-md border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed" className="px-4 py-2 text-xs font-medium rounded-lg border border-gray-300 bg-white hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed"
> >
Next Next
</button> </button>

View File

@ -112,10 +112,10 @@ export default function AdminUserVerifyPage() {
if (!isClient) { if (!isClient) {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="min-h-screen flex items-center justify-center bg-blue-50">
<div className="text-center"> <div className="text-center">
<div className="h-12 w-12 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" /> <div className="h-12 w-12 rounded-full border-2 border-blue-900 border-b-transparent animate-spin mx-auto mb-4" />
<p className="text-gray-600">Loading...</p> <p className="text-blue-900">Loading...</p>
</div> </div>
</div> </div>
</PageLayout> </PageLayout>
@ -126,7 +126,7 @@ export default function AdminUserVerifyPage() {
if (!isAdmin) { if (!isAdmin) {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen flex items-center justify-center bg-gray-50"> <div className="min-h-screen flex items-center justify-center bg-blue-50">
<div className="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8"> <div className="mx-auto w-full max-w-xl rounded-2xl bg-white shadow ring-1 ring-red-500/20 p-8">
<div className="text-center"> <div className="text-center">
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" /> <ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" />
@ -141,19 +141,21 @@ export default function AdminUserVerifyPage() {
return ( return (
<PageLayout> <PageLayout>
<div className="min-h-screen bg-gray-50"> <div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
{/* Title (unified) */} {/* Header */}
<div className="mb-8 text-left"> <header className="sticky top-0 z-10 bg-white/90 backdrop-blur border-b border-blue-100 py-10 px-8 rounded-2xl shadow-lg flex flex-col gap-4 mb-8">
<h1 className="text-3xl font-bold text-gray-900">User Verification Center</h1> <div>
<p className="mt-2 text-sm sm:text-base text-gray-600"> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">User Verification Center</h1>
Review and verify all users who need admin approval. Users must complete all steps before verification. <p className="text-lg text-blue-700 mt-2">
</p> Review and verify all users who need admin approval. Users must complete all steps before verification.
</div> </p>
</div>
</header>
{/* Error Message */} {/* Error Message */}
{error && ( {error && (
<div className="rounded-lg border border-red-300 bg-red-50 text-red-700 px-5 py-4 flex gap-3 items-start mb-6"> <div className="rounded-xl border border-red-300 bg-red-50 text-red-700 px-6 py-5 flex gap-3 items-start mb-8 shadow">
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" /> <ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" />
<div> <div>
<p className="font-semibold">Error loading data</p> <p className="font-semibold">Error loading data</p>
@ -171,21 +173,21 @@ export default function AdminUserVerifyPage() {
{/* Filter Card */} {/* Filter Card */}
<form <form
onSubmit={applyFilters} onSubmit={applyFilters}
className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 px-4 sm:px-6 py-5 flex flex-col gap-4 mb-6" className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 px-8 py-8 flex flex-col gap-6 mb-8"
> >
<h2 className="text-sm font-semibold text-[#0f2c55]"> <h2 className="text-lg font-semibold text-blue-900">
Search & Filter Pending Users Search & Filter Pending Users
</h2> </h2>
<div className="grid grid-cols-1 md:grid-cols-6 gap-4"> <div className="grid grid-cols-1 md:grid-cols-6 gap-6">
<div className="md:col-span-2"> <div className="md:col-span-2">
<label className="sr-only">Search</label> <label className="sr-only">Search</label>
<div className="relative"> <div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-400" /> <MagnifyingGlassIcon className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-blue-300" />
<input <input
value={search} value={search}
onChange={e => setSearch(e.target.value)} onChange={e => setSearch(e.target.value)}
placeholder="Email, name, company..." placeholder="Email, name, company..."
className="w-full rounded-md border border-gray-300 pl-10 pr-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 pl-10 pr-3 py-3 text-sm focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
/> />
</div> </div>
</div> </div>
@ -193,7 +195,7 @@ export default function AdminUserVerifyPage() {
<select <select
value={fType} value={fType}
onChange={e => { setFType(e.target.value as any); setPage(1) }} onChange={e => { setFType(e.target.value as any); setPage(1) }}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
> >
<option value="all">All Types</option> <option value="all">All Types</option>
<option value="personal">Personal</option> <option value="personal">Personal</option>
@ -204,7 +206,7 @@ export default function AdminUserVerifyPage() {
<select <select
value={fRole} value={fRole}
onChange={e => { setFRole(e.target.value as any); setPage(1) }} onChange={e => { setFRole(e.target.value as any); setPage(1) }}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
> >
<option value="all">All Roles</option> <option value="all">All Roles</option>
<option value="user">User</option> <option value="user">User</option>
@ -215,7 +217,7 @@ export default function AdminUserVerifyPage() {
<select <select
value={perPage} value={perPage}
onChange={e => { setPerPage(parseInt(e.target.value, 10)); setPage(1) }} onChange={e => { setPerPage(parseInt(e.target.value, 10)); setPage(1) }}
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm focus:ring-2 focus:ring-indigo-500 focus:border-transparent" className="w-full rounded-lg border border-gray-300 px-4 py-3 text-sm text-blue-900 focus:ring-2 focus:ring-blue-900 focus:border-transparent shadow"
> >
{[5, 10, 15, 20].map(n => <option key={n} value={n}>{n}</option>)} {[5, 10, 15, 20].map(n => <option key={n} value={n}>{n}</option>)}
</select> </select>
@ -223,7 +225,7 @@ export default function AdminUserVerifyPage() {
<div className="flex items-stretch"> <div className="flex items-stretch">
<button <button
type="submit" type="submit"
className="w-full inline-flex items-center justify-center rounded-md bg-blue-600 hover:bg-blue-500 text-white text-sm font-semibold px-5 py-2.5 shadow transition" className="w-full inline-flex items-center justify-center rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 text-sm font-semibold px-5 py-3 shadow transition"
> >
Filter Filter
</button> </button>
@ -232,9 +234,9 @@ export default function AdminUserVerifyPage() {
</form> </form>
{/* Pending Users Table */} {/* Pending Users Table */}
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 overflow-hidden"> <div className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 overflow-hidden mb-8">
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between"> <div className="px-8 py-6 border-b border-gray-100 flex items-center justify-between">
<div className="text-sm font-semibold text-[#0f2c55]"> <div className="text-lg font-semibold text-blue-900">
Users Pending Verification Users Pending Verification
</div> </div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
@ -243,15 +245,15 @@ export default function AdminUserVerifyPage() {
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-100 text-sm"> <table className="min-w-full divide-y divide-gray-100 text-sm">
<thead className="bg-gray-50 text-gray-600 font-medium"> <thead className="bg-blue-50 text-blue-900 font-medium">
<tr> <tr>
<th className="px-4 py-2 text-left">User</th> <th className="px-4 py-3 text-left">User</th>
<th className="px-4 py-2 text-left">Type</th> <th className="px-4 py-3 text-left">Type</th>
<th className="px-4 py-2 text-left">Progress</th> <th className="px-4 py-3 text-left">Progress</th>
<th className="px-4 py-2 text-left">Status</th> <th className="px-4 py-3 text-left">Status</th>
<th className="px-4 py-2 text-left">Role</th> <th className="px-4 py-3 text-left">Role</th>
<th className="px-4 py-2 text-left">Created</th> <th className="px-4 py-3 text-left">Created</th>
<th className="px-4 py-2 text-left">Actions</th> <th className="px-4 py-3 text-left">Actions</th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-100"> <tbody className="divide-y divide-gray-100">
@ -259,8 +261,8 @@ export default function AdminUserVerifyPage() {
<tr> <tr>
<td colSpan={7} className="px-4 py-10 text-center"> <td colSpan={7} className="px-4 py-10 text-center">
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<div className="h-4 w-4 rounded-full border-2 border-blue-500 border-b-transparent animate-spin" /> <div className="h-4 w-4 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
<span className="text-sm text-gray-500">Loading users...</span> <span className="text-sm text-blue-900">Loading users...</span>
</div> </div>
</td> </td>
</tr> </tr>
@ -281,33 +283,33 @@ export default function AdminUserVerifyPage() {
u.documents_uploaded === 1 && u.contract_signed === 1 u.documents_uploaded === 1 && u.contract_signed === 1
return ( return (
<tr key={u.id} className="hover:bg-gray-50"> <tr key={u.id} className="hover:bg-blue-50">
<td className="px-4 py-3"> <td className="px-4 py-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="h-9 w-9 flex items-center justify-center rounded-full bg-gradient-to-br from-indigo-500 to-indigo-600 text-white text-xs font-semibold shadow"> <div className="h-9 w-9 flex items-center justify-center rounded-full bg-gradient-to-br from-blue-900 to-blue-700 text-white text-xs font-semibold shadow">
{initials} {initials}
</div> </div>
<div> <div>
<div className="font-medium text-gray-900 leading-tight"> <div className="font-medium text-blue-900 leading-tight">
{displayName} {displayName}
</div> </div>
<div className="text-[11px] text-gray-500">{u.email}</div> <div className="text-[11px] text-blue-700">{u.email}</div>
</div> </div>
</div> </div>
</td> </td>
<td className="px-4 py-3">{typeBadge(u.user_type)}</td> <td className="px-4 py-4">{typeBadge(u.user_type)}</td>
<td className="px-4 py-3">{verificationStatusBadge(u)}</td> <td className="px-4 py-4">{verificationStatusBadge(u)}</td>
<td className="px-4 py-3">{statusBadge(u.status)}</td> <td className="px-4 py-4">{statusBadge(u.status)}</td>
<td className="px-4 py-3">{roleBadge(u.role)}</td> <td className="px-4 py-4">{roleBadge(u.role)}</td>
<td className="px-4 py-3 text-gray-700">{createdDate}</td> <td className="px-4 py-4 text-blue-900">{createdDate}</td>
<td className="px-4 py-3"> <td className="px-4 py-4">
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
onClick={() => { onClick={() => {
setSelectedUserId(u.id.toString()) setSelectedUserId(u.id.toString())
setIsDetailModalOpen(true) setIsDetailModalOpen(true)
}} }}
className="inline-flex items-center gap-1 rounded-md border border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-700 px-2.5 py-1 text-xs font-medium transition" className="inline-flex items-center gap-1 rounded-lg border border-blue-200 bg-blue-50 hover:bg-blue-100 text-blue-900 px-3 py-2 text-xs font-medium transition"
> >
<EyeIcon className="h-4 w-4" /> View <EyeIcon className="h-4 w-4" /> View
</button> </button>
@ -316,7 +318,7 @@ export default function AdminUserVerifyPage() {
<button <button
onClick={() => handleVerifyUser(u.id.toString())} onClick={() => handleVerifyUser(u.id.toString())}
disabled={isVerifying} disabled={isVerifying}
className={`inline-flex items-center gap-1 rounded-md border px-2.5 py-1 text-xs font-medium transition className={`inline-flex items-center gap-1 rounded-lg border px-3 py-2 text-xs font-medium transition
${isVerifying ${isVerifying
? 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed' ? 'border-gray-200 bg-gray-100 text-gray-400 cursor-not-allowed'
: 'border-emerald-200 bg-emerald-50 hover:bg-emerald-100 text-emerald-700' : 'border-emerald-200 bg-emerald-50 hover:bg-emerald-100 text-emerald-700'
@ -343,7 +345,7 @@ export default function AdminUserVerifyPage() {
})} })}
{current.length === 0 && !loading && ( {current.length === 0 && !loading && (
<tr> <tr>
<td colSpan={7} className="px-4 py-10 text-center text-sm text-gray-500"> <td colSpan={7} className="px-4 py-10 text-center text-sm text-blue-700">
No unverified users match current filters. No unverified users match current filters.
</td> </td>
</tr> </tr>
@ -352,22 +354,22 @@ export default function AdminUserVerifyPage() {
</table> </table>
</div> </div>
{/* Pagination */} {/* Pagination */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 px-5 py-4 bg-gray-50 border-t border-gray-100"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 px-8 py-6 bg-blue-50 border-t border-blue-100">
<div className="text-xs text-gray-600"> <div className="text-xs text-blue-700">
Page {page} of {totalPages} ({filtered.length} pending users) Page {page} of {totalPages} ({filtered.length} pending users)
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<button <button
disabled={page === 1} disabled={page === 1}
onClick={() => setPage(p => Math.max(1, p - 1))} onClick={() => setPage(p => Math.max(1, p - 1))}
className="px-3 py-1.5 text-xs font-medium rounded-md border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed" className="px-4 py-2 text-xs font-medium rounded-lg border border-gray-300 bg-white hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed"
> >
Previous Previous
</button> </button>
<button <button
disabled={page === totalPages} disabled={page === totalPages}
onClick={() => setPage(p => Math.min(totalPages, p + 1))} onClick={() => setPage(p => Math.min(totalPages, p + 1))}
className="px-3 py-1.5 text-xs font-medium rounded-md border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-40 disabled:cursor-not-allowed" className="px-4 py-2 text-xs font-medium rounded-lg border border-gray-300 bg-white hover:bg-blue-50 disabled:opacity-40 disabled:cursor-not-allowed"
> >
Next Next
</button> </button>