last cleanup
This commit is contained in:
@@ -35,6 +35,7 @@ This file is the running release and change log for CODEXIUM. Keep it updated wh
|
|||||||
- Continued density standardization across warehouse list/detail/editor screens and the manufacturing station surface, including tighter status blocks, denser location/station cards, and removal of older roomy header patterns
|
- Continued density standardization across warehouse list/detail/editor screens and the manufacturing station surface, including tighter status blocks, denser location/station cards, and removal of older roomy header patterns
|
||||||
- Continued density standardization across company settings and deeper manufacturing detail surfaces, including tighter admin/profile/theme sections, denser work-order execution panels, and compact issue/completion history cards
|
- Continued density standardization across company settings and deeper manufacturing detail surfaces, including tighter admin/profile/theme sections, denser work-order execution panels, and compact issue/completion history cards
|
||||||
- Continued density standardization across project cockpit/detail internals, including tighter cockpit cards, denser purchasing and readiness panels, and compact milestone, manufacturing-link, and activity-timeline surfaces
|
- Continued density standardization across project cockpit/detail internals, including tighter cockpit cards, denser purchasing and readiness panels, and compact milestone, manufacturing-link, and activity-timeline surfaces
|
||||||
|
- Continued density standardization across admin diagnostics, user management, and CRM contacts, including tighter filter/forms, denser summary cards, and compact contact/account management surfaces
|
||||||
- Project-side milestone and work-order rollups surfaced on project list and detail pages
|
- Project-side milestone and work-order rollups surfaced on project list and detail pages
|
||||||
- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form
|
- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form
|
||||||
- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support
|
- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support
|
||||||
|
|||||||
@@ -58,12 +58,11 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="min-w-0 rounded-[20px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
|
<article className="surface-panel min-w-0">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contacts</p>
|
<p className="section-kicker">CONTACTS</p>
|
||||||
<h4 className="mt-2 text-lg font-bold text-text">People on this account</h4>
|
<div className="mt-3 space-y-2">
|
||||||
<div className="mt-5 space-y-3">
|
|
||||||
{contacts.length === 0 ? (
|
{contacts.length === 0 ? (
|
||||||
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
|
<div className="rounded-[18px] border border-dashed border-line/70 bg-page/60 px-3 py-5 text-center text-sm text-muted">
|
||||||
No contacts have been added yet.
|
No contacts have been added yet.
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -72,7 +71,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
|
|||||||
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-2 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-semibold text-text">
|
<div className="text-sm font-semibold text-text">
|
||||||
{contact.fullName} {contact.isPrimary ? <span className="text-brand">• Primary</span> : null}
|
{contact.fullName} {contact.isPrimary ? <span className="text-brand">- PRIMARY</span> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-sm text-muted">{crmContactRoleOptions.find((option) => option.value === contact.role)?.label ?? contact.role}</div>
|
<div className="mt-1 text-sm text-muted">{crmContactRoleOptions.find((option) => option.value === contact.role)?.label ?? contact.role}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -86,10 +85,10 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{canManage ? (
|
{canManage ? (
|
||||||
<form className="mt-5 space-y-4" onSubmit={handleSubmit}>
|
<form className="mt-3 space-y-3" onSubmit={handleSubmit}>
|
||||||
<div className="grid gap-3 xl:grid-cols-2">
|
<div className="grid gap-3 xl:grid-cols-2">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Full name</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Full name</span>
|
||||||
<input
|
<input
|
||||||
value={form.fullName}
|
value={form.fullName}
|
||||||
onChange={(event) => updateField("fullName", event.target.value)}
|
onChange={(event) => updateField("fullName", event.target.value)}
|
||||||
@@ -97,7 +96,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Role</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Role</span>
|
||||||
<select
|
<select
|
||||||
value={form.role}
|
value={form.role}
|
||||||
onChange={(event) => updateField("role", event.target.value as CrmContactInput["role"])}
|
onChange={(event) => updateField("role", event.target.value as CrmContactInput["role"])}
|
||||||
@@ -111,7 +110,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Email</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Email</span>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={form.email}
|
value={form.email}
|
||||||
@@ -120,7 +119,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Phone</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Phone</span>
|
||||||
<input
|
<input
|
||||||
value={form.phone}
|
value={form.phone}
|
||||||
onChange={(event) => updateField("phone", event.target.value)}
|
onChange={(event) => updateField("phone", event.target.value)}
|
||||||
@@ -151,4 +150,3 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
|
|||||||
</article>
|
</article>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
}, [token, supportLogLevel, supportLogSource, supportLogQuery, supportLogWindowDays]);
|
}, [token, supportLogLevel, supportLogSource, supportLogQuery, supportLogWindowDays]);
|
||||||
|
|
||||||
if (!diagnostics || !backupGuidance) {
|
if (!diagnostics || !backupGuidance) {
|
||||||
return <div className="rounded-[20px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
|
return <div className="surface-panel text-sm text-muted">{status}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleExportSupportSnapshot() {
|
async function handleExportSupportSnapshot() {
|
||||||
@@ -156,7 +156,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="page-stack">
|
||||||
<section className="surface-panel backdrop-blur">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -186,11 +186,11 @@ export function AdminDiagnosticsPage() {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
<div className="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{summaryCards.map(([label, value]) => (
|
{summaryCards.map(([label, value]) => (
|
||||||
<div key={label} className="rounded-[18px] border border-line/70 bg-page/70 p-4">
|
<div key={label} className="rounded-[18px] border border-line/70 bg-page/70 px-3 py-3">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted">{label}</p>
|
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted">{label}</p>
|
||||||
<p className="mt-3 text-lg font-bold text-text">{value}</p>
|
<p className="mt-2 text-lg font-bold text-text">{value}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -201,16 +201,16 @@ export function AdminDiagnosticsPage() {
|
|||||||
<div>
|
<div>
|
||||||
<p className="section-kicker">BACKUP AND RESTORE</p>
|
<p className="section-kicker">BACKUP AND RESTORE</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-muted">
|
<div className="rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">
|
||||||
<div>Data: {backupGuidance.dataPath}</div>
|
<div>Data: {backupGuidance.dataPath}</div>
|
||||||
<div>DB: {backupGuidance.databasePath}</div>
|
<div>DB: {backupGuidance.databasePath}</div>
|
||||||
<div>Uploads: {backupGuidance.uploadsPath}</div>
|
<div>Uploads: {backupGuidance.uploadsPath}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-4 xl:grid-cols-2">
|
<div className="mt-3 grid gap-3 xl:grid-cols-2">
|
||||||
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
|
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||||
<p className="text-sm font-semibold text-text">Backup checklist</p>
|
<p className="text-sm font-semibold text-text">Backup checklist</p>
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-2">
|
||||||
{backupGuidance.backupSteps.map((step) => (
|
{backupGuidance.backupSteps.map((step) => (
|
||||||
<div key={step.id}>
|
<div key={step.id}>
|
||||||
<p className="text-sm font-semibold text-text">{step.label}</p>
|
<p className="text-sm font-semibold text-text">{step.label}</p>
|
||||||
@@ -219,9 +219,9 @@ export function AdminDiagnosticsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
|
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||||
<p className="text-sm font-semibold text-text">Restore checklist</p>
|
<p className="text-sm font-semibold text-text">Restore checklist</p>
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-2">
|
||||||
{backupGuidance.restoreSteps.map((step) => (
|
{backupGuidance.restoreSteps.map((step) => (
|
||||||
<div key={step.id}>
|
<div key={step.id}>
|
||||||
<p className="text-sm font-semibold text-text">{step.label}</p>
|
<p className="text-sm font-semibold text-text">{step.label}</p>
|
||||||
@@ -231,10 +231,10 @@ export function AdminDiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-4 xl:grid-cols-2">
|
<div className="mt-3 grid gap-3 xl:grid-cols-2">
|
||||||
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
|
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||||
<p className="text-sm font-semibold text-text">Backup verification checklist</p>
|
<p className="text-sm font-semibold text-text">Backup verification checklist</p>
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-2">
|
||||||
{backupGuidance.verificationChecklist.map((item) => (
|
{backupGuidance.verificationChecklist.map((item) => (
|
||||||
<div key={item.id}>
|
<div key={item.id}>
|
||||||
<p className="text-sm font-semibold text-text">{item.label}</p>
|
<p className="text-sm font-semibold text-text">{item.label}</p>
|
||||||
@@ -244,9 +244,9 @@ export function AdminDiagnosticsPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl border border-line/70 bg-page/70 p-4">
|
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||||
<p className="text-sm font-semibold text-text">Restore drill runbook</p>
|
<p className="text-sm font-semibold text-text">Restore drill runbook</p>
|
||||||
<div className="mt-3 space-y-3">
|
<div className="mt-3 space-y-2">
|
||||||
{backupGuidance.restoreDrillSteps.map((step) => (
|
{backupGuidance.restoreDrillSteps.map((step) => (
|
||||||
<div key={step.id}>
|
<div key={step.id}>
|
||||||
<p className="text-sm font-semibold text-text">{step.label}</p>
|
<p className="text-sm font-semibold text-text">{step.label}</p>
|
||||||
@@ -268,7 +268,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
{diagnostics.startup.status}
|
{diagnostics.startup.status}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-3 xl:grid-cols-2">
|
<div className="mt-3 grid gap-3 xl:grid-cols-2">
|
||||||
{diagnostics.startup.checks.map((check) => (
|
{diagnostics.startup.checks.map((check) => (
|
||||||
<div key={check.id} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
<div key={check.id} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
@@ -279,7 +279,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-3 lg:grid-cols-3">
|
<div className="mt-3 grid gap-3 lg:grid-cols-3">
|
||||||
{startupSummaryCards.map(([label, value]) => (
|
{startupSummaryCards.map(([label, value]) => (
|
||||||
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
|
||||||
@@ -291,7 +291,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
|
|
||||||
<section className="surface-panel backdrop-blur">
|
<section className="surface-panel backdrop-blur">
|
||||||
<p className="section-kicker">SYSTEM FOOTPRINT</p>
|
<p className="section-kicker">SYSTEM FOOTPRINT</p>
|
||||||
<div className="mt-5 grid gap-3 xl:grid-cols-2">
|
<div className="mt-3 grid gap-3 xl:grid-cols-2">
|
||||||
{footprintCards.map(([label, value]) => (
|
{footprintCards.map(([label, value]) => (
|
||||||
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
<div key={label} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
||||||
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">{label}</p>
|
||||||
@@ -310,9 +310,9 @@ export function AdminDiagnosticsPage() {
|
|||||||
{supportLogSummary ? `${supportLogSummary.filteredCount} of ${supportLogSummary.totalCount} entries` : "No entries loaded"}
|
{supportLogSummary ? `${supportLogSummary.filteredCount} of ${supportLogSummary.totalCount} entries` : "No entries loaded"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
<div className="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
||||||
<input
|
<input
|
||||||
value={supportLogQuery}
|
value={supportLogQuery}
|
||||||
onChange={(event) => setSupportLogQuery(event.target.value)}
|
onChange={(event) => setSupportLogQuery(event.target.value)}
|
||||||
@@ -321,7 +321,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Level</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Level</span>
|
||||||
<select value={supportLogLevel} onChange={(event) => setSupportLogLevel(event.target.value as "ALL" | SupportLogEntryDto["level"])} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
<select value={supportLogLevel} onChange={(event) => setSupportLogLevel(event.target.value as "ALL" | SupportLogEntryDto["level"])} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
||||||
<option value="ALL">All levels</option>
|
<option value="ALL">All levels</option>
|
||||||
<option value="ERROR">Error</option>
|
<option value="ERROR">Error</option>
|
||||||
@@ -330,7 +330,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Source</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Source</span>
|
||||||
<select value={supportLogSource} onChange={(event) => setSupportLogSource(event.target.value)} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
<select value={supportLogSource} onChange={(event) => setSupportLogSource(event.target.value)} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
||||||
<option value="ALL">All sources</option>
|
<option value="ALL">All sources</option>
|
||||||
{supportLogSources.map((source) => (
|
{supportLogSources.map((source) => (
|
||||||
@@ -339,7 +339,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Window</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Window</span>
|
||||||
<select value={supportLogWindowDays} onChange={(event) => setSupportLogWindowDays(event.target.value as "ALL" | "1" | "7" | "14")} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
<select value={supportLogWindowDays} onChange={(event) => setSupportLogWindowDays(event.target.value as "ALL" | "1" | "7" | "14")} className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none">
|
||||||
<option value="ALL">All retained</option>
|
<option value="ALL">All retained</option>
|
||||||
<option value="1">Last 24 hours</option>
|
<option value="1">Last 24 hours</option>
|
||||||
@@ -347,13 +347,13 @@ export function AdminDiagnosticsPage() {
|
|||||||
<option value="14">Last 14 days</option>
|
<option value="14">Last 14 days</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-muted">
|
<div className="rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">
|
||||||
<div>Errors: {supportLogSummary?.levelCounts.ERROR ?? 0}</div>
|
<div>Errors: {supportLogSummary?.levelCounts.ERROR ?? 0}</div>
|
||||||
<div>Warnings: {supportLogSummary?.levelCounts.WARN ?? 0}</div>
|
<div>Warnings: {supportLogSummary?.levelCounts.WARN ?? 0}</div>
|
||||||
<div>Info: {supportLogSummary?.levelCounts.INFO ?? 0}</div>
|
<div>Info: {supportLogSummary?.levelCounts.INFO ?? 0}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 overflow-x-auto">
|
<div className="mt-3 overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
|
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
|
||||||
@@ -402,7 +402,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-muted">{status}</p>
|
<p className="text-sm text-muted">{status}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 overflow-x-auto">
|
<div className="mt-3 overflow-x-auto">
|
||||||
<table className="min-w-full divide-y divide-line/70 text-sm">
|
<table className="min-w-full divide-y divide-line/70 text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
|
<tr className="text-left text-xs font-semibold uppercase tracking-[0.18em] text-muted">
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ export function UserManagementPage() {
|
|||||||
const reviewSessionCount = sessions.filter((session) => session.reviewState === "REVIEW").length;
|
const reviewSessionCount = sessions.filter((session) => session.reviewState === "REVIEW").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="page-stack">
|
||||||
<section className="surface-panel backdrop-blur">
|
<section className="surface-panel backdrop-blur">
|
||||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
||||||
<div>
|
<div>
|
||||||
@@ -290,7 +290,7 @@ export function UserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="grid gap-6 xl:grid-cols-2">
|
<section className="grid gap-3 xl:grid-cols-2">
|
||||||
<form className="surface-panel backdrop-blur" onSubmit={handleUserSave}>
|
<form className="surface-panel backdrop-blur" onSubmit={handleUserSave}>
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
@@ -310,9 +310,9 @@ export function UserManagementPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 grid gap-4 md:grid-cols-2">
|
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Email</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Email</span>
|
||||||
<input
|
<input
|
||||||
value={userForm.email}
|
value={userForm.email}
|
||||||
onChange={(event) => setUserForm((current) => ({ ...current, email: event.target.value }))}
|
onChange={(event) => setUserForm((current) => ({ ...current, email: event.target.value }))}
|
||||||
@@ -320,7 +320,7 @@ export function UserManagementPage() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Password</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Password</span>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={userForm.password ?? ""}
|
value={userForm.password ?? ""}
|
||||||
@@ -330,7 +330,7 @@ export function UserManagementPage() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">First name</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">First name</span>
|
||||||
<input
|
<input
|
||||||
value={userForm.firstName}
|
value={userForm.firstName}
|
||||||
onChange={(event) => setUserForm((current) => ({ ...current, firstName: event.target.value }))}
|
onChange={(event) => setUserForm((current) => ({ ...current, firstName: event.target.value }))}
|
||||||
@@ -338,7 +338,7 @@ export function UserManagementPage() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Last name</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Last name</span>
|
||||||
<input
|
<input
|
||||||
value={userForm.lastName}
|
value={userForm.lastName}
|
||||||
onChange={(event) => setUserForm((current) => ({ ...current, lastName: event.target.value }))}
|
onChange={(event) => setUserForm((current) => ({ ...current, lastName: event.target.value }))}
|
||||||
@@ -347,7 +347,7 @@ export function UserManagementPage() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label className="mt-4 flex items-center gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
|
<label className="mt-3 flex items-center gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-text">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={userForm.isActive}
|
checked={userForm.isActive}
|
||||||
@@ -356,11 +356,11 @@ export function UserManagementPage() {
|
|||||||
User can sign in
|
User can sign in
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-3">
|
||||||
<p className="text-sm font-semibold text-text">Assigned roles</p>
|
<p className="section-kicker">ASSIGNED ROLES</p>
|
||||||
<div className="mt-3 grid gap-3">
|
<div className="mt-3 grid gap-2">
|
||||||
{roles.map((role) => (
|
{roles.map((role) => (
|
||||||
<label key={role.id} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
|
<label key={role.id} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-text">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={userForm.roleIds.includes(role.id)}
|
checked={userForm.roleIds.includes(role.id)}
|
||||||
@@ -375,7 +375,7 @@ export function UserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
<div className="mt-3 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2">
|
||||||
<span className="text-sm text-muted">{status}</span>
|
<span className="text-sm text-muted">{status}</span>
|
||||||
<button type="submit" className="rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white">
|
<button type="submit" className="rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white">
|
||||||
{selectedUserId === "new" ? "Create user" : "Save user"}
|
{selectedUserId === "new" ? "Create user" : "Save user"}
|
||||||
@@ -402,9 +402,9 @@ export function UserManagementPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 grid gap-4">
|
<div className="mt-3 grid gap-3">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Role name</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Role name</span>
|
||||||
<input
|
<input
|
||||||
value={roleForm.name}
|
value={roleForm.name}
|
||||||
onChange={(event) => setRoleForm((current) => ({ ...current, name: event.target.value }))}
|
onChange={(event) => setRoleForm((current) => ({ ...current, name: event.target.value }))}
|
||||||
@@ -412,7 +412,7 @@ export function UserManagementPage() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Description</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Description</span>
|
||||||
<textarea
|
<textarea
|
||||||
value={roleForm.description}
|
value={roleForm.description}
|
||||||
onChange={(event) => setRoleForm((current) => ({ ...current, description: event.target.value }))}
|
onChange={(event) => setRoleForm((current) => ({ ...current, description: event.target.value }))}
|
||||||
@@ -422,11 +422,11 @@ export function UserManagementPage() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5">
|
<div className="mt-3">
|
||||||
<p className="text-sm font-semibold text-text">Role permissions</p>
|
<p className="section-kicker">ROLE PERMISSIONS</p>
|
||||||
<div className="mt-3 grid gap-3 md:grid-cols-2">
|
<div className="mt-3 grid gap-2 md:grid-cols-2">
|
||||||
{permissions.map((permission) => (
|
{permissions.map((permission) => (
|
||||||
<label key={permission.key} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3 text-sm text-text">
|
<label key={permission.key} className="flex items-start gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-text">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={roleForm.permissionKeys.includes(permission.key)}
|
checked={roleForm.permissionKeys.includes(permission.key)}
|
||||||
@@ -441,9 +441,9 @@ export function UserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 grid gap-3 md:grid-cols-3">
|
<div className="mt-3 grid gap-2 md:grid-cols-3">
|
||||||
{roles.map((role) => (
|
{roles.map((role) => (
|
||||||
<div key={role.id} className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
<div key={role.id} className="rounded-2xl border border-line/70 bg-page/70 px-2 py-2">
|
||||||
<p className="text-sm font-semibold text-text">{role.name}</p>
|
<p className="text-sm font-semibold text-text">{role.name}</p>
|
||||||
<p className="mt-1 text-xs text-muted">{role.userCount} assigned users</p>
|
<p className="mt-1 text-xs text-muted">{role.userCount} assigned users</p>
|
||||||
<p className="mt-2 text-xs text-muted">{role.permissionKeys.length} permissions</p>
|
<p className="mt-2 text-xs text-muted">{role.permissionKeys.length} permissions</p>
|
||||||
@@ -451,7 +451,7 @@ export function UserManagementPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-5 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
|
<div className="mt-3 flex items-center justify-between gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2">
|
||||||
<span className="text-sm text-muted">{status}</span>
|
<span className="text-sm text-muted">{status}</span>
|
||||||
<button type="submit" className="rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white">
|
<button type="submit" className="rounded-2xl bg-brand px-4 py-2 text-sm font-semibold text-white">
|
||||||
{selectedRoleId === "new" ? "Create role" : "Save role"}
|
{selectedRoleId === "new" ? "Create role" : "Save role"}
|
||||||
@@ -467,7 +467,7 @@ export function UserManagementPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
|
||||||
<input
|
<input
|
||||||
value={sessionQuery}
|
value={sessionQuery}
|
||||||
onChange={(event) => setSessionQuery(event.target.value)}
|
onChange={(event) => setSessionQuery(event.target.value)}
|
||||||
@@ -476,7 +476,7 @@ export function UserManagementPage() {
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">User</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">User</span>
|
||||||
<select
|
<select
|
||||||
value={sessionUserFilter}
|
value={sessionUserFilter}
|
||||||
onChange={(event) => setSessionUserFilter(event.target.value)}
|
onChange={(event) => setSessionUserFilter(event.target.value)}
|
||||||
@@ -491,7 +491,7 @@ export function UserManagementPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Status</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Status</span>
|
||||||
<select
|
<select
|
||||||
value={sessionStatusFilter}
|
value={sessionStatusFilter}
|
||||||
onChange={(event) => setSessionStatusFilter(event.target.value as "ALL" | AdminAuthSessionDto["status"])}
|
onChange={(event) => setSessionStatusFilter(event.target.value as "ALL" | AdminAuthSessionDto["status"])}
|
||||||
@@ -504,7 +504,7 @@ export function UserManagementPage() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="mb-2 block text-sm font-semibold text-text">Review</span>
|
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Review</span>
|
||||||
<select
|
<select
|
||||||
value={sessionReviewFilter}
|
value={sessionReviewFilter}
|
||||||
onChange={(event) => setSessionReviewFilter(event.target.value as "ALL" | AdminAuthSessionDto["reviewState"])}
|
onChange={(event) => setSessionReviewFilter(event.target.value as "ALL" | AdminAuthSessionDto["reviewState"])}
|
||||||
|
|||||||
Reference in New Issue
Block a user