From 740e887ce981eb4e4d288a527e35a2807d78d204 Mon Sep 17 00:00:00 2001 From: Diego Madero Islas Date: Tue, 16 Jun 2026 21:57:41 -0600 Subject: [PATCH] Split admin setup and harden instance integrity --- app/admin/setup/[section]/page.tsx | 12 + app/admin/setup/page.tsx | 2283 +--------------- app/admin/setup/setup-page.tsx | 2397 +++++++++++++++++ app/bootstrap/page.tsx | 3 +- app/entry/page.tsx | 12 +- app/globals.css | 80 + app/orders/page.tsx | 6 +- components/orders/order-form.tsx | 2 +- .../operational-onboarding-checklist.tsx | 31 +- docs/bootstrap-v2-onboarding.md | 31 +- docs/first-deploy-rehearsal.md | 4 +- docs/first-entry-and-onboarding.md | 4 +- lib/server/actions/admin-core.ts | 111 +- lib/server/actions/entry-bootstrap.ts | 62 +- lib/server/auth.ts | 6 +- lib/server/instance-state.ts | 111 + lib/server/store.ts | 10 +- tests/state-integrity.test.ts | 99 + 18 files changed, 2875 insertions(+), 2389 deletions(-) create mode 100644 app/admin/setup/[section]/page.tsx create mode 100644 app/admin/setup/setup-page.tsx create mode 100644 lib/server/instance-state.ts create mode 100644 tests/state-integrity.test.ts diff --git a/app/admin/setup/[section]/page.tsx b/app/admin/setup/[section]/page.tsx new file mode 100644 index 0000000..de94bdb --- /dev/null +++ b/app/admin/setup/[section]/page.tsx @@ -0,0 +1,12 @@ +import { AdminSetupPage, type SetupSearchParams } from '../setup-page'; + +export default async function SetupSectionPage({ + params, + searchParams, +}: { + params: Promise<{ section: string }>; + searchParams?: Promise; +}) { + const { section } = await params; + return ; +} diff --git a/app/admin/setup/page.tsx b/app/admin/setup/page.tsx index c65f3c9..a044038 100644 --- a/app/admin/setup/page.tsx +++ b/app/admin/setup/page.tsx @@ -1,2288 +1,9 @@ -import Link from 'next/link'; -import { skossCoreRoutes } from '@/lib/application-planes'; -import { - formatCurrency, - formatDateLabel, - formatUnitRate, -} from '@/lib/domain/formatters'; -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, - updateUserAction, - resetDemoWorkspaceAction, -} from '@/lib/server/actions'; -import { requireAdminPlaneAccess } from '@/lib/server/application-access'; -import { getSetupWorkspace } from '@/lib/server/demo-data'; -import { getServerTranslator } from '@/lib/i18n/server'; -import { getVisibleWorkspacesForRole } from '@/lib/workspaces'; -import { ThemeSwitcher } from '@/components/theme-switcher'; -import { OperationalOnboardingChecklist } from '@/components/setup/operational-onboarding-checklist'; -import { getRuntimeMode, isNonProductionMode } from '@/lib/server/runtime-mode'; - -const basicUnits = [ - 'g', - 'kg', - 'ml', - 'l', - 'unit', - 'piece', - 'dozen', - 'eggs', -] as const; - -type SetupSearchParams = { - section?: - | 'onboarding' - | 'business-setup' - | 'users' - | 'preferences-system' - | 'products' - | 'suppliers' - | 'raw-materials' - | 'recipes' - | 'costing' - | 'price-history'; - error?: string; - saved?: string; - supplier?: string; - material?: string; - product?: string; - productSetup?: string; - historySupplier?: string; - historyMaterial?: string; - recipe?: string; - user?: string; - costingStatus?: - | 'all' - | 'fully_costed' - | 'partially_costed' - | 'missing_cost_evidence' - | 'no_recipe'; - costingItem?: string; - importedEntity?: 'customers' | 'suppliers' | 'rawMaterials'; - importedCount?: string; - skippedCount?: string; -}; - -function buildSetupHref(params: Record) { - return { - pathname: skossCoreRoutes.adminSetup, - query: Object.fromEntries( - Object.entries(params).filter(([, value]) => Boolean(value)), - ), - }; -} - -function getProductLabel( - product: Pick, - variant?: { name: string } | null, -) { - return variant ? `${product.name} / ${variant.name}` : product.name; -} - -function getRecipeLinkLabel(recipe: Recipe, products: Product[]) { - const product = products.find((entry) => entry.id === recipe.productId); - if (!product) { - return recipe.title; - } - - const variant = recipe.productVariantId - ? (product.variants.find((entry) => entry.id === recipe.productVariantId) ?? - null) - : null; - - return getProductLabel(product, variant); -} - -function buildRecipeLineRows(recipe?: Recipe | null) { - const existing = recipe?.lines ?? []; - const blankCount = Math.max(3, 5 - existing.length); - - return [ - ...existing.map((line) => ({ ...line, isBlank: false, key: line.id })), - ...Array.from({ length: blankCount }, (_, index) => ({ - id: '', - rawMaterialId: '', - rawMaterialLabel: '', - quantity: undefined, - unit: '', - note: '', - isBlank: true, - key: `blank-${index}`, - })), - ]; -} - -function renderRequiredMark() { - return ( - - ); -} - -async function SavedMessage({ saved }: { saved?: string }) { - const { t } = await getServerTranslator(); - - if (saved === 'supplier') { - return

{t('setup.saved.supplier')}

; - } - - if (saved === 'raw-material') { - return

{t('setup.saved.rawMaterial')}

; - } - - if (saved === 'price') { - return

{t('setup.saved.price')}

; - } - - if (saved === 'recipe') { - return

{t('setup.saved.recipe')}

; - } - - if (saved === 'product') { - return

Product saved.

; - } - - if (saved === 'preferences') { - return

{t('setup.saved.preferences')}

; - } - - if (saved === 'user') { - return

{t('setup.saved.user')}

; - } - - if (saved === 'import') { - return

{t('setup.saved.import')}

; - } - - if (saved === 'demo-reset') { - return

Demo workspace reseeded successfully.

; - } - - if (saved === 'onboarding-section') { - return

Onboarding section updated.

; - } - - return null; -} +import { AdminSetupPage, type SetupSearchParams } from './setup-page'; export default async function SetupPage({ searchParams, }: { searchParams?: Promise; }) { - const userContext = await requireAdminPlaneAccess(skossCoreRoutes.adminSetup); - const [data, params, { t, locale, term }] = await Promise.all([ - getSetupWorkspace(), - searchParams, - getServerTranslator(), - ]); - const today = new Date().toISOString().slice(0, 10); - const runtimeMode = getRuntimeMode(); - const canResetDemoWorkspace = isNonProductionMode(); - const identitySetup = data.identitySetup; - const customerSetup = data.customerSetup; - const businessSetup = data.businessSetup; - const procurementSetup = data.procurementSetup; - const catalogSetup = data.catalogSetup; - - const editingSupplier = params?.supplier - ? (procurementSetup.suppliers.find((supplier) => supplier.id === params.supplier) ?? - null) - : null; - const editingMaterial = params?.material - ? (procurementSetup.rawMaterials.find((material) => material.id === params.material) ?? - null) - : null; - 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; - const editingUser = params?.user - ? (identitySetup.users.find((user) => user.id === params.user) ?? null) - : null; - const historySupplier = params?.historySupplier - ? (procurementSetup.suppliers.find( - (supplier) => supplier.id === params.historySupplier, - ) ?? null) - : null; - const historyMaterial = params?.historyMaterial - ? (procurementSetup.rawMaterials.find( - (material) => material.id === params.historyMaterial, - ) ?? null) - : null; - - const supplierFormAction = editingSupplier - ? updateSupplierAction.bind(null, editingSupplier.id) - : createSupplierAction; - 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; - const userFormAction = editingUser - ? updateUserAction.bind(null, editingUser.id) - : createUserAction; - - const supplierPriceCounts = new Map(); - const materialPriceCounts = new Map(); - for (const entry of procurementSetup.supplierPriceEntries) { - supplierPriceCounts.set( - entry.supplierId, - (supplierPriceCounts.get(entry.supplierId) ?? 0) + 1, - ); - materialPriceCounts.set( - entry.rawMaterialId, - (materialPriceCounts.get(entry.rawMaterialId) ?? 0) + 1, - ); - } - - const filteredPriceEntries = procurementSetup.supplierPriceEntries.filter((entry) => { - if (historySupplier && entry.supplierId !== historySupplier.id) { - return false; - } - - if (historyMaterial && entry.rawMaterialId !== historyMaterial.id) { - return false; - } - - return true; - }); - - const supplierHistoryHref = (supplierId: string) => - buildSetupHref({ - historySupplier: supplierId, - historyMaterial: historyMaterial?.id, - supplier: editingSupplier?.id, - material: editingMaterial?.id, - recipe: editingRecipe?.id, - }); - - const materialHistoryHref = (materialId: string) => - buildSetupHref({ - historySupplier: historySupplier?.id, - historyMaterial: materialId, - supplier: editingSupplier?.id, - material: editingMaterial?.id, - recipe: editingRecipe?.id, - }); - - const recipeLineRows = buildRecipeLineRows(editingRecipe); - const recipeProductId = editingRecipe?.productId ?? selectedProduct?.id ?? ''; - const editingRecipeCost = editingRecipe - ? (catalogSetup.recipeCostById.get(editingRecipe.id) ?? null) - : null; - const activeRecipes = catalogSetup.recipes.filter((recipe) => recipe.active).length; - const linkedProductCount = new Set( - catalogSetup.recipes.map((recipe) => recipe.productVariantId ?? recipe.productId), - ).size; - const recipesByProduct = new Map(); - for (const recipe of catalogSetup.recipes) { - const existingRecipes = recipesByProduct.get(recipe.productId) ?? []; - recipesByProduct.set(recipe.productId, [...existingRecipes, recipe]); - } - const costingItems = buildCostingSnapshotItems( - catalogSetup.products, - catalogSetup.recipes, - catalogSetup.recipeCostById, - ); - const costingStatusFilter = params?.costingStatus ?? 'all'; - const filteredCostingItems = costingItems.filter( - (item) => - costingStatusFilter === 'all' || item.status === costingStatusFilter, - ); - const selectedCostingItem = - (params?.costingItem - ? (filteredCostingItems.find((item) => item.id === params.costingItem) ?? - costingItems.find((item) => item.id === params.costingItem) ?? - null) - : null) ?? - filteredCostingItems[0] ?? - costingItems[0] ?? - null; - const costingSummary = { - fullyCosted: costingItems.filter((item) => item.status === 'fully_costed') - .length, - partiallyCosted: costingItems.filter( - (item) => item.status === 'partially_costed', - ).length, - missingEvidence: costingItems.filter( - (item) => item.status === 'missing_cost_evidence', - ).length, - noRecipe: costingItems.filter((item) => item.status === 'no_recipe').length, - }; - const buildCostingHref = (overrides: Record) => - buildSetupHref({ - supplier: editingSupplier?.id, - material: editingMaterial?.id, - historySupplier: historySupplier?.id, - historyMaterial: historyMaterial?.id, - recipe: editingRecipe?.id, - product: selectedProduct?.id, - costingStatus: costingStatusFilter, - costingItem: selectedCostingItem?.id, - ...overrides, - }); - const importedCount = Number(params?.importedCount ?? 0); - const skippedCount = Number(params?.skippedCount ?? 0); - const hasImportFeedback = - params?.saved === 'import' && params?.importedEntity; - const setupSection = params?.section; - const sectionNavItems: Array<{ - key: - | 'onboarding' - | 'business-setup' - | 'users' - | 'preferences-system' - | 'products' - | 'suppliers' - | 'raw-materials' - | 'recipes' - | 'costing' - | 'price-history'; - label: string; - }> = [ - { key: 'onboarding', label: 'Onboarding' }, - { 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') }, - { key: 'costing', label: t('setup.sections.costing') }, - { key: 'price-history', label: t('setup.sections.priceHistory') }, - ]; - const buildSectionLink = (section: (typeof sectionNavItems)[number]['key']) => ({ - pathname: skossCoreRoutes.adminSetup, - query: { section }, - hash: section, - }); - - return ( -
- - {basicUnits.map((unit) => ( - - -
-
-

{t('setup.workspace')}

-

{t('setup.title')}

-

{t('setup.description')}

-
-
- - - {params?.error ?

{params.error}

: null} - {hasImportFeedback ? ( -

- {t('setup.import.result', { - entity: t(`setup.import.entityLabels.${params.importedEntity}`), - imported: Number.isFinite(importedCount) ? importedCount : 0, - skipped: Number.isFinite(skippedCount) ? skippedCount : 0, - })} -

- ) : null} - - {canResetDemoWorkspace ? ( -
-
-
-

Local demo data safety

-

- Runtime mode: {runtimeMode}. Use reset only for local/pilot testing so real records stay separate. -

-
-
-
- -
-
- ) : null} - -
-
-
-

{t('setup.title')}

-

{t('setup.adminReadinessHelp')}

-
-
-
- {sectionNavItems.map((item) => ( - - {item.label} - - ))} -
-
- - - -
-
-
-
-

{t('setup.customerMemoryTitle')}

-

{t('setup.customerMemoryHelp')}

-
- - {customerSetup.customers.length} {t('common.customers')} - -
- {customerSetup.customers.length > 0 ? ( -
    - {customerSetup.customers.slice(0, 6).map((customer) => ( -
  • -
    - {customer.displayName} - - {customer.phone ?? - customer.email ?? - t('customers.noContactYet')} - -
    -
    - - {t('setup.actions.openCustomer')} - - - {t('setup.actions.newOrder')} - -
    -
  • - ))} -
- ) : ( -

{t('setup.customerMemoryEmpty')}

- )} -
- - {t('setup.actions.manageCustomers')} - - - {t('setup.actions.addCustomer')} - -
-
- - -
- - {!userContext.canManageSettings ? ( -

{t('setup.roleShapingNote')}

- ) : null} - -
-
-
-
-

{t('setup.sections.preferencesSystem')}

-

{t('setup.preferencesAreaBody')}

-
-
- - {t('setup.openPreferences')} - -
- -
-
-
-

{t('setup.appearance')}

-

{t('setup.appearanceHelp')}

-
- - {userContext.currentUser - ? userContext.currentUser.displayName - : t('nav.preferences')} - -
- -
-
- -
-
- {t('setup.activeSuppliers')} - - {procurementSetup.suppliers.filter((supplier) => supplier.active).length} - - {t('setup.activeSuppliersHelp')} -
-
- {t('setup.rawMaterials')} - {procurementSetup.rawMaterials.length} - {t('setup.rawMaterialsHelp')} -
-
- {t('setup.recordedPrices')} - {procurementSetup.supplierPriceEntries.length} - {t('setup.recordedPricesHelp')} -
-
- {t('setup.recipes')} - {activeRecipes} - {t('setup.recipesHelp')} -
-
- -
-
-
-
-

Products for order capture

-

Confirm the basic sellable items your team needs for first orders. Recipes, costing, inventory, and procurement can come later.

-
- - {catalogSetup.products.filter((product) => product.active).length}/{catalogSetup.products.length} active - -
-

- 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.{' '} - - {t('setup.openOrderCapture')} - -

-
-
    - {catalogSetup.products.map((product) => ( -
  • -
    - - {product.name} - {!product.active ? ` · ${t('common.inactive').toLowerCase()}` : ''} - - - {[product.category, `${product.defaultUnit} default unit`].filter(Boolean).join(' · ')} - - - {product.variants.length} variant{product.variants.length === 1 ? '' : 's'} · {(recipesByProduct.get(product.id) ?? []).length} {t('common.recipes')} - -
    -
    - - {t('setup.actions.edit')} - - - {t('setup.actions.addRecipe')} - -
    -
  • - ))} -
-
- -
-
-

{editingProduct ? 'Edit product' : 'Add product'}

-

Keep this to the name and unit needed for first orders.

-
- {editingProduct ? ( - - {t('setup.actions.cancelEdit')} - - ) : null} -
-
- - - - -
- -
-
-
- -
-
-
-

{t('setup.destinations')}

-

{t('setup.destinationsHelp')}

-
- - {businessSetup.destinations.length} {term('destination', 'many')} - -
-

- {t('setup.destinationsManagedFromOrders')}{' '} - - {t('setup.openOrderCapture')} - -

-
    - {businessSetup.destinations.map((destination) => ( -
  • - {destination.name} - {destination.kind} -
  • - ))} -
-
-
- -
-
-
-
-

{t('setup.users')}

-

{t('setup.usersHelp')}

-
- - {identitySetup.users.length} {t('common.users')} - -
-
- - {identitySetup.users.filter((user) => user.active).length}{' '} - {t('setup.labels.activeUsers')} - - - {identitySetup.users.filter((user) => !user.active).length}{' '} - {t('setup.labels.inactiveUsers')} - - - {new Set(identitySetup.users.flatMap((user) => user.roles ?? [user.role])).size}{' '} - {t('setup.labels.rolesInUse')} - - - {t('setup.openPreferences')} - -
-
-
- {identitySetup.users.length > 0 ? ( -
    - {identitySetup.users.map((user) => ( -
  • -
    - - {user.displayName} - {!user.active - ? ` · ${t('common.inactive').toLowerCase()}` - : ''} - - - {(user.roles?.length ? user.roles : [user.role]).map((role) => t(`roles.${role}.label`)).join(' · ')} ·{' '} - {user.loginIdentifier} - - - {t('setup.labels.defaultWorkspace')}:{' '} - {t( - `nav.${user.preferences?.defaultWorkspace ?? user.defaultWorkspace}`, - )} - - - {t('setup.labels.visibleWorkspaces')}:{' '} - {getVisibleWorkspacesForRole((user.roles?.[0] ?? user.role)) - .map((workspace) => t(`nav.${workspace}`)) - .join(' · ')} - - - {t('setup.labels.password')}:{' '} - {user.mustChangePassword - ? t('setup.labels.passwordNeedsReset') - : t('setup.labels.passwordReady')} - -
    -
    - - {t('setup.actions.edit')} - -
    -
  • - ))} -
- ) : ( -

{t('setup.usersEmpty')}

- )} -
-
-
-
-

- {editingUser ? t('setup.editUser') : t('setup.addUser')} -

-

- {editingUser - ? t('setup.editUserHelp') - : t('setup.addUserHelp')} -

-
- {editingUser ? ( - - {t('setup.actions.cancelEdit')} - - ) : null} -
-
- - -
-
- - -
-
- - -
-
- - {editingUser ? ( - - ) :
} -
- - - -
-
-
- -
-
-
-

{t('setup.sections.catalogData')}

-

{t('setup.rawMaterialsHelp')}

-
-
-
-
-
-
-

{t('setup.suppliers')}

-

{t('setup.suppliersHelp')}

-
-
- - {procurementSetup.suppliers.length} {t('common.suppliers')} - - - {t('setup.actions.importSuppliersCsv')} - -
-
-
- {procurementSetup.suppliers.length > 0 ? ( -
    - {procurementSetup.suppliers.map((supplier) => ( -
  • -
    - - {supplier.name} - {!supplier.active - ? ` · ${t('common.inactive').toLowerCase()}` - : ''} - - - {supplier.contact ?? - supplier.notes ?? - t('setup.noExtraContactYet')} - - - {supplierPriceCounts.get(supplier.id) ?? 0}{' '} - {t('setup.labels.priceEntries')} - -
    -
    - - {t('setup.actions.edit')} - - - {t('setup.actions.viewHistory')} - -
    -
  • - ))} -
- ) : ( -

{t('setup.suppliersEmpty')}

- )} -
-
-
-

- {editingSupplier - ? t('setup.editSupplier') - : t('setup.addSupplier')} -

-

- {editingSupplier - ? t('setup.editSupplierHelp') - : t('setup.addSupplierHelp')} -

-
- {editingSupplier ? ( - - {t('setup.actions.cancelEdit')} - - ) : null} -
-
- - -
-