Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 96 additions & 39 deletions app/admin/setup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import {
import type { Product, Recipe } from '@/lib/domain/types';
import { buildCostingSnapshotItems } from '@/lib/domain/recipe-costing';
import {
createProductAction,
createRawMaterialAction,
createRecipeAction,
createSupplierAction,
createSupplierPriceEntryAction,
createUserAction,
updateProductAction,
updateRawMaterialAction,
updateRecipeAction,
updateSupplierAction,
Expand Down Expand Up @@ -42,6 +44,7 @@ type SetupSearchParams = {
| 'business-setup'
| 'users'
| 'preferences-system'
| 'products'
| 'suppliers'
| 'raw-materials'
| 'recipes'
Expand All @@ -52,6 +55,7 @@ type SetupSearchParams = {
supplier?: string;
material?: string;
product?: string;
productSetup?: string;
historySupplier?: string;
historyMaterial?: string;
recipe?: string;
Expand Down Expand Up @@ -144,6 +148,10 @@ async function SavedMessage({ saved }: { saved?: string }) {
return <p className="inline-success">{t('setup.saved.recipe')}</p>;
}

if (saved === 'product') {
return <p className="inline-success">Product saved.</p>;
}

if (saved === 'preferences') {
return <p className="inline-success">{t('setup.saved.preferences')}</p>;
}
Expand Down Expand Up @@ -194,6 +202,9 @@ export default async function SetupPage({
const selectedProduct = params?.product
? (catalogSetup.products.find((product) => product.id === params.product) ?? null)
: null;
const editingProduct = params?.productSetup
? (catalogSetup.products.find((product) => product.id === params.productSetup) ?? null)
: null;
const editingRecipe = params?.recipe
? (catalogSetup.recipes.find((recipe) => recipe.id === params.recipe) ?? null)
: null;
Expand All @@ -217,6 +228,9 @@ export default async function SetupPage({
const rawMaterialFormAction = editingMaterial
? updateRawMaterialAction.bind(null, editingMaterial.id)
: createRawMaterialAction;
const productFormAction = editingProduct
? updateProductAction.bind(null, editingProduct.id)
: createProductAction;
const recipeFormAction = editingRecipe
? updateRecipeAction.bind(null, editingRecipe.id)
: createRecipeAction;
Expand Down Expand Up @@ -333,6 +347,7 @@ export default async function SetupPage({
| 'business-setup'
| 'users'
| 'preferences-system'
| 'products'
| 'suppliers'
| 'raw-materials'
| 'recipes'
Expand All @@ -343,6 +358,7 @@ export default async function SetupPage({
{ key: 'business-setup', label: t('setup.sections.businessSetup') },
{ key: 'users', label: t('setup.sections.users') },
{ key: 'preferences-system', label: t('setup.sections.preferencesSystem') },
{ key: 'products', label: 'Products' },
{ key: 'suppliers', label: t('setup.sections.suppliers') },
{ key: 'raw-materials', label: t('setup.sections.rawMaterials') },
{ key: 'recipes', label: t('setup.sections.recipes') },
Expand Down Expand Up @@ -597,68 +613,109 @@ export default async function SetupPage({
</section>

<section className="grid-two">
<article className="panel page-stack">
<article className="panel page-stack" id="products">
<div className="table-header-row">
<div>
<h2>{t('setup.productsAndVariants')}</h2>
<p>{t('setup.productsAndVariantsHelp')}</p>
<h2>Products for order capture</h2>
<p>Confirm the basic sellable items your team needs for first orders. Recipes, costing, inventory, and procurement can come later.</p>
</div>
<span className="summary-pill">
{catalogSetup.products.length} {t('common.products')}
{catalogSetup.products.filter((product) => product.active).length}/{catalogSetup.products.length} active
</span>
</div>
<p className="helper-text no-margin">
{t('setup.managedFromOrders')}{' '}
Confirmed products appear as suggestions during order capture. Operators can still type draft product names when real work needs to move before setup is complete.{' '}
<Link href="/orders/new" className="inline-link">
{t('setup.openOrderCapture')}
</Link>
</p>
<ul className="stack-list">
{catalogSetup.products.map((product) => (
<li key={product.id} className="list-with-actions">
<div>
<strong>{product.name}</strong>
<span>
{product.variants.map((variant) => variant.name).join(', ')}
</span>
<span className="inline-meta">
{(recipesByProduct.get(product.id) ?? []).length}{' '}
{t('common.recipes')}
</span>
</div>
<div className="inline-action-row">
<Link
href={buildSetupHref({
product: product.id,
supplier: editingSupplier?.id,
material: editingMaterial?.id,
historySupplier: historySupplier?.id,
historyMaterial: historyMaterial?.id,
recipe: undefined,
})}
className="inline-link"
>
{t('setup.actions.addRecipe')}
</Link>
{(recipesByProduct.get(product.id) ?? [])[0] ? (
<div className="admin-split-layout">
<ul className="stack-list compact-list">
{catalogSetup.products.map((product) => (
<li key={product.id} className={`list-with-actions ${editingProduct?.id === product.id ? 'is-selected-row' : ''}`}>
<div>
<strong>
{product.name}
{!product.active ? ` · ${t('common.inactive').toLowerCase()}` : ''}
</strong>
<span>
{[product.category, `${product.defaultUnit} default unit`].filter(Boolean).join(' · ')}
</span>
<span className="inline-meta">
{product.variants.length} variant{product.variants.length === 1 ? '' : 's'} · {(recipesByProduct.get(product.id) ?? []).length} {t('common.recipes')}
</span>
</div>
<div className="inline-action-row">
<Link
href={buildSetupHref({
productSetup: product.id,
product: selectedProduct?.id,
supplier: editingSupplier?.id,
material: editingMaterial?.id,
historySupplier: historySupplier?.id,
historyMaterial: historyMaterial?.id,
recipe: editingRecipe?.id,
})}
className="inline-link"
>
{t('setup.actions.edit')}
</Link>
<Link
href={buildSetupHref({
recipe: (recipesByProduct.get(product.id) ?? [])[0]?.id,
product: product.id,
supplier: editingSupplier?.id,
material: editingMaterial?.id,
historySupplier: historySupplier?.id,
historyMaterial: historyMaterial?.id,
recipe: undefined,
})}
className="inline-link"
>
{t('setup.actions.editRecipe')}
{t('setup.actions.addRecipe')}
</Link>
) : null}
</div>
</li>
))}
</ul>
<form action={productFormAction} className="field-section page-stack">
<input type="hidden" name="redirectTo" value="/admin/setup" />
<div className="field-section-header">
<div>
<h3>{editingProduct ? 'Edit product' : 'Add product'}</h3>
<p className="helper-text">Keep this to the name and unit needed for first orders.</p>
</div>
</li>
))}
</ul>
{editingProduct ? (
<Link href={buildSetupHref({ product: selectedProduct?.id })} className="button-secondary compact-button">
{t('setup.actions.cancelEdit')}
</Link>
) : null}
</div>
<div className="grid-two">
<label>
<span className="field-heading">Product name {renderRequiredMark()}</span>
<input name="name" defaultValue={editingProduct?.name ?? ''} placeholder="Sourdough loaf" required />
</label>
<label>
<span className="field-heading">Default unit {renderRequiredMark()}</span>
<input name="defaultUnit" list="basic-unit-options" defaultValue={editingProduct?.defaultUnit ?? 'piece'} required />
</label>
<label>
<span className="field-heading">Category <span className="optional-pill">{t('common.optional')}</span></span>
<input name="category" defaultValue={editingProduct?.category ?? ''} placeholder="Bread, pastry, prepared food" />
</label>
<label className="checkbox-row">
<input name="active" type="checkbox" defaultChecked={editingProduct?.active ?? true} />
<span>
<strong>Active for order suggestions</strong>
<span className="helper-text">Inactive products stay saved but stop appearing as active order suggestions.</span>
</span>
</label>
</div>
<button type="submit" className="button-primary">
{editingProduct ? 'Update product' : 'Save product'}
</button>
</form>
</div>
</article>

<article className="panel page-stack">
Expand Down
4 changes: 4 additions & 0 deletions app/orders/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ export default async function OrdersPage({
<SparklesIcon className="button-icon" />
<span>{t('orders.newRecurringTemplate')}</span>
</Link>
<Link href="/admin/setup?section=products#products" className="button-secondary">
<span>Confirm products</span>
</Link>
<Link href="/orders/new" className="button-primary">
<OrdersIcon className="button-icon" />
<span>{t('orders.newOrder')}</span>
Expand Down Expand Up @@ -199,6 +202,7 @@ export default async function OrdersPage({
<div className="inline-action-row top-gap-small">
<Link href="/orders/new" className="button-primary compact-button">Create first order</Link>
<Link href="/customers" className="button-secondary compact-button">Confirm customers</Link>
<Link href="/admin/setup?section=products#products" className="button-secondary compact-button">Confirm products</Link>
</div>
</div>
</section>
Expand Down
1 change: 1 addition & 0 deletions components/orders/order-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,7 @@ export function OrderForm({
<p className="helper-text">{t('orders.orderForm.sections.linesHelp')}</p>
</div>
</div>
<p className="helper-text no-margin">Confirmed products appear as suggestions, but you can still type a draft item when setup is incomplete. <Link href="/admin/setup?section=products#products" className="inline-link">Confirm products</Link>.</p>
<LineItemsEditor
editorId="order-lines-editor"
initialLines={lineDrafts}
Expand Down
12 changes: 7 additions & 5 deletions docs/mwp-readiness-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,12 @@ The main blockers are readiness and clarity issues rather than missing infrastru
- Empty operational states did not always tell a first-time user what to do next.
- Runtime/demo state was clearer on `/entry` and `/admin/setup` than on the operator home.
- Production and handoff could look empty or broken before the first order exists.
- Product setup is still indirect. Operators can type draft products in orders and create a starter product in bootstrap, but dedicated product confirmation remains thin.
- Product setup was indirect. This is now partially addressed by a narrow `/admin/setup` product confirmation section for product name, default unit, category, and active state.
- The app still contains demo-seed-oriented copy and example setup surfaces that could be mistaken for real operational readiness.

## Rough edges

- `/bootstrap` has an optional starter product step, but product/catalog management after launch is still mostly implied through order capture and setup recipe links.
- `/bootstrap` has an optional starter product step, and `/admin/setup` now has a narrow product confirmation section. This is still intentionally basic and not a full catalog workflow.
- `/orders` showed an empty recurring-demand grid even when recurring templates did not exist.
- `/production` had good empty handling when there were no boards, but not enough guidance when a board date exists with no visible orders.
- `/handoff` showed several empty lists without explaining what makes items appear.
Expand All @@ -47,13 +47,13 @@ The main blockers are readiness and clarity issues rather than missing infrastru
- Confirm production runtime configuration: explicitly set and verify `SKOSS_RUNTIME_MODE` for the target environment.
- Ensure a fresh instance does not ship with demo records unless the deploy is explicitly a demo/pilot training workspace.
- Confirm backup/restore behavior for the chosen persistence adapter and document the manual recovery path.
- Add a product/basic catalog confirmation path or make the draft-product flow intentionally documented as the MWP path.
- Run the new product confirmation path with real starter products and verify order suggestions are helpful without blocking draft product entry.
- Validate login/session behavior on the actual target host with at least owner admin and one operator role.
- Run a real scenario: create customer, create order, update production quantities, record WIP, record handoff, print/review output.

## Nice-to-have after first deploy

- Dedicated basic product management outside procurement/recipe setup.
- Dedicated product import and richer catalog editing after the basic confirmation path proves useful.
- Better guided first-run checklist on the operator home after bootstrap.
- More complete product/catalog import or quick-add affordances.
- More precise role onboarding for sales, kitchen, and shift lead users.
Expand All @@ -75,11 +75,13 @@ Do not include these in the first deploy readiness scope:

## Stabilization applied in this pass

- Added a narrow product confirmation path in `/admin/setup` for MWP product name, default unit, optional category, and active state.
- Kept draft product order lines available so catalog setup does not block real work.
- Added operator-home runtime/demo visibility and a first-deploy workflow card.
- Added empty-state guidance to `/orders` for missing recurring templates and missing first orders.
- Added production entry links and clearer empty guidance for production boards with no visible orders.
- Added handoff entry links and empty-list guidance for WIP, packing, assignment, pickup, and focus-day order watches.

## Recommended next PR

Add a narrow basic product/catalog confirmation path for the MWP. Keep it separate from recipes, costing, procurement, and inventory: just enough to confirm product names, default units, active state, and variants so first deploy users are not forced to rely only on draft product text in order lines.
Run a first-deploy rehearsal against a fresh non-demo instance: bootstrap, create admin, confirm products/customers, create orders, update production, record WIP, record handoff, and verify browser-print output. Keep the rehearsal focused on operational confidence before adding richer catalog, procurement, inventory, or deployment automation.
2 changes: 1 addition & 1 deletion lib/domain/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export interface AuditFields {
updatedByUserId?: string;
}

export type ActivityEntityType = 'order' | 'customer' | 'supplier' | 'raw_material' | 'recipe' | 'user';
export type ActivityEntityType = 'order' | 'customer' | 'product' | 'supplier' | 'raw_material' | 'recipe' | 'user';
export type ActivityActionType = 'created' | 'updated' | 'deleted' | 'status_changed';

export interface ActivityEntry {
Expand Down
Loading
Loading