This commit is contained in:
2026-03-14 22:21:31 -05:00
parent 2cf6bf858d
commit 6589581908
18 changed files with 197 additions and 196 deletions

Submodule .claude/worktrees/inspiring-leavitt added at 2cf6bf858d

View File

@@ -18,11 +18,11 @@ export function AppShell() {
return ( return (
<div className="min-h-screen px-4 py-5 xl:px-6 2xl:px-8"> <div className="min-h-screen px-4 py-5 xl:px-6 2xl:px-8">
<div className="mx-auto flex w-full max-w-[1760px] gap-4 2xl:gap-6"> <div className="mx-auto flex w-full max-w-[1760px] gap-3 2xl:gap-4">
<aside className="hidden w-72 shrink-0 flex-col rounded-[28px] border border-line/70 bg-surface/90 p-5 shadow-panel backdrop-blur md:flex 2xl:w-80"> <aside className="hidden w-72 shrink-0 flex-col rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur md:flex 2xl:w-80">
<div> <div>
<div className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">MRP Codex</div> <div className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">MRP Codex</div>
<h1 className="mt-3 text-2xl font-extrabold text-text">Manufacturing foundation</h1> <h1 className="mt-2 text-xl font-extrabold text-text">Manufacturing foundation</h1>
<p className="mt-2 text-sm text-muted">Single-tenant platform shell with branding, auth, file storage, and planning foundations.</p> <p className="mt-2 text-sm text-muted">Single-tenant platform shell with branding, auth, file storage, and planning foundations.</p>
</div> </div>
<nav className="mt-6 space-y-2"> <nav className="mt-6 space-y-2">
@@ -53,10 +53,10 @@ export function AppShell() {
</div> </div>
</aside> </aside>
<main className="min-w-0 flex-1"> <main className="min-w-0 flex-1">
<div className="mb-4 flex items-center justify-between rounded-[28px] border border-line/70 bg-surface/90 px-4 py-3 shadow-panel backdrop-blur 2xl:px-5"> <div className="mb-4 flex items-center justify-between rounded-[28px] border border-line/70 bg-surface/90 px-2 py-2 shadow-panel backdrop-blur 2xl:px-3">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.22em] text-muted">Operations Command</p> <p className="text-xs font-semibold uppercase tracking-[0.22em] text-muted">Operations Command</p>
<h2 className="text-xl font-bold text-text">Foundation Console</h2> <h2 className="text-lg font-bold text-text">Foundation Console</h2>
</div> </div>
<ThemeToggle /> <ThemeToggle />
</div> </div>

View File

@@ -120,29 +120,29 @@ export function CrmAttachmentsPanel({ ownerType, ownerId, onAttachmentCountChang
} }
return ( return (
<article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Attachments</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Attachments</p>
<h4 className="mt-3 text-xl font-bold text-text">Shared files</h4> <h4 className="mt-2 text-lg font-bold text-text">Shared files</h4>
<p className="mt-2 text-sm text-muted"> <p className="mt-2 text-sm text-muted">
Drawings, customer markups, vendor documents, and other reference files linked to this record. Drawings, customer markups, vendor documents, and other reference files linked to this record.
</p> </p>
</div> </div>
{canWriteFiles ? ( {canWriteFiles ? (
<label className="inline-flex cursor-pointer items-center justify-center rounded-2xl bg-brand px-5 py-3 text-sm font-semibold text-white"> <label className="inline-flex cursor-pointer items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white">
{isUploading ? "Uploading..." : "Upload file"} {isUploading ? "Uploading..." : "Upload file"}
<input className="hidden" type="file" onChange={handleUpload} disabled={isUploading} /> <input className="hidden" type="file" onChange={handleUpload} disabled={isUploading} />
</label> </label>
) : null} ) : null}
</div> </div>
<div className="mt-5 rounded-2xl border border-line/70 bg-page/70 px-4 py-3 text-sm text-muted">{status}</div> <div className="mt-5 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">{status}</div>
{!canReadFiles ? ( {!canReadFiles ? (
<div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-6 py-12 text-center text-sm text-muted"> <div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
You do not have permission to view file attachments. You do not have permission to view file attachments.
</div> </div>
) : attachments.length === 0 ? ( ) : attachments.length === 0 ? (
<div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-6 py-12 text-center text-sm text-muted"> <div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
No attachments have been added to this record yet. No attachments have been added to this record yet.
</div> </div>
) : ( ) : (
@@ -150,7 +150,7 @@ export function CrmAttachmentsPanel({ ownerType, ownerId, onAttachmentCountChang
{attachments.map((attachment) => ( {attachments.map((attachment) => (
<div <div
key={attachment.id} key={attachment.id}
className="flex flex-col gap-3 rounded-3xl border border-line/70 bg-page/60 px-4 py-4 lg:flex-row lg:items-center lg:justify-between" className="flex flex-col gap-2 rounded-3xl border border-line/70 bg-page/60 px-2 py-2 lg:flex-row lg:items-center lg:justify-between"
> >
<div className="min-w-0"> <div className="min-w-0">
<p className="truncate text-sm font-semibold text-text">{attachment.originalName}</p> <p className="truncate text-sm font-semibold text-text">{attachment.originalName}</p>

View File

@@ -13,7 +13,7 @@ interface CrmContactEntryFormProps {
export function CrmContactEntryForm({ form, isSaving, status, onChange, onSubmit }: CrmContactEntryFormProps) { export function CrmContactEntryForm({ form, isSaving, status, onChange, onSubmit }: CrmContactEntryFormProps) {
return ( return (
<form className="space-y-4" onSubmit={onSubmit}> <form className="space-y-4" onSubmit={onSubmit}>
<div className="grid gap-4 xl:grid-cols-[minmax(0,0.9fr)_minmax(220px,1.1fr)]"> <div className="grid gap-3 xl:grid-cols-[minmax(0,0.9fr)_minmax(220px,1.1fr)]">
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Interaction type</span> <span className="mb-2 block text-sm font-semibold text-text">Interaction type</span>
<select <select
@@ -57,12 +57,12 @@ export function CrmContactEntryForm({ form, isSaving, status, onChange, onSubmit
placeholder="Capture what happened, follow-ups, and commitments." placeholder="Capture what happened, follow-ups, and commitments."
/> />
</label> </label>
<div className="flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span> <span className="min-w-0 text-sm text-muted">{status}</span>
<button <button
type="submit" type="submit"
disabled={isSaving} disabled={isSaving}
className="rounded-2xl bg-brand px-5 py-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"
> >
{isSaving ? "Saving..." : "Add entry"} {isSaving ? "Saving..." : "Add entry"}
</button> </button>

View File

@@ -58,17 +58,17 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
} }
return ( return (
<article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contacts</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contacts</p>
<h4 className="mt-3 text-xl font-bold text-text">People on this account</h4> <h4 className="mt-2 text-lg font-bold text-text">People on this account</h4>
<div className="mt-5 space-y-3"> <div className="mt-5 space-y-3">
{contacts.length === 0 ? ( {contacts.length === 0 ? (
<div className="rounded-3xl border border-dashed border-line/70 bg-page/60 px-6 py-10 text-center text-sm text-muted"> <div className="rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
No contacts have been added yet. No contacts have been added yet.
</div> </div>
) : ( ) : (
contacts.map((contact) => ( contacts.map((contact) => (
<div key={contact.id} className="rounded-3xl border border-line/70 bg-page/60 px-4 py-4"> <div key={contact.id} className="rounded-3xl border border-line/70 bg-page/60 px-2 py-2">
<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">
@@ -87,7 +87,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
</div> </div>
{canManage ? ( {canManage ? (
<form className="mt-5 space-y-4" onSubmit={handleSubmit}> <form className="mt-5 space-y-4" onSubmit={handleSubmit}>
<div className="grid gap-4 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-sm font-semibold text-text">Full name</span>
<input <input
@@ -128,7 +128,7 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
/> />
</label> </label>
</div> </div>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input <input
type="checkbox" type="checkbox"
checked={form.isPrimary} checked={form.isPrimary}
@@ -136,12 +136,12 @@ export function CrmContactsPanel({ entity, ownerId, contacts, onContactsChange }
/> />
<span className="text-sm font-semibold text-text">Primary contact</span> <span className="text-sm font-semibold text-text">Primary contact</span>
</label> </label>
<div className="flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="text-sm text-muted">{status}</span> <span className="text-sm text-muted">{status}</span>
<button <button
type="submit" type="submit"
disabled={isSaving} disabled={isSaving}
className="rounded-2xl bg-brand px-5 py-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"
> >
{isSaving ? "Saving..." : "Add contact"} {isSaving ? "Saving..." : "Add contact"}
</button> </button>

View File

@@ -50,7 +50,7 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
}, [config.singularLabel, entity, recordId, token]); }, [config.singularLabel, entity, recordId, token]);
if (!record) { if (!record) {
return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-8 text-sm text-muted shadow-panel">{status}</div>; return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
} }
function updateContactEntryField<Key extends keyof CrmContactEntryInput>(key: Key, value: CrmContactEntryInput[Key]) { function updateContactEntryField<Key extends keyof CrmContactEntryInput>(key: Key, value: CrmContactEntryInput[Key]) {
@@ -104,11 +104,11 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
return ( return (
<section className="space-y-4"> <section className="space-y-4">
<div className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM Detail</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM Detail</p>
<h3 className="mt-3 text-3xl font-bold text-text">{record.name}</h3> <h3 className="mt-2 text-2xl font-bold text-text">{record.name}</h3>
<div className="mt-4"> <div className="mt-4">
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<CrmStatusBadge status={record.status} /> <CrmStatusBadge status={record.status} />
@@ -122,14 +122,14 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Link <Link
to={config.routeBase} to={config.routeBase}
className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"
> >
Back to {config.collectionLabel.toLowerCase()} Back to {config.collectionLabel.toLowerCase()}
</Link> </Link>
{canManage ? ( {canManage ? (
<Link <Link
to={`${config.routeBase}/${record.id}/edit`} to={`${config.routeBase}/${record.id}/edit`}
className="inline-flex items-center justify-center rounded-2xl bg-brand px-5 py-3 text-sm font-semibold text-white" className="inline-flex items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white"
> >
Edit {config.singularLabel.toLowerCase()} Edit {config.singularLabel.toLowerCase()}
</Link> </Link>
@@ -137,21 +137,21 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</div> </div>
</div> </div>
</div> </div>
<div className="grid gap-4 2xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]"> <div className="grid gap-3 2xl:grid-cols-[minmax(0,1.2fr)_minmax(320px,0.8fr)]">
<article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contact</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contact</p>
<dl className="mt-5 grid gap-4 xl:grid-cols-2"> <dl className="mt-5 grid gap-3 xl:grid-cols-2">
<div> <div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Email</dt> <dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Email</dt>
<dd className="mt-2 text-base text-text">{record.email}</dd> <dd className="mt-1 text-sm text-text">{record.email}</dd>
</div> </div>
<div> <div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Phone</dt> <dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Phone</dt>
<dd className="mt-2 text-base text-text">{record.phone}</dd> <dd className="mt-1 text-sm text-text">{record.phone}</dd>
</div> </div>
<div className="md:col-span-2"> <div className="md:col-span-2">
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Address</dt> <dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Address</dt>
<dd className="mt-2 whitespace-pre-line text-base text-text"> <dd className="mt-1 whitespace-pre-line text-sm text-text">
{[record.addressLine1, record.addressLine2, `${record.city}, ${record.state} ${record.postalCode}`, record.country] {[record.addressLine1, record.addressLine2, `${record.city}, ${record.state} ${record.postalCode}`, record.country]
.filter(Boolean) .filter(Boolean)
.join("\n")} .join("\n")}
@@ -168,15 +168,15 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</div> </div>
</dl> </dl>
</article> </article>
<article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Internal Notes</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Internal Notes</p>
<p className="mt-4 whitespace-pre-line text-sm leading-7 text-text"> <p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">
{record.notes || "No internal notes recorded for this account yet."} {record.notes || "No internal notes recorded for this account yet."}
</p> </p>
<div className="mt-8 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 text-sm text-muted"> <div className="mt-8 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">
Created {new Date(record.createdAt).toLocaleDateString()} Created {new Date(record.createdAt).toLocaleDateString()}
</div> </div>
<div className="mt-4 rounded-2xl border border-line/70 bg-page/70 px-4 py-4"> <div className="mt-4 rounded-2xl border border-line/70 bg-page/70 px-2 py-2">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Operational Flags</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Operational Flags</p>
<div className="mt-3 flex flex-wrap gap-2 text-xs font-semibold uppercase tracking-[0.12em]"> <div className="mt-3 flex flex-wrap gap-2 text-xs font-semibold uppercase tracking-[0.12em]">
{record.preferredAccount ? <span className="rounded-full border border-line/70 px-3 py-1 text-text">Preferred</span> : null} {record.preferredAccount ? <span className="rounded-full border border-line/70 px-3 py-1 text-text">Preferred</span> : null}
@@ -189,7 +189,7 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</div> </div>
</div> </div>
{entity === "customer" ? ( {entity === "customer" ? (
<div className="mt-4 rounded-2xl border border-line/70 bg-page/70 px-4 py-4"> <div className="mt-4 rounded-2xl border border-line/70 bg-page/70 px-2 py-2">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Reseller Profile</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Reseller Profile</p>
<div className="mt-3 grid gap-3 text-sm text-text"> <div className="mt-3 grid gap-3 text-sm text-text">
<div> <div>
@@ -210,36 +210,36 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
) : null} ) : null}
</article> </article>
</div> </div>
<section className="grid gap-4 xl:grid-cols-4"> <section className="grid gap-3 xl:grid-cols-4">
<article className="rounded-[24px] border border-line/70 bg-surface/90 px-5 py-5 shadow-panel"> <article className="rounded-[24px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Last Contact</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Last Contact</p>
<div className="mt-3 text-lg font-bold text-text"> <div className="mt-2 text-base font-bold text-text">
{record.rollups?.lastContactAt ? new Date(record.rollups.lastContactAt).toLocaleDateString() : "None"} {record.rollups?.lastContactAt ? new Date(record.rollups.lastContactAt).toLocaleDateString() : "None"}
</div> </div>
</article> </article>
<article className="rounded-[24px] border border-line/70 bg-surface/90 px-5 py-5 shadow-panel"> <article className="rounded-[24px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Timeline Entries</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Timeline Entries</p>
<div className="mt-3 text-lg font-bold text-text">{record.rollups?.contactHistoryCount ?? record.contactHistory.length}</div> <div className="mt-2 text-base font-bold text-text">{record.rollups?.contactHistoryCount ?? record.contactHistory.length}</div>
</article> </article>
<article className="rounded-[24px] border border-line/70 bg-surface/90 px-5 py-5 shadow-panel"> <article className="rounded-[24px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Account Contacts</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Account Contacts</p>
<div className="mt-3 text-lg font-bold text-text">{record.rollups?.contactCount ?? record.contacts?.length ?? 0}</div> <div className="mt-2 text-base font-bold text-text">{record.rollups?.contactCount ?? record.contacts?.length ?? 0}</div>
</article> </article>
<article className="rounded-[24px] border border-line/70 bg-surface/90 px-5 py-5 shadow-panel"> <article className="rounded-[24px] border border-line/70 bg-surface/90 px-3 py-3 shadow-panel">
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Attachments</p> <p className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Attachments</p>
<div className="mt-3 text-lg font-bold text-text">{record.rollups?.attachmentCount ?? 0}</div> <div className="mt-2 text-base font-bold text-text">{record.rollups?.attachmentCount ?? 0}</div>
</article> </article>
</section> </section>
{entity === "customer" && (record.childCustomers?.length ?? 0) > 0 ? ( {entity === "customer" && (record.childCustomers?.length ?? 0) > 0 ? (
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Hierarchy</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Hierarchy</p>
<h4 className="mt-3 text-xl font-bold text-text">End customers under this reseller</h4> <h4 className="mt-2 text-lg font-bold text-text">End customers under this reseller</h4>
<div className="mt-5 grid gap-3 xl:grid-cols-2 2xl:grid-cols-3"> <div className="mt-5 grid gap-3 xl:grid-cols-2 2xl:grid-cols-3">
{record.childCustomers?.map((child) => ( {record.childCustomers?.map((child) => (
<Link <Link
key={child.id} key={child.id}
to={`/crm/customers/${child.id}`} to={`/crm/customers/${child.id}`}
className="rounded-3xl border border-line/70 bg-page/60 px-4 py-4 transition hover:border-brand/50 hover:bg-page/80" className="rounded-3xl border border-line/70 bg-page/60 px-2 py-2 transition hover:border-brand/50 hover:bg-page/80"
> >
<div className="text-sm font-semibold text-text">{child.name}</div> <div className="text-sm font-semibold text-text">{child.name}</div>
<div className="mt-2"> <div className="mt-2">
@@ -272,11 +272,11 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
) )
} }
/> />
<section className="grid gap-4 2xl:grid-cols-[minmax(360px,0.88fr)_minmax(0,1.12fr)]"> <section className="grid gap-3 2xl:grid-cols-[minmax(360px,0.88fr)_minmax(0,1.12fr)]">
{canManage ? ( {canManage ? (
<article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contact History</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Contact History</p>
<h4 className="mt-3 text-xl font-bold text-text">Add timeline entry</h4> <h4 className="mt-2 text-lg font-bold text-text">Add timeline entry</h4>
<p className="mt-2 text-sm text-muted"> <p className="mt-2 text-sm text-muted">
Record calls, emails, meetings, and follow-up notes directly against this account. Record calls, emails, meetings, and follow-up notes directly against this account.
</p> </p>
@@ -291,24 +291,24 @@ export function CrmDetailPage({ entity }: CrmDetailPageProps) {
</div> </div>
</article> </article>
) : null} ) : null}
<article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="min-w-0 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Timeline</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Timeline</p>
<h4 className="mt-3 text-xl font-bold text-text">Recent interactions</h4> <h4 className="mt-2 text-lg font-bold text-text">Recent interactions</h4>
{record.contactHistory.length === 0 ? ( {record.contactHistory.length === 0 ? (
<div className="mt-6 rounded-3xl border border-dashed border-line/70 bg-page/60 px-6 py-12 text-center text-sm text-muted"> <div className="mt-6 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
No contact history has been recorded for this account yet. No contact history has been recorded for this account yet.
</div> </div>
) : ( ) : (
<div className="mt-6 space-y-4"> <div className="mt-6 space-y-3">
{record.contactHistory.map((entry) => ( {record.contactHistory.map((entry) => (
<article key={entry.id} className="rounded-3xl border border-line/70 bg-page/60 p-5"> <article key={entry.id} className="rounded-3xl border border-line/70 bg-page/60 p-3">
<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>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<CrmContactTypeBadge type={entry.type} /> <CrmContactTypeBadge type={entry.type} />
<h5 className="text-base font-semibold text-text">{entry.summary}</h5> <h5 className="text-sm font-semibold text-text">{entry.summary}</h5>
</div> </div>
<p className="mt-3 whitespace-pre-line text-sm leading-7 text-text">{entry.body}</p> <p className="mt-2 whitespace-pre-line text-sm leading-6 text-text">{entry.body}</p>
</div> </div>
<div className="text-sm text-muted lg:text-right"> <div className="text-sm text-muted lg:text-right">
<div>{new Date(entry.contactAt).toLocaleString()}</div> <div>{new Date(entry.contactAt).toLocaleString()}</div>

View File

@@ -111,11 +111,11 @@ export function CrmFormPage({ entity, mode }: CrmFormPageProps) {
return ( return (
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM Editor</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM Editor</p>
<h3 className="mt-3 text-2xl font-bold text-text"> <h3 className="mt-2 text-xl font-bold text-text">
{mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`} {mode === "create" ? `New ${config.singularLabel}` : `Edit ${config.singularLabel}`}
</h3> </h3>
<p className="mt-2 max-w-2xl text-sm text-muted"> <p className="mt-2 max-w-2xl text-sm text-muted">
@@ -124,20 +124,20 @@ export function CrmFormPage({ entity, mode }: CrmFormPageProps) {
</div> </div>
<Link <Link
to={mode === "create" ? config.routeBase : `${config.routeBase}/${recordId}`} to={mode === "create" ? config.routeBase : `${config.routeBase}/${recordId}`}
className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"
> >
Cancel Cancel
</Link> </Link>
</div> </div>
</section> </section>
<section className="space-y-5 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="space-y-4 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<CrmRecordForm entity={entity} form={form} hierarchyOptions={hierarchyOptions} onChange={updateField} /> <CrmRecordForm entity={entity} form={form} hierarchyOptions={hierarchyOptions} onChange={updateField} />
<div className="flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 sm:flex-row sm:items-center sm:justify-between"> <div className="flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span> <span className="min-w-0 text-sm text-muted">{status}</span>
<button <button
type="submit" type="submit"
disabled={isSaving} disabled={isSaving}
className="rounded-2xl bg-brand px-5 py-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"
> >
{isSaving ? "Saving..." : mode === "create" ? `Create ${config.singularLabel}` : "Save changes"} {isSaving ? "Saving..." : mode === "create" ? `Create ${config.singularLabel}` : "Save changes"}
</button> </button>

View File

@@ -55,11 +55,11 @@ export function CrmListPage({ entity }: CrmListPageProps) {
}, [config.collectionLabel, entity, lifecycleFilter, operationalFilter, searchTerm, stateFilter, statusFilter, token]); }, [config.collectionLabel, entity, lifecycleFilter, operationalFilter, searchTerm, stateFilter, statusFilter, token]);
return ( return (
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">CRM</p>
<h3 className="mt-3 text-xl font-bold text-text">{config.collectionLabel}</h3> <h3 className="mt-2 text-lg font-bold text-text">{config.collectionLabel}</h3>
<p className="mt-2 max-w-2xl text-sm text-muted"> <p className="mt-2 max-w-2xl text-sm text-muted">
Operational contact records, shipping addresses, and account context for active {config.collectionLabel.toLowerCase()}. Operational contact records, shipping addresses, and account context for active {config.collectionLabel.toLowerCase()}.
</p> </p>
@@ -73,14 +73,14 @@ export function CrmListPage({ entity }: CrmListPageProps) {
</Link> </Link>
) : null} ) : null}
</div> </div>
<div className="mt-6 grid gap-4 rounded-3xl border border-line/70 bg-page/60 p-4 xl:grid-cols-[1.35fr_0.8fr_0.8fr_0.9fr_0.9fr]"> <div className="mt-6 grid gap-3 rounded-3xl border border-line/70 bg-page/60 p-3 xl:grid-cols-[1.35fr_0.8fr_0.8fr_0.9fr_0.9fr]">
<label className="block"> <label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span> <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
<input <input
value={searchTerm} value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)} onChange={(event) => setSearchTerm(event.target.value)}
placeholder={`Search ${config.collectionLabel.toLowerCase()} by company, email, phone, or location`} placeholder={`Search ${config.collectionLabel.toLowerCase()} by company, email, phone, or location`}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
<label className="block"> <label className="block">
@@ -88,7 +88,7 @@ export function CrmListPage({ entity }: CrmListPageProps) {
<select <select
value={statusFilter} value={statusFilter}
onChange={(event) => setStatusFilter(event.target.value as "ALL" | CrmRecordStatus)} onChange={(event) => setStatusFilter(event.target.value as "ALL" | CrmRecordStatus)}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
> >
{crmStatusFilters.map((option) => ( {crmStatusFilters.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@@ -102,7 +102,7 @@ export function CrmListPage({ entity }: CrmListPageProps) {
<select <select
value={lifecycleFilter} value={lifecycleFilter}
onChange={(event) => setLifecycleFilter(event.target.value as "ALL" | CrmLifecycleStage)} onChange={(event) => setLifecycleFilter(event.target.value as "ALL" | CrmLifecycleStage)}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
> >
{crmLifecycleFilters.map((option) => ( {crmLifecycleFilters.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@@ -117,7 +117,7 @@ export function CrmListPage({ entity }: CrmListPageProps) {
value={stateFilter} value={stateFilter}
onChange={(event) => setStateFilter(event.target.value)} onChange={(event) => setStateFilter(event.target.value)}
placeholder="Filter by region" placeholder="Filter by region"
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
<label className="block"> <label className="block">
@@ -127,7 +127,7 @@ export function CrmListPage({ entity }: CrmListPageProps) {
onChange={(event) => onChange={(event) =>
setOperationalFilter(event.target.value as "ALL" | "PREFERRED" | "STRATEGIC" | "REQUIRES_APPROVAL" | "BLOCKED") setOperationalFilter(event.target.value as "ALL" | "PREFERRED" | "STRATEGIC" | "REQUIRES_APPROVAL" | "BLOCKED")
} }
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
> >
{crmOperationalFilters.map((option) => ( {crmOperationalFilters.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>

View File

@@ -58,7 +58,7 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
</select> </select>
</label> </label>
{entity === "customer" ? ( {entity === "customer" ? (
<div className="grid gap-4 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)_minmax(0,1fr)]"> <div className="grid gap-3 xl:grid-cols-[minmax(0,0.9fr)_minmax(0,1.1fr)_minmax(0,1fr)]">
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Reseller account</span> <span className="mb-2 block text-sm font-semibold text-text">Reseller account</span>
<select <select
@@ -102,7 +102,7 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
</label> </label>
</div> </div>
) : null} ) : null}
<div className="grid gap-4 xl:grid-cols-4"> <div className="grid gap-3 xl:grid-cols-4">
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Payment terms</span> <span className="mb-2 block text-sm font-semibold text-text">Payment terms</span>
<input <input
@@ -121,7 +121,7 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
maxLength={8} maxLength={8}
/> />
</label> </label>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input <input
type="checkbox" type="checkbox"
checked={form.taxExempt ?? false} checked={form.taxExempt ?? false}
@@ -129,7 +129,7 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
/> />
<span className="text-sm font-semibold text-text">Tax exempt</span> <span className="text-sm font-semibold text-text">Tax exempt</span>
</label> </label>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input <input
type="checkbox" type="checkbox"
checked={form.creditHold ?? false} checked={form.creditHold ?? false}
@@ -138,8 +138,8 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
<span className="text-sm font-semibold text-text">Credit hold</span> <span className="text-sm font-semibold text-text">Credit hold</span>
</label> </label>
</div> </div>
<div className="grid gap-4 xl:grid-cols-4"> <div className="grid gap-3 xl:grid-cols-4">
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input <input
type="checkbox" type="checkbox"
checked={form.preferredAccount ?? false} checked={form.preferredAccount ?? false}
@@ -147,7 +147,7 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
/> />
<span className="text-sm font-semibold text-text">Preferred account</span> <span className="text-sm font-semibold text-text">Preferred account</span>
</label> </label>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input <input
type="checkbox" type="checkbox"
checked={form.strategicAccount ?? false} checked={form.strategicAccount ?? false}
@@ -155,7 +155,7 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
/> />
<span className="text-sm font-semibold text-text">Strategic account</span> <span className="text-sm font-semibold text-text">Strategic account</span>
</label> </label>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input <input
type="checkbox" type="checkbox"
checked={form.requiresApproval ?? false} checked={form.requiresApproval ?? false}
@@ -163,7 +163,7 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
/> />
<span className="text-sm font-semibold text-text">Requires approval</span> <span className="text-sm font-semibold text-text">Requires approval</span>
</label> </label>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input <input
type="checkbox" type="checkbox"
checked={form.blockedAccount ?? false} checked={form.blockedAccount ?? false}
@@ -172,7 +172,7 @@ export function CrmRecordForm({ entity, form, hierarchyOptions = [], onChange }:
<span className="text-sm font-semibold text-text">Blocked account</span> <span className="text-sm font-semibold text-text">Blocked account</span>
</label> </label>
</div> </div>
<div className="grid gap-4 xl:grid-cols-2 2xl:grid-cols-3"> <div className="grid gap-3 xl:grid-cols-2 2xl:grid-cols-3">
{fields.map((field) => ( {fields.map((field) => (
<label key={String(field.key)} className="block"> <label key={String(field.key)} className="block">
<span className="mb-2 block text-sm font-semibold text-text">{field.label}</span> <span className="mb-2 block text-sm font-semibold text-text">{field.label}</span>

View File

@@ -2,11 +2,11 @@ import { Link } from "react-router-dom";
export function DashboardPage() { export function DashboardPage() {
return ( return (
<div className="grid gap-6 xl:grid-cols-[1.15fr_0.85fr]"> <div className="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel backdrop-blur"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Foundation Status</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Foundation Status</p>
<h3 className="mt-3 text-2xl font-bold text-text">Platform primitives are online.</h3> <h3 className="mt-2 text-xl font-bold text-text">Platform primitives are online.</h3>
<p className="mt-4 max-w-2xl text-sm leading-7 text-muted"> <p className="mt-3 max-w-2xl text-sm leading-6 text-muted">
Authentication, RBAC, runtime branding, attachment storage, Docker deployment, and a planning visualization wrapper are now structured for future domain expansion. Authentication, RBAC, runtime branding, attachment storage, Docker deployment, and a planning visualization wrapper are now structured for future domain expansion.
</p> </p>
<div className="mt-8 flex flex-wrap gap-3"> <div className="mt-8 flex flex-wrap gap-3">
@@ -21,7 +21,7 @@ export function DashboardPage() {
</Link> </Link>
</div> </div>
</section> </section>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel backdrop-blur"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Roadmap</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Roadmap</p>
<div className="mt-5 space-y-4"> <div className="mt-5 space-y-4">
{[ {[
@@ -29,7 +29,7 @@ export function DashboardPage() {
"Company Settings drives runtime brand tokens and PDF identity.", "Company Settings drives runtime brand tokens and PDF identity.",
"Inventory item master and BOM records now have a dedicated protected module.", "Inventory item master and BOM records now have a dedicated protected module.",
].map((item) => ( ].map((item) => (
<div key={item} className="rounded-2xl border border-line/70 bg-page/70 px-4 py-4 text-sm text-text"> <div key={item} className="rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-text">
{item} {item}
</div> </div>
))} ))}

View File

@@ -34,17 +34,17 @@ export function InventoryDetailPage() {
}, [itemId, token]); }, [itemId, token]);
if (!item) { if (!item) {
return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-6 text-sm text-muted shadow-panel">{status}</div>; return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
} }
return ( return (
<section className="space-y-4"> <section className="space-y-4">
<div className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Detail</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Detail</p>
<h3 className="mt-3 text-2xl font-bold text-text">{item.sku}</h3> <h3 className="mt-2 text-xl font-bold text-text">{item.sku}</h3>
<p className="mt-2 text-base text-text">{item.name}</p> <p className="mt-1 text-sm text-text">{item.name}</p>
<div className="mt-4 flex flex-wrap gap-3"> <div className="mt-4 flex flex-wrap gap-3">
<InventoryTypeBadge type={item.type} /> <InventoryTypeBadge type={item.type} />
<InventoryStatusBadge status={item.status} /> <InventoryStatusBadge status={item.status} />
@@ -52,7 +52,7 @@ export function InventoryDetailPage() {
<p className="mt-3 text-sm text-muted">Last updated {new Date(item.updatedAt).toLocaleString()}.</p> <p className="mt-3 text-sm text-muted">Last updated {new Date(item.updatedAt).toLocaleString()}.</p>
</div> </div>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Link to="/inventory/items" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text"> <Link to="/inventory/items" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Back to items Back to items
</Link> </Link>
{canManage ? ( {canManage ? (
@@ -63,13 +63,13 @@ export function InventoryDetailPage() {
</div> </div>
</div> </div>
</div> </div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,1.05fr)_minmax(340px,0.95fr)]"> <div className="grid gap-3 xl:grid-cols-[minmax(0,1.05fr)_minmax(340px,0.95fr)]">
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Item Definition</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Item Definition</p>
<dl className="mt-5 grid gap-4 xl:grid-cols-2"> <dl className="mt-5 grid gap-3 xl:grid-cols-2">
<div> <div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Description</dt> <dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Description</dt>
<dd className="mt-2 text-sm leading-7 text-text">{item.description || "No description provided."}</dd> <dd className="mt-1 text-sm leading-6 text-text">{item.description || "No description provided."}</dd>
</div> </div>
<div> <div>
<dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Unit of measure</dt> <dt className="text-xs font-semibold uppercase tracking-[0.18em] text-muted">Unit of measure</dt>
@@ -87,17 +87,17 @@ export function InventoryDetailPage() {
</div> </div>
</dl> </dl>
</article> </article>
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Internal Notes</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Internal Notes</p>
<p className="mt-4 whitespace-pre-line text-sm leading-7 text-text">{item.notes || "No internal notes recorded for this item yet."}</p> <p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{item.notes || "No internal notes recorded for this item yet."}</p>
<div className="mt-8 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 text-sm text-muted"> <div className="mt-8 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">
Created {new Date(item.createdAt).toLocaleDateString()} Created {new Date(item.createdAt).toLocaleDateString()}
</div> </div>
</article> </article>
</div> </div>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Bill Of Materials</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Bill Of Materials</p>
<h4 className="mt-3 text-xl font-bold text-text">Component structure</h4> <h4 className="mt-2 text-lg font-bold text-text">Component structure</h4>
{item.bomLines.length === 0 ? ( {item.bomLines.length === 0 ? (
<div className="mt-6 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted"> <div className="mt-6 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
No BOM lines are defined for this item yet. No BOM lines are defined for this item yet.

View File

@@ -153,25 +153,25 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
return ( return (
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Editor</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory Editor</p>
<h3 className="mt-3 text-2xl font-bold text-text">{mode === "create" ? "New Item" : "Edit Item"}</h3> <h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Item" : "Edit Item"}</h3>
<p className="mt-2 max-w-2xl text-sm text-muted"> <p className="mt-2 max-w-2xl text-sm text-muted">
Define item master data and the first revision of the bill of materials for assemblies and manufactured items. Define item master data and the first revision of the bill of materials for assemblies and manufactured items.
</p> </p>
</div> </div>
<Link <Link
to={mode === "create" ? "/inventory/items" : `/inventory/items/${itemId}`} to={mode === "create" ? "/inventory/items" : `/inventory/items/${itemId}`}
className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"
> >
Cancel Cancel
</Link> </Link>
</div> </div>
</section> </section>
<section className="space-y-5 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="space-y-4 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="grid gap-4 xl:grid-cols-2 2xl:grid-cols-4"> <div className="grid gap-3 xl:grid-cols-2 2xl:grid-cols-4">
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">SKU</span> <span className="mb-2 block text-sm font-semibold text-text">SKU</span>
<input <input
@@ -200,7 +200,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
/> />
</label> </label>
</div> </div>
<div className="grid gap-4 xl:grid-cols-4"> <div className="grid gap-3 xl:grid-cols-4">
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Type</span> <span className="mb-2 block text-sm font-semibold text-text">Type</span>
<select <select
@@ -244,11 +244,11 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
</select> </select>
</label> </label>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input type="checkbox" checked={form.isSellable} onChange={(event) => updateField("isSellable", event.target.checked)} /> <input type="checkbox" checked={form.isSellable} onChange={(event) => updateField("isSellable", event.target.checked)} />
<span className="text-sm font-semibold text-text">Sellable</span> <span className="text-sm font-semibold text-text">Sellable</span>
</label> </label>
<label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-4 py-3"> <label className="flex items-center gap-3 rounded-2xl border border-line/70 bg-page px-2 py-2">
<input <input
type="checkbox" type="checkbox"
checked={form.isPurchasable} checked={form.isPurchasable}
@@ -264,7 +264,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
value={form.description} value={form.description}
onChange={(event) => updateField("description", event.target.value)} onChange={(event) => updateField("description", event.target.value)}
rows={4} rows={4}
className="w-full rounded-3xl border border-line/70 bg-page px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-3xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
<label className="block"> <label className="block">
@@ -273,34 +273,34 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
value={form.notes} value={form.notes}
onChange={(event) => updateField("notes", event.target.value)} onChange={(event) => updateField("notes", event.target.value)}
rows={4} rows={4}
className="w-full rounded-3xl border border-line/70 bg-page px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-3xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
</section> </section>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Bill Of Materials</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Bill Of Materials</p>
<h4 className="mt-3 text-xl font-bold text-text">Component lines</h4> <h4 className="mt-2 text-lg font-bold text-text">Component lines</h4>
<p className="mt-2 text-sm text-muted">Add BOM components for manufactured or assembly items. Purchased and service items can be saved without BOM lines.</p> <p className="mt-2 text-sm text-muted">Add BOM components for manufactured or assembly items. Purchased and service items can be saved without BOM lines.</p>
</div> </div>
<button <button
type="button" type="button"
onClick={addBomLine} onClick={addBomLine}
className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"
> >
Add BOM line Add BOM line
</button> </button>
</div> </div>
{form.bomLines.length === 0 ? ( {form.bomLines.length === 0 ? (
<div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-6 py-10 text-center text-sm text-muted"> <div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
No BOM lines added yet. No BOM lines added yet.
</div> </div>
) : ( ) : (
<div className="mt-5 space-y-4"> <div className="mt-5 space-y-4">
{form.bomLines.map((line, index) => ( {form.bomLines.map((line, index) => (
<div key={`${line.componentItemId}-${line.position}-${index}`} className="rounded-3xl border border-line/70 bg-page/60 p-4"> <div key={`${line.componentItemId}-${line.position}-${index}`} className="rounded-3xl border border-line/70 bg-page/60 p-3">
<div className="grid gap-4 xl:grid-cols-[1.4fr_0.7fr_0.7fr_0.7fr_auto]"> <div className="grid gap-3 xl:grid-cols-[1.4fr_0.7fr_0.7fr_0.7fr_auto]">
<label className="block"> <label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Component</span> <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Component</span>
<div className="relative"> <div className="relative">
@@ -323,7 +323,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
}, 120); }, 120);
}} }}
placeholder="Search by SKU" placeholder="Search by SKU"
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
<div className="mt-2 min-h-5 text-xs text-muted"> <div className="mt-2 min-h-5 text-xs text-muted">
{getComponentOption(line.componentItemId)?.name ?? "No component selected"} {getComponentOption(line.componentItemId)?.name ?? "No component selected"}
@@ -364,7 +364,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
return option.sku.toLowerCase().includes(query); return option.sku.toLowerCase().includes(query);
}).length === 0 ? ( }).length === 0 ? (
<div className="px-4 py-3 text-sm text-muted">No matching components found.</div> <div className="px-2 py-2 text-sm text-muted">No matching components found.</div>
) : null} ) : null}
</div> </div>
) : null} ) : null}
@@ -378,7 +378,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
step={0.0001} step={0.0001}
value={line.quantity} value={line.quantity}
onChange={(event) => updateBomLine(index, { ...line, quantity: Number(event.target.value) })} onChange={(event) => updateBomLine(index, { ...line, quantity: Number(event.target.value) })}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
<label className="block"> <label className="block">
@@ -386,7 +386,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
<select <select
value={line.unitOfMeasure} value={line.unitOfMeasure}
onChange={(event) => updateBomLine(index, { ...line, unitOfMeasure: event.target.value as InventoryBomLineInput["unitOfMeasure"] })} onChange={(event) => updateBomLine(index, { ...line, unitOfMeasure: event.target.value as InventoryBomLineInput["unitOfMeasure"] })}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
> >
{inventoryUnitOptions.map((option) => ( {inventoryUnitOptions.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@@ -403,14 +403,14 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
step={10} step={10}
value={line.position} value={line.position}
onChange={(event) => updateBomLine(index, { ...line, position: Number(event.target.value) })} onChange={(event) => updateBomLine(index, { ...line, position: Number(event.target.value) })}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
<div className="flex items-end"> <div className="flex items-end">
<button <button
type="button" type="button"
onClick={() => removeBomLine(index)} onClick={() => removeBomLine(index)}
className="rounded-2xl border border-rose-400/40 px-4 py-3 text-sm font-semibold text-rose-700 dark:text-rose-300" className="rounded-2xl border border-rose-400/40 px-2 py-2 text-sm font-semibold text-rose-700 dark:text-rose-300"
> >
Remove Remove
</button> </button>
@@ -421,19 +421,19 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
<input <input
value={line.notes} value={line.notes}
onChange={(event) => updateBomLine(index, { ...line, notes: event.target.value })} onChange={(event) => updateBomLine(index, { ...line, notes: event.target.value })}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
</div> </div>
))} ))}
</div> </div>
)} )}
<div className="mt-6 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 sm:flex-row sm:items-center sm:justify-between"> <div className="mt-6 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span> <span className="min-w-0 text-sm text-muted">{status}</span>
<button <button
type="submit" type="submit"
disabled={isSaving} disabled={isSaving}
className="rounded-2xl bg-brand px-5 py-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60" className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"
> >
{isSaving ? "Saving..." : mode === "create" ? "Create item" : "Save changes"} {isSaving ? "Saving..." : mode === "create" ? "Create item" : "Save changes"}
</button> </button>

View File

@@ -41,11 +41,11 @@ export function InventoryListPage() {
}, [searchTerm, statusFilter, token, typeFilter]); }, [searchTerm, statusFilter, token, typeFilter]);
return ( return (
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory</p>
<h3 className="mt-3 text-xl font-bold text-text">Item Master</h3> <h3 className="mt-2 text-lg font-bold text-text">Item Master</h3>
<p className="mt-2 max-w-2xl text-sm text-muted"> <p className="mt-2 max-w-2xl text-sm text-muted">
Core item and BOM definitions for purchased parts, manufactured items, assemblies, and service SKUs. Core item and BOM definitions for purchased parts, manufactured items, assemblies, and service SKUs.
</p> </p>
@@ -56,14 +56,14 @@ export function InventoryListPage() {
</Link> </Link>
) : null} ) : null}
</div> </div>
<div className="mt-6 grid gap-4 rounded-3xl border border-line/70 bg-page/60 p-4 xl:grid-cols-[1.3fr_0.8fr_0.8fr]"> <div className="mt-6 grid gap-3 rounded-3xl border border-line/70 bg-page/60 p-3 xl:grid-cols-[1.3fr_0.8fr_0.8fr]">
<label className="block"> <label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span> <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Search</span>
<input <input
value={searchTerm} value={searchTerm}
onChange={(event) => setSearchTerm(event.target.value)} onChange={(event) => setSearchTerm(event.target.value)}
placeholder="Search by SKU, item name, or description" placeholder="Search by SKU, item name, or description"
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
<label className="block"> <label className="block">
@@ -71,7 +71,7 @@ export function InventoryListPage() {
<select <select
value={statusFilter} value={statusFilter}
onChange={(event) => setStatusFilter(event.target.value as "ALL" | InventoryItemStatus)} onChange={(event) => setStatusFilter(event.target.value as "ALL" | InventoryItemStatus)}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
> >
{inventoryStatusFilters.map((option) => ( {inventoryStatusFilters.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>
@@ -85,7 +85,7 @@ export function InventoryListPage() {
<select <select
value={typeFilter} value={typeFilter}
onChange={(event) => setTypeFilter(event.target.value as "ALL" | InventoryItemType)} onChange={(event) => setTypeFilter(event.target.value as "ALL" | InventoryItemType)}
className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand"
> >
{inventoryTypeFilters.map((option) => ( {inventoryTypeFilters.map((option) => (
<option key={option.value} value={option.value}> <option key={option.value} value={option.value}>

View File

@@ -37,45 +37,45 @@ export function WarehouseDetailPage() {
return ( return (
<section className="space-y-4"> <section className="space-y-4">
<div className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Warehouse Detail</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Warehouse Detail</p>
<h3 className="mt-3 text-3xl font-bold text-text">{warehouse.code}</h3> <h3 className="mt-2 text-2xl font-bold text-text">{warehouse.code}</h3>
<p className="mt-2 text-base text-text">{warehouse.name}</p> <p className="mt-1 text-sm text-text">{warehouse.name}</p>
<p className="mt-3 text-sm text-muted">Last updated {new Date(warehouse.updatedAt).toLocaleString()}.</p> <p className="mt-3 text-sm text-muted">Last updated {new Date(warehouse.updatedAt).toLocaleString()}.</p>
</div> </div>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Link to="/inventory/warehouses" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text"> <Link to="/inventory/warehouses" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Back to warehouses Back to warehouses
</Link> </Link>
{canManage ? ( {canManage ? (
<Link to={`/inventory/warehouses/${warehouse.id}/edit`} className="inline-flex items-center justify-center rounded-2xl bg-brand px-5 py-3 text-sm font-semibold text-white"> <Link to={`/inventory/warehouses/${warehouse.id}/edit`} className="inline-flex items-center justify-center rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white">
Edit warehouse Edit warehouse
</Link> </Link>
) : null} ) : null}
</div> </div>
</div> </div>
</div> </div>
<div className="grid gap-4 xl:grid-cols-[minmax(0,0.85fr)_minmax(0,1.15fr)]"> <div className="grid gap-3 xl:grid-cols-[minmax(0,0.85fr)_minmax(0,1.15fr)]">
<article className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <article className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Notes</p>
<p className="mt-4 whitespace-pre-line text-sm leading-7 text-text">{warehouse.notes || "No warehouse notes recorded."}</p> <p className="mt-3 whitespace-pre-line text-sm leading-6 text-text">{warehouse.notes || "No warehouse notes recorded."}</p>
<div className="mt-8 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 text-sm text-muted"> <div className="mt-8 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 text-sm text-muted">
Created {new Date(warehouse.createdAt).toLocaleDateString()} Created {new Date(warehouse.createdAt).toLocaleDateString()}
</div> </div>
</article> </article>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Locations</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Locations</p>
<h4 className="mt-3 text-xl font-bold text-text">Stock locations</h4> <h4 className="mt-2 text-lg font-bold text-text">Stock locations</h4>
{warehouse.locations.length === 0 ? ( {warehouse.locations.length === 0 ? (
<div className="mt-6 rounded-3xl border border-dashed border-line/70 bg-page/60 px-6 py-12 text-center text-sm text-muted"> <div className="mt-6 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
No stock locations have been defined for this warehouse yet. No stock locations have been defined for this warehouse yet.
</div> </div>
) : ( ) : (
<div className="mt-6 grid gap-3 xl:grid-cols-2"> <div className="mt-6 grid gap-3 xl:grid-cols-2">
{warehouse.locations.map((location: WarehouseLocationDto) => ( {warehouse.locations.map((location: WarehouseLocationDto) => (
<article key={location.id} className="rounded-3xl border border-line/70 bg-page/60 px-4 py-4"> <article key={location.id} className="rounded-3xl border border-line/70 bg-page/60 px-2 py-2">
<div className="text-sm font-semibold text-text">{location.code}</div> <div className="text-sm font-semibold text-text">{location.code}</div>
<div className="mt-1 text-sm text-text">{location.name}</div> <div className="mt-1 text-sm text-text">{location.name}</div>
<div className="mt-2 text-xs leading-6 text-muted">{location.notes || "No notes."}</div> <div className="mt-2 text-xs leading-6 text-muted">{location.notes || "No notes."}</div>

View File

@@ -89,22 +89,22 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
return ( return (
<form className="space-y-6" onSubmit={handleSubmit}> <form className="space-y-6" onSubmit={handleSubmit}>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Warehouse Editor</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Warehouse Editor</p>
<h3 className="mt-3 text-2xl font-bold text-text">{mode === "create" ? "New Warehouse" : "Edit Warehouse"}</h3> <h3 className="mt-2 text-xl font-bold text-text">{mode === "create" ? "New Warehouse" : "Edit Warehouse"}</h3>
</div> </div>
<Link <Link
to={mode === "create" ? "/inventory/warehouses" : `/inventory/warehouses/${warehouseId}`} to={mode === "create" ? "/inventory/warehouses" : `/inventory/warehouses/${warehouseId}`}
className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text" className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"
> >
Cancel Cancel
</Link> </Link>
</div> </div>
</section> </section>
<section className="space-y-5 rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="space-y-4 rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="grid gap-4 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">Warehouse code</span> <span className="mb-2 block text-sm font-semibold text-text">Warehouse code</span>
<input value={form.code} onChange={(event) => updateField("code", event.target.value)} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" /> <input value={form.code} onChange={(event) => updateField("code", event.target.value)} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
@@ -116,53 +116,53 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
</div> </div>
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Notes</span> <span className="mb-2 block text-sm font-semibold text-text">Notes</span>
<textarea value={form.notes} onChange={(event) => updateField("notes", event.target.value)} rows={4} className="w-full rounded-3xl border border-line/70 bg-page px-4 py-3 text-text outline-none transition focus:border-brand" /> <textarea value={form.notes} onChange={(event) => updateField("notes", event.target.value)} rows={4} className="w-full rounded-3xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label> </label>
</section> </section>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Locations</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Locations</p>
<h4 className="mt-3 text-xl font-bold text-text">Internal stock locations</h4> <h4 className="mt-2 text-lg font-bold text-text">Internal stock locations</h4>
</div> </div>
<button type="button" onClick={addLocation} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text"> <button type="button" onClick={addLocation} className="inline-flex items-center justify-center rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text">
Add location Add location
</button> </button>
</div> </div>
{form.locations.length === 0 ? ( {form.locations.length === 0 ? (
<div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-6 py-10 text-center text-sm text-muted"> <div className="mt-5 rounded-3xl border border-dashed border-line/70 bg-page/60 px-4 py-8 text-center text-sm text-muted">
No locations added yet. No locations added yet.
</div> </div>
) : ( ) : (
<div className="mt-5 space-y-4"> <div className="mt-5 space-y-4">
{form.locations.map((location: WarehouseLocationInput, index: number) => ( {form.locations.map((location: WarehouseLocationInput, index: number) => (
<div key={`${location.code}-${index}`} className="rounded-3xl border border-line/70 bg-page/60 p-4"> <div key={`${location.code}-${index}`} className="rounded-3xl border border-line/70 bg-page/60 p-3">
<div className="grid gap-4 xl:grid-cols-[0.7fr_1fr_auto]"> <div className="grid gap-3 xl:grid-cols-[0.7fr_1fr_auto]">
<label className="block"> <label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Code</span> <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Code</span>
<input value={location.code} onChange={(event) => updateLocation(index, { ...location, code: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" /> <input value={location.code} onChange={(event) => updateLocation(index, { ...location, code: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label> </label>
<label className="block"> <label className="block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Name</span> <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Name</span>
<input value={location.name} onChange={(event) => updateLocation(index, { ...location, name: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" /> <input value={location.name} onChange={(event) => updateLocation(index, { ...location, name: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label> </label>
<div className="flex items-end"> <div className="flex items-end">
<button type="button" onClick={() => removeLocation(index)} className="rounded-2xl border border-rose-400/40 px-4 py-3 text-sm font-semibold text-rose-700 dark:text-rose-300"> <button type="button" onClick={() => removeLocation(index)} className="rounded-2xl border border-rose-400/40 px-2 py-2 text-sm font-semibold text-rose-700 dark:text-rose-300">
Remove Remove
</button> </button>
</div> </div>
</div> </div>
<label className="mt-4 block"> <label className="mt-4 block">
<span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Notes</span> <span className="mb-2 block text-xs font-semibold uppercase tracking-[0.16em] text-muted">Notes</span>
<input value={location.notes} onChange={(event) => updateLocation(index, { ...location, notes: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-4 py-3 text-text outline-none transition focus:border-brand" /> <input value={location.notes} onChange={(event) => updateLocation(index, { ...location, notes: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label> </label>
</div> </div>
))} ))}
</div> </div>
)} )}
<div className="mt-6 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 sm:flex-row sm:items-center sm:justify-between"> <div className="mt-6 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 sm:flex-row sm:items-center sm:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span> <span className="min-w-0 text-sm text-muted">{status}</span>
<button type="submit" disabled={isSaving} className="rounded-2xl bg-brand px-5 py-3 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"> <button type="submit" disabled={isSaving} className="rounded-2xl bg-brand px-2 py-2 text-sm font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60">
{isSaving ? "Saving..." : mode === "create" ? "Create warehouse" : "Save changes"} {isSaving ? "Saving..." : mode === "create" ? "Create warehouse" : "Save changes"}
</button> </button>
</div> </div>

View File

@@ -31,11 +31,11 @@ export function WarehousesPage() {
}, [token]); }, [token]);
return ( return (
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel 2xl:p-5">
<div className="flex flex-col gap-4 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>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Inventory</p>
<h3 className="mt-3 text-xl font-bold text-text">Warehouses</h3> <h3 className="mt-2 text-lg font-bold text-text">Warehouses</h3>
<p className="mt-2 max-w-2xl text-sm text-muted">Physical warehouse records and their internal stock locations.</p> <p className="mt-2 max-w-2xl text-sm text-muted">Physical warehouse records and their internal stock locations.</p>
</div> </div>
{canManage ? ( {canManage ? (

View File

@@ -31,15 +31,15 @@ export function LoginPage() {
return ( return (
<div className="flex min-h-screen items-center justify-center px-4 py-8"> <div className="flex min-h-screen items-center justify-center px-4 py-8">
<div className="grid w-full max-w-5xl overflow-hidden rounded-[32px] border border-line/70 bg-surface/90 shadow-panel backdrop-blur lg:grid-cols-[1.2fr_0.8fr]"> <div className="grid w-full max-w-5xl overflow-hidden rounded-[32px] border border-line/70 bg-surface/90 shadow-panel backdrop-blur lg:grid-cols-[1.2fr_0.8fr]">
<section className="bg-brand px-8 py-12 text-white md:px-12"> <section className="bg-brand px-6 py-10 text-white md:px-10">
<p className="text-xs font-semibold uppercase tracking-[0.26em] text-white/75">MRP Codex</p> <p className="text-xs font-semibold uppercase tracking-[0.26em] text-white/75">MRP Codex</p>
<h1 className="mt-6 text-4xl font-extrabold">A streamlined manufacturing operating system.</h1> <h1 className="mt-6 text-4xl font-extrabold">A streamlined manufacturing operating system.</h1>
<p className="mt-5 max-w-xl text-base text-white/82"> <p className="mt-4 max-w-xl text-sm leading-6 text-white/82">
This foundation release establishes authentication, company settings, brand theming, file persistence, and planning scaffolding. This foundation release establishes authentication, company settings, brand theming, file persistence, and planning scaffolding.
</p> </p>
</section> </section>
<section className="px-8 py-12 md:px-12"> <section className="px-6 py-10 md:px-10">
<h2 className="text-xl font-bold text-text">Sign in</h2> <h2 className="text-lg font-bold text-text">Sign in</h2>
<p className="mt-2 text-sm text-muted">Use the seeded admin account to access the initial platform shell.</p> <p className="mt-2 text-sm text-muted">Use the seeded admin account to access the initial platform shell.</p>
<form className="mt-8 space-y-5" onSubmit={handleSubmit}> <form className="mt-8 space-y-5" onSubmit={handleSubmit}>
<label className="block"> <label className="block">
@@ -59,11 +59,11 @@ export function LoginPage() {
className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand"
/> />
</label> </label>
{error ? <div className="rounded-2xl border border-red-500/30 bg-red-500/10 px-4 py-3 text-sm text-red-200 dark:text-red-200">{error}</div> : null} {error ? <div className="rounded-2xl border border-red-500/30 bg-red-500/10 px-2 py-2 text-sm text-red-200 dark:text-red-200">{error}</div> : null}
<button <button
type="submit" type="submit"
disabled={isSubmitting} disabled={isSubmitting}
className="w-full rounded-2xl bg-text px-4 py-3 text-sm font-semibold text-page transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60" className="w-full rounded-2xl bg-text px-2 py-2 text-sm font-semibold text-page transition hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-60"
> >
{isSubmitting ? "Signing in..." : "Enter workspace"} {isSubmitting ? "Signing in..." : "Enter workspace"}
</button> </button>

View File

@@ -92,7 +92,7 @@ export function CompanySettingsPage() {
}, [logoUrl]); }, [logoUrl]);
if (!form || !token) { if (!form || !token) {
return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-6 text-sm text-muted shadow-panel">{status}</div>; return <div className="rounded-[28px] border border-line/70 bg-surface/90 p-4 text-sm text-muted shadow-panel">{status}</div>;
} }
async function handleSave(event: React.FormEvent<HTMLFormElement>) { async function handleSave(event: React.FormEvent<HTMLFormElement>) {
@@ -145,11 +145,11 @@ export function CompanySettingsPage() {
return ( return (
<form className="space-y-6" onSubmit={handleSave}> <form className="space-y-6" onSubmit={handleSave}>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel backdrop-blur 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between"> <div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div> <div>
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Company Profile</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Company Profile</p>
<h3 className="mt-3 text-xl font-bold text-text">Branding and legal identity</h3> <h3 className="mt-2 text-lg font-bold text-text">Branding and legal identity</h3>
<p className="mt-2 max-w-2xl text-sm text-muted">Every internal document and PDF template will inherit its company identity from this profile.</p> <p className="mt-2 max-w-2xl text-sm text-muted">Every internal document and PDF template will inherit its company identity from this profile.</p>
</div> </div>
<div className="rounded-3xl border border-dashed border-line/70 bg-page/80 p-4"> <div className="rounded-3xl border border-dashed border-line/70 bg-page/80 p-4">
@@ -186,33 +186,33 @@ export function CompanySettingsPage() {
))} ))}
</div> </div>
</section> </section>
<section className="rounded-[28px] border border-line/70 bg-surface/90 p-6 shadow-panel backdrop-blur 2xl:p-7"> <section className="rounded-[28px] border border-line/70 bg-surface/90 p-4 shadow-panel backdrop-blur 2xl:p-5">
<p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Theme</p> <p className="text-xs font-semibold uppercase tracking-[0.24em] text-muted">Theme</p>
<div className="mt-5 grid gap-4 md:grid-cols-2 2xl:grid-cols-4"> <div className="mt-5 grid gap-4 md:grid-cols-2 2xl:grid-cols-4">
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Primary color</span> <span className="mb-2 block text-sm font-semibold text-text">Primary color</span>
<input type="color" value={form.theme.primaryColor} onChange={(event) => updateField("theme", { ...form.theme, primaryColor: event.target.value })} className="h-12 w-full rounded-2xl border border-line/70 bg-page p-2" /> <input type="color" value={form.theme.primaryColor} onChange={(event) => updateField("theme", { ...form.theme, primaryColor: event.target.value })} className="h-10 w-full rounded-2xl border border-line/70 bg-page p-2" />
</label> </label>
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Accent color</span> <span className="mb-2 block text-sm font-semibold text-text">Accent color</span>
<input type="color" value={form.theme.accentColor} onChange={(event) => updateField("theme", { ...form.theme, accentColor: event.target.value })} className="h-12 w-full rounded-2xl border border-line/70 bg-page p-2" /> <input type="color" value={form.theme.accentColor} onChange={(event) => updateField("theme", { ...form.theme, accentColor: event.target.value })} className="h-10 w-full rounded-2xl border border-line/70 bg-page p-2" />
</label> </label>
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Surface color</span> <span className="mb-2 block text-sm font-semibold text-text">Surface color</span>
<input type="color" value={form.theme.surfaceColor} onChange={(event) => updateField("theme", { ...form.theme, surfaceColor: event.target.value })} className="h-12 w-full rounded-2xl border border-line/70 bg-page p-2" /> <input type="color" value={form.theme.surfaceColor} onChange={(event) => updateField("theme", { ...form.theme, surfaceColor: event.target.value })} className="h-10 w-full rounded-2xl border border-line/70 bg-page p-2" />
</label> </label>
<label className="block"> <label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Font family</span> <span className="mb-2 block text-sm font-semibold text-text">Font family</span>
<input value={form.theme.fontFamily} onChange={(event) => updateField("theme", { ...form.theme, fontFamily: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" /> <input value={form.theme.fontFamily} onChange={(event) => updateField("theme", { ...form.theme, fontFamily: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-page px-2 py-2 text-text outline-none transition focus:border-brand" />
</label> </label>
</div> </div>
<div className="mt-5 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-4 py-4 lg:flex-row lg:items-center lg:justify-between"> <div className="mt-5 flex flex-col gap-3 rounded-2xl border border-line/70 bg-page/70 px-2 py-2 lg:flex-row lg:items-center lg:justify-between">
<span className="min-w-0 text-sm text-muted">{status}</span> <span className="min-w-0 text-sm text-muted">{status}</span>
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<button <button
type="button" type="button"
onClick={handlePdfPreview} onClick={handlePdfPreview}
className="rounded-2xl border border-line/70 px-4 py-3 text-sm font-semibold text-text" className="rounded-2xl border border-line/70 px-2 py-2 text-sm font-semibold text-text"
> >
Preview PDF Preview PDF
</button> </button>