beautify: admin redesign
This commit is contained in:
parent
2439928eff
commit
805ed1fdf2
@ -132,29 +132,29 @@ export default function ContractEditor({ onSaved }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Template name"
|
||||
value={name}
|
||||
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">
|
||||
<button
|
||||
type="button"
|
||||
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'}
|
||||
</button>
|
||||
{isPreview && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { const w = iframeRef.current?.contentWindow; w?.focus(); w?.print(); }}
|
||||
className="inline-flex items-center rounded-md bg-gray-100 hover:bg-gray-200 text-gray-900 px-3 py-2 text-sm"
|
||||
onClick={printPreview}
|
||||
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
|
||||
</button>
|
||||
@ -163,18 +163,18 @@ export default function ContractEditor({ onSaved }: Props) {
|
||||
</div>
|
||||
|
||||
{/* New metadata inputs */}
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Type (e.g., contract, nda, invoice)"
|
||||
value={type}
|
||||
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
|
||||
value={lang}
|
||||
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="de">Deutsch (de)</option>
|
||||
@ -184,7 +184,7 @@ export default function ContractEditor({ onSaved }: Props) {
|
||||
placeholder="Description (optional)"
|
||||
value={description}
|
||||
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>
|
||||
@ -194,33 +194,33 @@ export default function ContractEditor({ onSaved }: Props) {
|
||||
value={htmlCode}
|
||||
onChange={(e) => setHtmlCode(e.target.value)}
|
||||
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 && (
|
||||
<div className="rounded-md border border-gray-300 bg-white">
|
||||
<div className="rounded-lg border border-gray-300 bg-white shadow">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title="Contract Preview"
|
||||
className="w-full rounded-md"
|
||||
className="w-full rounded-lg"
|
||||
style={{ height: 1200, background: 'transparent' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => save(false)}
|
||||
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)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => save(true)}
|
||||
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
|
||||
</button>
|
||||
|
||||
@ -17,11 +17,11 @@ type ContractTemplate = {
|
||||
|
||||
function StatusBadge({ status }: { status: string }) {
|
||||
const map: Record<string, string> = {
|
||||
draft: 'bg-gray-100 text-gray-800',
|
||||
published: 'bg-green-100 text-green-800',
|
||||
archived: 'bg-yellow-100 text-yellow-800',
|
||||
draft: 'bg-gray-100 text-gray-800 border border-gray-300',
|
||||
published: 'bg-green-100 text-green-800 border border-green-300',
|
||||
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>;
|
||||
}
|
||||
|
||||
@ -97,47 +97,48 @@ export default function ContractTemplateList({ refreshKey = 0 }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-2">
|
||||
<div className="space-y-4">
|
||||
<div className="flex gap-2 items-center">
|
||||
<input
|
||||
placeholder="Search…"
|
||||
placeholder="Search templates…"
|
||||
value={q}
|
||||
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
|
||||
onClick={load}
|
||||
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'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul className="divide-y divide-gray-200">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{filtered.map((c) => (
|
||||
<li key={c.id} className="py-3 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-medium text-sm text-gray-900 truncate">{c.name}</p>
|
||||
<StatusBadge status={c.status} />
|
||||
</div>
|
||||
<p className="text-xs text-gray-600">Version {c.version}{c.updatedAt ? ` • Updated ${new Date(c.updatedAt).toLocaleString()}` : ''}</p>
|
||||
<div key={c.id} className="rounded-xl border border-gray-100 bg-white shadow-sm p-4 flex flex-col gap-2 hover:shadow-md transition">
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="font-semibold text-lg text-gray-900 truncate">{c.name}</p>
|
||||
<StatusBadge status={c.status} />
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<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-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-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-2 py-1 text-xs rounded bg-indigo-600 hover:bg-indigo-500 text-white">
|
||||
<p className="text-xs text-gray-500">Version {c.version}{c.updatedAt ? ` • Updated ${new Date(c.updatedAt).toLocaleString()}` : ''}</p>
|
||||
<div className="flex flex-wrap gap-2 mt-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={() => 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={() => onDownloadPdf(c.id)} className="px-3 py-1 text-xs rounded-lg bg-gray-100 hover:bg-gray-200 text-gray-800 border border-gray-200 transition">Download</button>
|
||||
<button onClick={() => onToggleState(c.id, c.status)} className={`px-3 py-1 text-xs rounded-lg font-semibold transition
|
||||
${c.status === 'published'
|
||||
? 'bg-red-100 hover:bg-red-200 text-red-700 border border-red-200'
|
||||
: 'bg-indigo-600 hover:bg-indigo-500 text-white border border-indigo-600'}`}>
|
||||
{c.status === 'published' ? 'Deactivate' : 'Activate'}
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
{!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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -212,48 +212,48 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{/* Header with Add New Stamp modal trigger */}
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-sm text-gray-700">Manage your company stamps. One active at a time.</p>
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Emphasized Active stamp */}
|
||||
{activeStamp && (
|
||||
<div className="relative rounded-2xl p-[1px] bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500">
|
||||
<div className="rounded-2xl bg-white p-4 sm:p-5 flex items-center gap-4 sm:gap-6">
|
||||
<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-5 flex items-center gap-6">
|
||||
<div className="relative">
|
||||
{activeStamp.base64 ? (
|
||||
<img
|
||||
src={toImgSrc(activeStamp)}
|
||||
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
|
||||
</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
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-sm 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-base font-semibold text-gray-900 truncate">{activeStamp.label || activeStamp.id}</p>
|
||||
<p className="text-xs text-gray-500">Auto-applied to documents where applicable.</p>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -266,33 +266,33 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
|
||||
{!!stamps.length && (
|
||||
<div className="mt-2">
|
||||
<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) => {
|
||||
const src = toImgSrc(s);
|
||||
const activeCls = s.active
|
||||
? 'border-green-300 bg-green-50'
|
||||
: 'border-gray-200 hover:border-gray-300 transition-colors';
|
||||
? 'border-green-300 bg-green-50 shadow'
|
||||
: 'border-gray-200 hover:border-indigo-300 transition-colors';
|
||||
return (
|
||||
<li
|
||||
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">
|
||||
{s.base64 ? (
|
||||
<img
|
||||
src={src}
|
||||
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
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col">
|
||||
<span className="text-sm text-gray-900">{s.label || s.id}</span>
|
||||
{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
|
||||
</span>
|
||||
)}
|
||||
@ -302,21 +302,21 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
|
||||
{s.active ? (
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -333,14 +333,14 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
|
||||
<div className="fixed inset-0 z-50">
|
||||
<div className="absolute inset-0 bg-black/40" onClick={closeModal} />
|
||||
<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="p-5 border-b border-gray-100">
|
||||
<h3 className="text-base font-semibold text-gray-900">Add New Stamp</h3>
|
||||
<p className="mt-1 text-xs text-gray-600">
|
||||
<div className="w-full max-w-lg rounded-2xl bg-white shadow-2xl ring-1 ring-black/5">
|
||||
<div className="p-6 border-b border-gray-100">
|
||||
<h3 className="text-lg font-bold text-indigo-700">Add New Stamp</h3>
|
||||
<p className="mt-1 text-xs text-gray-500">
|
||||
Accepted types: PNG, JPEG, WebP, SVG. Max size: 5MB.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-5 space-y-4">
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-xs font-medium text-gray-700 mb-1">Label</label>
|
||||
<input
|
||||
@ -355,7 +355,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
|
||||
<div
|
||||
onDragOver={(e) => e.preventDefault()}
|
||||
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">
|
||||
{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"
|
||||
/>
|
||||
) : (
|
||||
<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
|
||||
</div>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<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">
|
||||
<label className="inline-block">
|
||||
<input
|
||||
@ -380,7 +380,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
|
||||
onChange={onBrowse}
|
||||
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>
|
||||
Choose file
|
||||
</span>
|
||||
@ -393,11 +393,11 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
|
||||
{modalError && <p className="text-xs text-red-600">{modalError}</p>}
|
||||
</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
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@ -405,7 +405,7 @@ export default function ContractUploadCompanyStamp({ onUploaded }: Props) {
|
||||
type="button"
|
||||
onClick={confirmUpload}
|
||||
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'}
|
||||
</button>
|
||||
|
||||
@ -8,15 +8,20 @@ import ContractTemplateList from './components/contractTemplateList';
|
||||
import useAuthStore from '../../store/authStore';
|
||||
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() {
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const router = useRouter();
|
||||
const [section, setSection] = useState('templates');
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
// Only allow admin
|
||||
const isAdmin =
|
||||
@ -30,7 +35,7 @@ export default function ContractManagementPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (mounted && !isAdmin) {
|
||||
router.replace('/'); // or show a 403 page
|
||||
router.replace('/');
|
||||
}
|
||||
}, [mounted, isAdmin, router]);
|
||||
|
||||
@ -41,33 +46,63 @@ export default function ContractManagementPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
{/* Force white background for this page */}
|
||||
<div className="bg-white min-h-screen">
|
||||
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-2xl font-bold text-gray-900">Contract Management</h1>
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Create contract templates in HTML, upload the company stamp, and manage existing contracts.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 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">
|
||||
{/* Sidebar Navigation */}
|
||||
<nav className="md:w-56 w-full flex md:flex-col flex-row gap-2 md:gap-4">
|
||||
{NAV.map((item) => (
|
||||
<button
|
||||
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>
|
||||
|
||||
{/* Reordered vertical layout */}
|
||||
<div className="space-y-6">
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Company Stamp</h2>
|
||||
<ContractUploadCompanyStamp onUploaded={bumpRefresh} />
|
||||
</div>
|
||||
{/* 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>
|
||||
</header>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Contracts</h2>
|
||||
<ContractTemplateList refreshKey={refreshKey} />
|
||||
</div>
|
||||
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-4 sm:p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold text-gray-900 mb-4">Create Template</h2>
|
||||
<ContractEditor onSaved={bumpRefresh} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Section Panels */}
|
||||
{section === 'stamp' && (
|
||||
<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"><circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/></svg>
|
||||
Company Stamp
|
||||
</h2>
|
||||
<ContractUploadCompanyStamp onUploaded={bumpRefresh} />
|
||||
</section>
|
||||
)}
|
||||
{section === 'templates' && (
|
||||
<section className="rounded-2xl bg-white shadow-lg border border-gray-100 p-6">
|
||||
<h2 className="text-xl font-semibold text-blue-900 mb-4 flex items-center gap-2">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor"><path d="M4 6h16M4 12h16M4 18h16"/></svg>
|
||||
Templates
|
||||
</h2>
|
||||
<ContractTemplateList refreshKey={refreshKey} />
|
||||
</section>
|
||||
)}
|
||||
{section === 'editor' && (
|
||||
<section className="rounded-2xl bg-white shadow-lg border border-gray-100 p-6">
|
||||
<h2 className="text-xl font-semibold text-blue-900 mb-4 flex items-center gap-2">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor"><path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 113 3L7 19l-4 1 1-4 12.5-12.5z"/></svg>
|
||||
Create Template
|
||||
</h2>
|
||||
<ContractEditor onSaved={bumpRefresh} />
|
||||
</section>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
|
||||
@ -266,35 +266,34 @@ export default function MatrixDetailPage() {
|
||||
{refreshing && (
|
||||
<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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Centered page container to avoid full-width stretch */}
|
||||
<div className="min-h-screen w-full bg-white">
|
||||
<div className="mx-auto max-w-6xl px-4 sm:px-6 py-6">
|
||||
{/* Modern header card with action */}
|
||||
<div className="mb-6 rounded-xl border border-gray-200 bg-white shadow-sm overflow-hidden">
|
||||
<div className="px-5 py-4 flex items-start justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen w-full">
|
||||
<div className="mx-auto max-w-6xl px-2 sm:px-6 py-8">
|
||||
{/* Header card */}
|
||||
<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="flex items-center justify-between">
|
||||
<div className="space-y-2">
|
||||
<button
|
||||
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" />
|
||||
Back to matrices
|
||||
</button>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">{matrixName}</h1>
|
||||
<p className="text-sm text-gray-600">
|
||||
Top node: <span className="font-medium text-gray-900">{topNodeEmail}</span>
|
||||
<h1 className="text-3xl font-extrabold text-blue-900">{matrixName}</h1>
|
||||
<p className="text-base text-blue-700">
|
||||
Top node: <span className="font-semibold text-blue-900">{topNodeEmail}</span>
|
||||
</p>
|
||||
<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
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
@ -302,90 +301,89 @@ export default function MatrixDetailPage() {
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
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" />
|
||||
Add users to matrix
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* banner for unlimited */}
|
||||
{/* Banner for unlimited */}
|
||||
{isUnlimited && (
|
||||
<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.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 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">
|
||||
{/* Sticky depth controls */}
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
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 ›
|
||||
</button>
|
||||
{/* NEW: include root toggle */}
|
||||
<label className="ml-3 inline-flex items-center gap-2 text-xs text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeRoot}
|
||||
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
|
||||
</label>
|
||||
<div className="ml-auto text-xs text-gray-600">
|
||||
Showing levels {depthA}–{depthB} of {isUnlimited ? 'Unlimited' : serverMaxDepth}
|
||||
</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>
|
||||
|
||||
{/* small stats */}
|
||||
<div className="mt-3 mb-4 grid grid-cols-1 sm:grid-cols-3 gap-3">
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div className="text-[11px] text-gray-500">Users (current slice)</div>
|
||||
<div className="text-base font-semibold text-gray-900">{usersInSlice.length}</div>
|
||||
{/* Small stats */}
|
||||
<div className="mb-8 grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
|
||||
<div className="text-xs text-gray-500 mb-1">Users (current slice)</div>
|
||||
<div className="text-xl font-semibold text-blue-900">{usersInSlice.length}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div className="text-[11px] text-gray-500">Total descendants</div>
|
||||
<div className="text-base font-semibold text-gray-900">{isUnlimited ? 'All descendants so far' : totalDescendants}</div>
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
|
||||
<div className="text-xs text-gray-500 mb-1">Total descendants</div>
|
||||
<div className="text-xl font-semibold text-blue-900">{isUnlimited ? 'All descendants so far' : totalDescendants}</div>
|
||||
</div>
|
||||
<div className="rounded-lg border border-gray-200 bg-white p-3">
|
||||
<div className="text-[11px] text-gray-500">Active levels loaded</div>
|
||||
<div className="text-base font-semibold text-gray-900">{depthA}–{depthB}</div>
|
||||
<div className="rounded-xl border border-gray-100 bg-white p-5 shadow">
|
||||
<div className="text-xs text-gray-500 mb-1">Active levels loaded</div>
|
||||
<div className="text-xl font-semibold text-blue-900">{depthA}–{depthB}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* dynamic levels */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||
{/* Dynamic levels */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||
{Array
|
||||
.from(byLevel.keys())
|
||||
.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)
|
||||
.map(l => (
|
||||
<LevelSection key={l} level={l} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* jump to level */}
|
||||
<div className="mt-4 mb-8">
|
||||
{/* Jump to level */}
|
||||
<div className="mb-8">
|
||||
<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
|
||||
setDepthA(lv); setDepthB(Math.max(lv, lv + 5))
|
||||
}}>
|
||||
@ -394,10 +392,10 @@ export default function MatrixDetailPage() {
|
||||
</div>
|
||||
|
||||
{/* Collapsible All users list by level */}
|
||||
<div className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-gray-100">
|
||||
<h2 className="text-base font-semibold text-gray-900">All users in this matrix</h2>
|
||||
<p className="text-xs text-gray-600">Grouped by levels (power of five structure).</p>
|
||||
<div className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden mb-8">
|
||||
<div className="px-8 py-6 border-b border-gray-100">
|
||||
<h2 className="text-xl font-semibold text-blue-900">All users in this matrix</h2>
|
||||
<p className="text-xs text-blue-700">Grouped by levels (power of five structure).</p>
|
||||
</div>
|
||||
<div className="divide-y divide-gray-100">
|
||||
{[0,1,2,3,4,5].map(lvl => {
|
||||
@ -414,15 +412,14 @@ export default function MatrixDetailPage() {
|
||||
return globalMatch && levelMatch
|
||||
})
|
||||
return (
|
||||
<div key={lvl} className="px-5 py-4">
|
||||
<div key={lvl} className="px-8 py-6">
|
||||
<button
|
||||
className="flex items-center justify-between w-full text-left"
|
||||
onClick={() => {
|
||||
console.log('[MatrixDetailPage] toggle level', { level: lvl, to: !collapsedLevels[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)
|
||||
</span>
|
||||
<span className="ml-2">
|
||||
@ -438,34 +435,31 @@ export default function MatrixDetailPage() {
|
||||
<>
|
||||
<div className="flex justify-end mt-2 mb-3">
|
||||
<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
|
||||
type="text"
|
||||
value={levelSearch[lvl]}
|
||||
onChange={e => {
|
||||
console.log('[MatrixDetailPage] levelSearch', { level: lvl, q: e.target.value })
|
||||
setLevelSearch(prev => ({ ...prev, [lvl]: e.target.value }))
|
||||
}}
|
||||
onChange={e => setLevelSearch(prev => ({ ...prev, [lvl]: e.target.value }))}
|
||||
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 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 && (
|
||||
<div className="col-span-full text-xs text-gray-500 italic">No users found.</div>
|
||||
)}
|
||||
{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="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 ${
|
||||
u.type === 'company' ? 'bg-indigo-100 text-indigo-800' : 'bg-blue-100 text-blue-800'
|
||||
}`}>
|
||||
{u.type === 'company' ? 'Company' : 'Personal'}
|
||||
</span>
|
||||
</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>
|
||||
|
||||
@ -252,22 +252,24 @@ export default function MatrixManagementPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-screen w-full px-4 sm:px-6 py-8 bg-white">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<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 px-2 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header + Create */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-gray-900">Matrix Management</h1>
|
||||
<p className="text-sm text-gray-600 mt-1">Manage matrices, see stats, and create new ones.</p>
|
||||
<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>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Matrix Management</h1>
|
||||
<p className="text-lg text-blue-700 mt-2">Manage matrices, see stats, and create new ones.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateOpen(true)}
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
Create Matrix
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setCreateOpen(true)}
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 shadow-sm"
|
||||
>
|
||||
<PlusIcon className="h-5 w-5" />
|
||||
Create New Matrix
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Error banner for stats */}
|
||||
{statsError && (
|
||||
@ -277,17 +279,17 @@ export default function MatrixManagementPage() {
|
||||
)}
|
||||
|
||||
{/* 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={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" />
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
<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="unlimited">Unlimited</option>
|
||||
<option value="five">5-level</option>
|
||||
@ -295,13 +297,13 @@ export default function MatrixManagementPage() {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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="asc">Asc</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500" title="Users count respects each matrix’s max depth policy.">
|
||||
ℹ️ Tooltip: Users count respects each matrix’s max depth policy.
|
||||
ℹ️ Users count respects each matrix’s max depth policy.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -313,12 +315,11 @@ export default function MatrixManagementPage() {
|
||||
)}
|
||||
|
||||
{/* 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 ? (
|
||||
// Simple skeleton
|
||||
Array.from({ length: 3 }).map((_, i) => (
|
||||
<div key={i} className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="p-5 animate-pulse space-y-4">
|
||||
<div key={i} className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden">
|
||||
<div className="p-6 animate-pulse space-y-4">
|
||||
<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-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>
|
||||
) : (
|
||||
matricesView.map(m => (
|
||||
<article key={m.id} className="rounded-xl bg-white border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="p-5">
|
||||
<article key={m.id} className="rounded-2xl bg-white border border-gray-100 shadow-lg overflow-hidden flex flex-col">
|
||||
<div className="p-6 flex-1 flex flex-col">
|
||||
<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} />
|
||||
</div>
|
||||
<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}
|
||||
</span>
|
||||
</div>
|
||||
@ -361,18 +362,16 @@ export default function MatrixManagementPage() {
|
||||
<div className="mt-5 flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => toggleStatus(m.id)}
|
||||
className={`rounded-md px-3 py-2 text-sm font-medium border ${
|
||||
m.status === 'active'
|
||||
className={`rounded-lg px-4 py-2 text-sm font-medium border shadow transition
|
||||
${m.status === 'active'
|
||||
? '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'}
|
||||
</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={() => {
|
||||
// get default depth range from localStorage per matrix
|
||||
const defA = Number(localStorage.getItem(`matrixDepthA:${m.id}`) ?? 0)
|
||||
const defB = Number(localStorage.getItem(`matrixDepthB:${m.id}`) ?? 5)
|
||||
const params = new URLSearchParams({
|
||||
@ -389,23 +388,22 @@ export default function MatrixManagementPage() {
|
||||
View details →
|
||||
</button>
|
||||
</div>
|
||||
{/* Quick default A–B editor */}
|
||||
<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
|
||||
type="number"
|
||||
min={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)))}
|
||||
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
|
||||
type="number"
|
||||
min={0}
|
||||
defaultValue={Number(localStorage.getItem(`matrixDepthB:${m.id}`) ?? 5)}
|
||||
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>
|
||||
@ -420,9 +418,9 @@ export default function MatrixManagementPage() {
|
||||
<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 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="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
||||
<h4 className="text-base font-semibold text-gray-900">Create New Matrix</h4>
|
||||
<div className="w-full max-w-md rounded-2xl bg-white shadow-2xl ring-1 ring-black/10">
|
||||
<div className="px-6 py-5 border-b border-gray-100 flex items-center justify-between">
|
||||
<h4 className="text-lg font-semibold text-blue-900">Create Matrix</h4>
|
||||
<button
|
||||
onClick={() => { setCreateOpen(false); resetForm() }}
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
@ -430,7 +428,7 @@ export default function MatrixManagementPage() {
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={handleCreate} className="p-5 space-y-4">
|
||||
<form onSubmit={handleCreate} className="p-6 space-y-5">
|
||||
{/* Success banner */}
|
||||
{createSuccess && (
|
||||
<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"
|
||||
onClick={confirmForce}
|
||||
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)
|
||||
</button>
|
||||
@ -459,7 +457,7 @@ export default function MatrixManagementPage() {
|
||||
type="button"
|
||||
onClick={() => setForcePrompt(null)}
|
||||
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
|
||||
</button>
|
||||
@ -469,24 +467,24 @@ export default function MatrixManagementPage() {
|
||||
|
||||
{/* Form fields */}
|
||||
<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
|
||||
type="text"
|
||||
value={createName}
|
||||
onChange={e => setCreateName(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</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
|
||||
type="email"
|
||||
value={createEmail}
|
||||
onChange={e => setCreateEmail(e.target.value)}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@ -497,19 +495,19 @@ export default function MatrixManagementPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pt-2 flex items-center justify-end gap-2">
|
||||
<div className="pt-2 flex items-center justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setCreateOpen(false); resetForm() }}
|
||||
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
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
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 ? 'Creating...' : 'Create Matrix'}
|
||||
|
||||
@ -51,21 +51,21 @@ export default function AdminDashboardPage() {
|
||||
if (!isClient) {
|
||||
return (
|
||||
<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="h-12 w-12 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" />
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
<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-blue-900">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Access check (only after client-side hydration)
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<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="text-center">
|
||||
<h1 className="text-2xl font-bold text-red-600 mb-2">Access Denied</h1>
|
||||
@ -79,19 +79,21 @@ export default function AdminDashboardPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Heading */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
|
||||
<p className="text-sm sm:text-base text-gray-600 mt-2">
|
||||
Manage all administrative features, user management, permissions, and global settings.
|
||||
</p>
|
||||
</div>
|
||||
<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-2 sm:px-6 lg:px-8 py-8">
|
||||
{/* 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>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 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">
|
||||
<ExclamationTriangleIcon className="h-5 w-5 flex-shrink-0 text-red-500 mt-0.5" />
|
||||
<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-6 w-6 flex-shrink-0 text-red-500 mt-0.5" />
|
||||
<div className="leading-relaxed">
|
||||
<p className="font-semibold mb-0.5">
|
||||
Warning: Settings and actions below this point can have consequences for the entire system!
|
||||
@ -102,123 +104,151 @@ export default function AdminDashboardPage() {
|
||||
</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="rounded-xl border border-gray-200 bg-[#f5f9ff] p-6 shadow-sm hover:shadow-md transition">
|
||||
<div className="flex items-start gap-3 mb-5">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
||||
<Squares2X2Icon className="h-6 w-6 text-blue-600" />
|
||||
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-blue-100">
|
||||
<Squares2X2Icon className="h-7 w-7 text-blue-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-[#0d315f]">Management Shortcuts</h2>
|
||||
<p className="text-xs text-[#52739a] mt-0.5">
|
||||
<h2 className="text-lg font-semibold text-blue-900">Management Shortcuts</h2>
|
||||
<p className="text-sm text-blue-700 mt-0.5">
|
||||
Quick access to common admin modules.
|
||||
</p>
|
||||
</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
|
||||
type="button"
|
||||
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">
|
||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-blue-50 border border-blue-100">
|
||||
<Squares2X2Icon className="h-5 w-5 text-blue-600" />
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-blue-100 border border-blue-200">
|
||||
<Squares2X2Icon className="h-6 w-6 text-blue-600" />
|
||||
</span>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-semibold text-[#0d315f]">Matrix Management</div>
|
||||
<div className="text-[11px] text-[#52739a]">Configure matrices and users</div>
|
||||
<div className="text-base font-semibold text-blue-900">Matrix Management</div>
|
||||
<div className="text-xs text-blue-700">Configure matrices and users</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
|
||||
type="button"
|
||||
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">
|
||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-amber-50 border border-amber-100">
|
||||
<BanknotesIcon className="h-5 w-5 text-amber-600" />
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-amber-100 border border-amber-200">
|
||||
<BanknotesIcon className="h-6 w-6 text-amber-600" />
|
||||
</span>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-semibold text-[#6b4e16]">Coffee Subscription Management</div>
|
||||
<div className="text-[11px] text-[#8a6c2b]">Plans, billing and renewals</div>
|
||||
<div className="text-base font-semibold text-amber-900">Coffee Subscription Management</div>
|
||||
<div className="text-xs text-amber-700">Plans, billing and renewals</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
|
||||
type="button"
|
||||
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">
|
||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-indigo-50 border border-indigo-100">
|
||||
<ClipboardDocumentListIcon className="h-5 w-5 text-indigo-600" />
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-indigo-100 border border-indigo-200">
|
||||
<ClipboardDocumentListIcon className="h-6 w-6 text-indigo-600" />
|
||||
</span>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-semibold text-[#1f2a5a]">Contract Management</div>
|
||||
<div className="text-[11px] text-[#4b5a9a]">Templates, approvals, status</div>
|
||||
<div className="text-base font-semibold text-indigo-900">Contract Management</div>
|
||||
<div className="text-xs text-indigo-700">Templates, approvals, status</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
|
||||
type="button"
|
||||
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">
|
||||
<span className="inline-flex h-9 w-9 items-center justify-center rounded-md bg-gray-50 border border-gray-100">
|
||||
<UsersIcon className="h-5 w-5 text-blue-600" />
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="inline-flex h-10 w-10 items-center justify-center rounded-md bg-blue-100 border border-blue-200">
|
||||
<UsersIcon className="h-6 w-6 text-blue-600" />
|
||||
</span>
|
||||
<div className="text-left">
|
||||
<div className="text-sm font-semibold text-[#0d315f]">User Management</div>
|
||||
<div className="text-[11px] text-[#52739a]">Browse, search, and manage all users</div>
|
||||
<div className="text-base font-semibold text-blue-900">User Management</div>
|
||||
<div className="text-xs text-blue-700">Browse, search, and manage all users</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Server Status & Logs */}
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm hover:shadow-md transition">
|
||||
<div className="flex items-start gap-3 mb-6">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-gray-100">
|
||||
<ServerStackIcon className="h-6 w-6 text-gray-700" />
|
||||
<div className="rounded-2xl border border-gray-100 bg-white p-8 shadow-lg hover:shadow-xl transition">
|
||||
<div className="flex items-start gap-4 mb-6">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-xl bg-gray-100">
|
||||
<ServerStackIcon className="h-7 w-7 text-gray-700" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-sm font-semibold text-gray-900">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
Server Status & Logs
|
||||
</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.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
<div className="grid gap-8 lg:grid-cols-3">
|
||||
{/* Metrics */}
|
||||
<div className="space-y-4">
|
||||
<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'}`} />
|
||||
<p className="text-sm">
|
||||
<p className="text-base">
|
||||
<span className="font-semibold">Server Status:</span>{' '}
|
||||
<span className={serverStats.status === 'Online' ? 'text-emerald-600 font-medium' : 'text-red-600 font-medium'}>
|
||||
{serverStats.status === 'Online' ? 'Server Online' : 'Offline'}
|
||||
</span>
|
||||
</p>
|
||||
</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">CPU Usage:</span> {serverStats.cpu}</p>
|
||||
<p><span className="font-medium text-gray-700">Memory Usage:</span> {serverStats.memory} GB</p>
|
||||
</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" />
|
||||
<span>Autoscaled environment (mock)</span>
|
||||
</div>
|
||||
@ -229,11 +259,11 @@ export default function AdminDashboardPage() {
|
||||
|
||||
{/* Logs */}
|
||||
<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
|
||||
</h3>
|
||||
{serverStats.recentErrors.length === 0 && (
|
||||
<p className="text-xs text-gray-500 italic">
|
||||
<p className="text-sm text-gray-500 italic">
|
||||
No recent logs.
|
||||
</p>
|
||||
)}
|
||||
@ -242,12 +272,12 @@ export default function AdminDashboardPage() {
|
||||
<div className="mt-6">
|
||||
<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
|
||||
onClick={() => {}}
|
||||
>
|
||||
View Full Logs
|
||||
<ArrowRightIcon className="h-4 w-4" />
|
||||
<ArrowRightIcon className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -55,51 +55,61 @@ export default function CreateSubscriptionPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Create Subscription</h1>
|
||||
<p className="text-sm sm:text-base text-gray-700 mt-2">Add a new product or subscription plan.</p>
|
||||
<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-2 sm:px-6 lg:px-8 py-8">
|
||||
{/* 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>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Create Subscription</h1>
|
||||
<p className="text-lg text-blue-700 mt-2">Add a new product or subscription plan.</p>
|
||||
</div>
|
||||
<Link href="/admin/subscriptions"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M6 18L18 6M6 6l12 12"/></svg>
|
||||
Back to list
|
||||
</Link>
|
||||
</div>
|
||||
<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">
|
||||
Back to list
|
||||
</Link>
|
||||
</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">
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||
{/* Title */}
|
||||
<div>
|
||||
<label htmlFor="title" className="block text-sm font-medium text-gray-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)} />
|
||||
<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-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>
|
||||
{/* Quantity */}
|
||||
<div>
|
||||
<label htmlFor="quantity" className="block text-sm font-medium text-gray-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))} />
|
||||
<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-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>
|
||||
|
||||
{/* Price */}
|
||||
<div>
|
||||
<label htmlFor="price" className="block text-sm font-medium text-gray-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))} />
|
||||
<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-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>
|
||||
{/* Currency */}
|
||||
<div>
|
||||
<label htmlFor="currency" className="block text-sm font-medium text-gray-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))} />
|
||||
<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-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>
|
||||
|
||||
{/* Tax Rate */}
|
||||
<div>
|
||||
<label htmlFor="tax_rate" className="block text-sm font-medium text-gray-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))} />
|
||||
<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-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>
|
||||
{/* Featured */}
|
||||
<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)} />
|
||||
<label htmlFor="featured" className="text-sm font-medium text-gray-900">Featured</label>
|
||||
<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-blue-900">Featured</label>
|
||||
</div>
|
||||
|
||||
{/* Billing Interval */}
|
||||
<div>
|
||||
<label htmlFor="billing_interval" className="block text-sm font-medium text-gray-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 => {
|
||||
<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-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;
|
||||
setBillingInterval(v);
|
||||
if (v) {
|
||||
@ -115,57 +125,77 @@ export default function CreateSubscriptionPage() {
|
||||
<option value="year">Year</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Interval Count */}
|
||||
<div>
|
||||
<label htmlFor="interval_count" className="block text-sm font-medium text-gray-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} />
|
||||
<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-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>
|
||||
|
||||
{/* SKU */}
|
||||
<div>
|
||||
<label htmlFor="sku" className="block text-sm font-medium text-gray-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)} />
|
||||
<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-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>
|
||||
{/* Slug */}
|
||||
<div>
|
||||
<label htmlFor="slug" className="block text-sm font-medium text-gray-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)} />
|
||||
<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-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>
|
||||
|
||||
{/* Availability */}
|
||||
<div>
|
||||
<label htmlFor="availability" className="block text-sm font-medium text-gray-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)}>
|
||||
<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-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="unavailable">Unavailable</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-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)} />
|
||||
<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-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>
|
||||
</div>
|
||||
|
||||
{/* Picture Upload */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-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 className="text-center">
|
||||
<PhotoIcon aria-hidden="true" className="mx-auto h-12 w-12 text-gray-500" />
|
||||
<div className="mt-4 flex text-sm text-gray-700">
|
||||
<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">
|
||||
<span>Upload a file</span>
|
||||
<input id="file-upload" name="file-upload" type="file" accept="image/*" className="sr-only" onChange={e => setPictureFile(e.target.files?.[0])} />
|
||||
</label>
|
||||
<p className="pl-1">or drag and drop</p>
|
||||
<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-blue-300 px-6 py-10 bg-blue-50 cursor-pointer"
|
||||
onClick={() => document.getElementById('file-upload')?.click()}
|
||||
onDragOver={e => e.preventDefault()}
|
||||
onDrop={e => {
|
||||
e.preventDefault();
|
||||
if (e.dataTransfer.files?.[0]) setPictureFile(e.dataTransfer.files[0]);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
<input
|
||||
id="file-upload"
|
||||
name="file-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={e => setPictureFile(e.target.files?.[0])}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-x-3">
|
||||
<Link href="/admin/subscriptions" className="text-sm font-medium text-gray-700 hover:text-gray-900">
|
||||
{/* Actions */}
|
||||
<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
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -37,34 +37,41 @@ export default function AdminSubscriptionsPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Subscription Products</h1>
|
||||
<p className="text-sm sm:text-base text-gray-700 mt-2">Manage all products and subscription plans.</p>
|
||||
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
|
||||
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8 py-8">
|
||||
{/* 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>
|
||||
<h1 className="text-4xl font-extrabold text-blue-900 tracking-tight">Subscription Products</h1>
|
||||
<p className="text-lg text-blue-700 mt-2">Manage all products and subscription plans.</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/subscriptions/createSubscription"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-blue-900 hover:bg-blue-800 text-blue-50 px-5 py-3 text-base font-semibold shadow transition"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor"><path d="M7 1a1 1 0 0 1 2 0v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V8H2a1 1 0 1 1 0-2h5V1z"/></svg>
|
||||
Create Subscription
|
||||
</Link>
|
||||
</div>
|
||||
<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">
|
||||
Create Subscription
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{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="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 && (
|
||||
<div className="col-span-full text-sm text-gray-700">Loading…</div>
|
||||
)}
|
||||
{!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">
|
||||
<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)}
|
||||
</div>
|
||||
{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>
|
||||
<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}
|
||||
</dl>
|
||||
<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'}
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!loading && !items.length && (
|
||||
<div className="col-span-full py-8 text-center text-sm text-gray-500">No subscriptions found.</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
);
|
||||
|
||||
@ -138,10 +138,10 @@ export default function AdminUserManagementPage() {
|
||||
if (!isClient) {
|
||||
return (
|
||||
<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="h-12 w-12 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" />
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
<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-blue-900">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
@ -152,7 +152,7 @@ export default function AdminUserManagementPage() {
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<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="text-center">
|
||||
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
||||
@ -251,48 +251,50 @@ export default function AdminUserManagementPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Title */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">User Management</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Manage all users, view statistics, and handle verification.
|
||||
</p>
|
||||
</div>
|
||||
<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-2 sm:px-6 lg:px-8 py-8">
|
||||
{/* 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>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Statistic Section + Verify Button */}
|
||||
<div className="mb-8 flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4 flex-1">
|
||||
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center">
|
||||
<div className="text-[11px] text-gray-500">Total Users</div>
|
||||
<div className="text-base font-semibold text-gray-900">{stats.total}</div>
|
||||
<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-6 flex-1">
|
||||
<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">{stats.total}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center">
|
||||
<div className="text-[11px] text-gray-500">Admins</div>
|
||||
<div className="text-base font-semibold text-indigo-700">{stats.admins}</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">{stats.admins}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center">
|
||||
<div className="text-[11px] text-gray-500">Personal</div>
|
||||
<div className="text-base font-semibold text-blue-700">{stats.personal}</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">{stats.personal}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center">
|
||||
<div className="text-[11px] text-gray-500">Company</div>
|
||||
<div className="text-base font-semibold text-purple-700">{stats.company}</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">{stats.company}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center">
|
||||
<div className="text-[11px] text-gray-500">Active</div>
|
||||
<div className="text-base font-semibold text-green-700">{stats.active}</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">{stats.active}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-white border border-gray-200 p-3 text-center">
|
||||
<div className="text-[11px] text-gray-500">Pending</div>
|
||||
<div className="text-base font-semibold text-amber-700">{stats.pending}</div>
|
||||
<div className="rounded-xl bg-white border border-gray-100 p-5 text-center shadow">
|
||||
<div className="text-xs text-gray-500">Pending</div>
|
||||
<div className="text-xl font-semibold text-amber-700">{stats.pending}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<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'}
|
||||
>
|
||||
Go to User Verification
|
||||
@ -302,7 +304,7 @@ export default function AdminUserManagementPage() {
|
||||
|
||||
{/* Error Message */}
|
||||
{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" />
|
||||
<div>
|
||||
<p className="font-semibold">Error loading users</p>
|
||||
@ -320,22 +322,22 @@ export default function AdminUserManagementPage() {
|
||||
{/* Filter Card */}
|
||||
<form
|
||||
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
|
||||
</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 */}
|
||||
<div className="md:col-span-2">
|
||||
<label className="sr-only">Search</label>
|
||||
<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
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
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>
|
||||
@ -344,7 +346,7 @@ export default function AdminUserManagementPage() {
|
||||
<select
|
||||
value={fType}
|
||||
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="personal">Personal</option>
|
||||
@ -356,7 +358,7 @@ export default function AdminUserManagementPage() {
|
||||
<select
|
||||
value={fStatus}
|
||||
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>
|
||||
{STATUSES.map(s => <option key={s} value={s}>{s[0].toUpperCase()+s.slice(1)}</option>)}
|
||||
@ -367,26 +369,25 @@ export default function AdminUserManagementPage() {
|
||||
<select
|
||||
value={fRole}
|
||||
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>
|
||||
{ROLES.map(r => <option key={r} value={r}>{r[0].toUpperCase()+r.slice(1)}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2">
|
||||
{/* NEW: Export CSV (exports all filtered results) */}
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
Export all users as CSV
|
||||
</button>
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -394,9 +395,9 @@ export default function AdminUserManagementPage() {
|
||||
</form>
|
||||
|
||||
{/* Users Table */}
|
||||
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
||||
<div className="text-sm font-semibold text-[#0f2c55]">
|
||||
<div className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 overflow-hidden mb-8">
|
||||
<div className="px-8 py-6 border-b border-gray-100 flex items-center justify-between">
|
||||
<div className="text-lg font-semibold text-blue-900">
|
||||
All Users
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
@ -405,15 +406,15 @@ export default function AdminUserManagementPage() {
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<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>
|
||||
<th className="px-4 py-2 text-left">User</th>
|
||||
<th className="px-4 py-2 text-left">Type</th>
|
||||
<th className="px-4 py-2 text-left">Status</th>
|
||||
<th className="px-4 py-2 text-left">Role</th>
|
||||
<th className="px-4 py-2 text-left">Created</th>
|
||||
<th className="px-4 py-2 text-left">Last Login</th>
|
||||
<th className="px-4 py-2 text-left">Actions</th>
|
||||
<th className="px-4 py-3 text-left">User</th>
|
||||
<th className="px-4 py-3 text-left">Type</th>
|
||||
<th className="px-4 py-3 text-left">Status</th>
|
||||
<th className="px-4 py-3 text-left">Role</th>
|
||||
<th className="px-4 py-3 text-left">Created</th>
|
||||
<th className="px-4 py-3 text-left">Last Login</th>
|
||||
<th className="px-4 py-3 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
@ -421,8 +422,8 @@ export default function AdminUserManagementPage() {
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-10 text-center">
|
||||
<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" />
|
||||
<span className="text-sm text-gray-500">Loading users...</span>
|
||||
<div className="h-4 w-4 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
|
||||
<span className="text-sm text-blue-900">Loading users...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -447,34 +448,34 @@ export default function AdminUserManagementPage() {
|
||||
const lastLoginDate = u.last_login_at ? new Date(u.last_login_at).toLocaleDateString() : 'Never'
|
||||
|
||||
return (
|
||||
<tr key={u.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
<tr key={u.id} className="hover:bg-blue-50">
|
||||
<td className="px-4 py-4">
|
||||
<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}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 leading-tight">
|
||||
<div className="font-medium text-blue-900 leading-tight">
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="text-[11px] text-gray-500">
|
||||
<div className="text-[11px] text-blue-700">
|
||||
{u.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">{typeBadge(u.user_type)}</td>
|
||||
<td className="px-4 py-3">{statusBadge(userStatus)}</td>
|
||||
<td className="px-4 py-3">{roleBadge(u.role)}</td>
|
||||
<td className="px-4 py-3 text-gray-700">{createdDate}</td>
|
||||
<td className="px-4 py-3 text-gray-500 italic">
|
||||
<td className="px-4 py-4">{typeBadge(u.user_type)}</td>
|
||||
<td className="px-4 py-4">{statusBadge(userStatus)}</td>
|
||||
<td className="px-4 py-4">{roleBadge(u.role)}</td>
|
||||
<td className="px-4 py-4 text-blue-900">{createdDate}</td>
|
||||
<td className="px-4 py-4 text-blue-700 italic">
|
||||
{lastLoginDate}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -485,7 +486,7 @@ export default function AdminUserManagementPage() {
|
||||
})}
|
||||
{current.length === 0 && (
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
@ -494,22 +495,22 @@ export default function AdminUserManagementPage() {
|
||||
</table>
|
||||
</div>
|
||||
{/* 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="text-xs text-gray-600">
|
||||
<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-blue-700">
|
||||
Page {page} of {totalPages} ({filtered.length} total users)
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
disabled={page===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
|
||||
</button>
|
||||
<button
|
||||
disabled={page===totalPages}
|
||||
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 ›
|
||||
</button>
|
||||
|
||||
@ -112,21 +112,21 @@ export default function AdminUserVerifyPage() {
|
||||
if (!isClient) {
|
||||
return (
|
||||
<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="h-12 w-12 rounded-full border-2 border-blue-500 border-b-transparent animate-spin mx-auto mb-4" />
|
||||
<p className="text-gray-600">Loading...</p>
|
||||
<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-blue-900">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// Access check (only after client-side hydration)
|
||||
if (!isAdmin) {
|
||||
return (
|
||||
<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="text-center">
|
||||
<ExclamationTriangleIcon className="mx-auto h-12 w-12 text-red-500 mb-4" />
|
||||
@ -141,19 +141,21 @@ export default function AdminUserVerifyPage() {
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Title (unified) */}
|
||||
<div className="mb-8 text-left">
|
||||
<h1 className="text-3xl font-bold text-gray-900">User Verification Center</h1>
|
||||
<p className="mt-2 text-sm sm:text-base text-gray-600">
|
||||
Review and verify all users who need admin approval. Users must complete all steps before verification.
|
||||
</p>
|
||||
</div>
|
||||
<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-2 sm:px-6 lg:px-8 py-8">
|
||||
{/* 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>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Error Message */}
|
||||
{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" />
|
||||
<div>
|
||||
<p className="font-semibold">Error loading data</p>
|
||||
@ -171,21 +173,21 @@ export default function AdminUserVerifyPage() {
|
||||
{/* Filter Card */}
|
||||
<form
|
||||
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
|
||||
</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">
|
||||
<label className="sr-only">Search</label>
|
||||
<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
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
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>
|
||||
@ -193,7 +195,7 @@ export default function AdminUserVerifyPage() {
|
||||
<select
|
||||
value={fType}
|
||||
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="personal">Personal</option>
|
||||
@ -204,7 +206,7 @@ export default function AdminUserVerifyPage() {
|
||||
<select
|
||||
value={fRole}
|
||||
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="user">User</option>
|
||||
@ -215,7 +217,7 @@ export default function AdminUserVerifyPage() {
|
||||
<select
|
||||
value={perPage}
|
||||
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>)}
|
||||
</select>
|
||||
@ -223,7 +225,7 @@ export default function AdminUserVerifyPage() {
|
||||
<div className="flex items-stretch">
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
@ -232,9 +234,9 @@ export default function AdminUserVerifyPage() {
|
||||
</form>
|
||||
|
||||
{/* Pending Users Table */}
|
||||
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 overflow-hidden">
|
||||
<div className="px-5 py-4 border-b border-gray-100 flex items-center justify-between">
|
||||
<div className="text-sm font-semibold text-[#0f2c55]">
|
||||
<div className="bg-white rounded-2xl shadow-lg ring-1 ring-gray-100 overflow-hidden mb-8">
|
||||
<div className="px-8 py-6 border-b border-gray-100 flex items-center justify-between">
|
||||
<div className="text-lg font-semibold text-blue-900">
|
||||
Users Pending Verification
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
@ -243,15 +245,15 @@ export default function AdminUserVerifyPage() {
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<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>
|
||||
<th className="px-4 py-2 text-left">User</th>
|
||||
<th className="px-4 py-2 text-left">Type</th>
|
||||
<th className="px-4 py-2 text-left">Progress</th>
|
||||
<th className="px-4 py-2 text-left">Status</th>
|
||||
<th className="px-4 py-2 text-left">Role</th>
|
||||
<th className="px-4 py-2 text-left">Created</th>
|
||||
<th className="px-4 py-2 text-left">Actions</th>
|
||||
<th className="px-4 py-3 text-left">User</th>
|
||||
<th className="px-4 py-3 text-left">Type</th>
|
||||
<th className="px-4 py-3 text-left">Progress</th>
|
||||
<th className="px-4 py-3 text-left">Status</th>
|
||||
<th className="px-4 py-3 text-left">Role</th>
|
||||
<th className="px-4 py-3 text-left">Created</th>
|
||||
<th className="px-4 py-3 text-left">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100">
|
||||
@ -259,8 +261,8 @@ export default function AdminUserVerifyPage() {
|
||||
<tr>
|
||||
<td colSpan={7} className="px-4 py-10 text-center">
|
||||
<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" />
|
||||
<span className="text-sm text-gray-500">Loading users...</span>
|
||||
<div className="h-4 w-4 rounded-full border-2 border-blue-900 border-b-transparent animate-spin" />
|
||||
<span className="text-sm text-blue-900">Loading users...</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -281,33 +283,33 @@ export default function AdminUserVerifyPage() {
|
||||
u.documents_uploaded === 1 && u.contract_signed === 1
|
||||
|
||||
return (
|
||||
<tr key={u.id} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-3">
|
||||
<tr key={u.id} className="hover:bg-blue-50">
|
||||
<td className="px-4 py-4">
|
||||
<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}
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-gray-900 leading-tight">
|
||||
<div className="font-medium text-blue-900 leading-tight">
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="text-[11px] text-gray-500">{u.email}</div>
|
||||
<div className="text-[11px] text-blue-700">{u.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">{typeBadge(u.user_type)}</td>
|
||||
<td className="px-4 py-3">{verificationStatusBadge(u)}</td>
|
||||
<td className="px-4 py-3">{statusBadge(u.status)}</td>
|
||||
<td className="px-4 py-3">{roleBadge(u.role)}</td>
|
||||
<td className="px-4 py-3 text-gray-700">{createdDate}</td>
|
||||
<td className="px-4 py-3">
|
||||
<td className="px-4 py-4">{typeBadge(u.user_type)}</td>
|
||||
<td className="px-4 py-4">{verificationStatusBadge(u)}</td>
|
||||
<td className="px-4 py-4">{statusBadge(u.status)}</td>
|
||||
<td className="px-4 py-4">{roleBadge(u.role)}</td>
|
||||
<td className="px-4 py-4 text-blue-900">{createdDate}</td>
|
||||
<td className="px-4 py-4">
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedUserId(u.id.toString())
|
||||
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
|
||||
</button>
|
||||
@ -316,7 +318,7 @@ export default function AdminUserVerifyPage() {
|
||||
<button
|
||||
onClick={() => handleVerifyUser(u.id.toString())}
|
||||
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
|
||||
? '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'
|
||||
@ -343,7 +345,7 @@ export default function AdminUserVerifyPage() {
|
||||
})}
|
||||
{current.length === 0 && !loading && (
|
||||
<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.
|
||||
</td>
|
||||
</tr>
|
||||
@ -352,22 +354,22 @@ export default function AdminUserVerifyPage() {
|
||||
</table>
|
||||
</div>
|
||||
{/* 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="text-xs text-gray-600">
|
||||
<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-blue-700">
|
||||
Page {page} of {totalPages} ({filtered.length} pending users)
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
disabled={page === 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
|
||||
</button>
|
||||
<button
|
||||
disabled={page === totalPages}
|
||||
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 ›
|
||||
</button>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user