beautify: admin redesign

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

View File

@ -132,29 +132,29 @@ export default function ContractEditor({ onSaved }: Props) {
};
return (
<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>

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 matrixs max depth policy.">
Tooltip: Users count respects each matrixs max depth policy.
Users count respects each matrixs 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 AB 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'}

View File

@ -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>

View File

@ -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>

View File

@ -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>
);

View File

@ -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>

View File

@ -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>