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-medium text-sm text-gray-900 truncate">{c.name}</p> <p className="font-semibold text-lg text-gray-900 truncate">{c.name}</p>
<StatusBadge status={c.status} /> <StatusBadge status={c.status} />
</div> </div>
<p className="text-xs text-gray-600">Version {c.version}{c.updatedAt ? ` • Updated ${new Date(c.updatedAt).toLocaleString()}` : ''}</p> <p className="text-xs text-gray-500">Version {c.version}{c.updatedAt ? ` • Updated ${new Date(c.updatedAt).toLocaleString()}` : ''}</p>
</div> <div className="flex flex-wrap gap-2 mt-2">
<div className="flex flex-wrap gap-2"> <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={() => 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> <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={() => 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={() => 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={() => 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={() => onToggleState(c.id, c.status)} className={`px-3 py-1 text-xs rounded-lg font-semibold transition
<button onClick={() => onToggleState(c.id, c.status)} className="px-2 py-1 text-xs rounded bg-indigo-600 hover:bg-indigo-500 text-white"> ${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}
onClick={() => setSection(item.key)}
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>
{/* Main Content */}
<main className="flex-1 space-y-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-4">
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Contract Management</h1>
<p className="text-lg text-blue-700">
Manage contract templates, company stamp, and create new templates.
</p> </p>
</div> </header>
{/* Reordered vertical layout */} {/* Section Panels */}
<div className="space-y-6"> {section === 'stamp' && (
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm"> <section className="rounded-2xl bg-white shadow-lg border border-gray-100 p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Company Stamp</h2> <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>
Company Stamp
</h2>
<ContractUploadCompanyStamp onUploaded={bumpRefresh} /> <ContractUploadCompanyStamp onUploaded={bumpRefresh} />
</div> </section>
)}
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm"> {section === 'templates' && (
<h2 className="text-lg font-semibold text-gray-900 mb-4">Contracts</h2> <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} /> <ContractTemplateList refreshKey={refreshKey} />
</div> </section>
)}
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm"> {section === 'editor' && (
<h2 className="text-lg font-semibold text-gray-900 mb-4">Create Template</h2> <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} /> <ContractEditor onSaved={bumpRefresh} />
</div> </section>
</div> )}
</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 className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Matrix Management</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Matrix Management</h1>
<p className="text-sm text-gray-600 mt-1">Manage matrices, see stats, and create new ones.</p> <p className="text-lg text-blue-700 mt-2">Manage matrices, see stats, and create new ones.</p>
</div> </div>
<button <button
onClick={() => setCreateOpen(true)} 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" 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" />
Create New Matrix Create Matrix
</button> </button>
</div> </div>
</header>
{/* 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>
<p className="text-lg text-blue-700 mt-2">
Manage all administrative features, user management, permissions, and global settings. Manage all administrative features, user management, permissions, and global settings.
</p> </p>
</div> </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 */}
<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 className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Create Subscription</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Create Subscription</h1>
<p className="text-sm sm:text-base text-gray-700 mt-2">Add a new product or subscription plan.</p> <p className="text-lg text-blue-700 mt-2">Add a new product or subscription plan.</p>
</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"> <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 Back to list
</Link> </Link>
</div> </div>
</header>
<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 */}
<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 className="flex items-center justify-between">
<div> <div>
<h1 className="text-3xl font-bold text-gray-900">Subscription Products</h1> <h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Subscription Products</h1>
<p className="text-sm sm:text-base text-gray-700 mt-2">Manage all products and subscription plans.</p> <p className="text-lg text-blue-700 mt-2">Manage all products and subscription plans.</p>
</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"> <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 Create Subscription
</Link> </Link>
</div> </div>
</header>
{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> </div>
</main>
</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>
<p className="text-lg text-blue-700 mt-2">
Manage all users, view statistics, and handle verification. Manage all users, view statistics, and handle verification.
</p> </p>
</div> </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>
<p className="text-lg text-blue-700 mt-2">
Review and verify all users who need admin approval. Users must complete all steps before verification. Review and verify all users who need admin approval. Users must complete all steps before verification.
</p> </p>
</div> </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>