This commit is contained in:
2026-03-15 19:40:35 -05:00
parent 275c73b584
commit dcac4f135d
17 changed files with 659 additions and 318 deletions
+5 -4
View File
@@ -28,8 +28,8 @@ MRP Codex is a modular Manufacturing Resource Planning platform intended to be a
- pegged work-order and purchase-order supply coverage tied back to sales demand, with preferred-vendor sourcing defaults
- shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing
- admin diagnostics with runtime footprint, record counts, and persisted audit-trail visibility
- admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation
- safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows
- admin user management with account creation, activation, role assignment, role-permission editing, session visibility/revocation, and review filtering
- safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, projects, warehouse/form editors, and attachment workflows
- CRM/shipping audit coverage and startup validation surfaced through the admin diagnostics workflow
- backup/restore guidance, richer startup diagnostics, and exportable support bundles in the admin diagnostics workflow
- backup verification checklist and restore-drill runbook in the admin diagnostics workflow
@@ -46,6 +46,7 @@ Read these before major work:
- [INSTRUCTIONS.md](D:/CODING/mrp-codex/INSTRUCTIONS.md)
- [STRUCTURE.md](D:/CODING/mrp-codex/STRUCTURE.md)
- [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md)
- [SHIPPED.md](D:/CODING/mrp-codex/SHIPPED.md)
- [UNRAID.md](D:/CODING/mrp-codex/UNRAID.md)
If implementation changes invalidate those docs, update them in the same change set. Keep `CHANGELOG.md` current for shipped features, behavior changes, and notable operational updates.
@@ -130,8 +131,8 @@ If implementation changes invalidate those docs, update them in the same change
Near-term priorities are:
1. Deeper session history, filtering, and admin-side access review polish
2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows
1. Support-log filtering, retention controls, and broader support-package polish
2. Revision comparison UX for changed sales and purchasing documents
When adding new modules, preserve the ability to extend the system without refactoring the existing app shell.
+6 -1
View File
@@ -6,9 +6,12 @@ This file is the running release and change log for MRP Codex. Keep it updated w
### Added
- Session review cues on admin auth sessions, including flagged stale activity, multi-session counts, and multi-IP warnings
- Session filters and text search for admin-side access review across user, email, IP, user agent, and review reasons
- Shared destructive-action confirmation dialog with impact and recovery guidance for high-risk operational actions
- Typed confirmation for sensitive admin actions such as account deactivation, current-session revocation, and terminal manufacturing/inventory postings
- Destructive-action confirmation and recovery coverage for sales approvals, quote conversion, purchase receiving, purchase status changes, and shipment status changes
- Destructive-action confirmation coverage for project customer/document unlinking and embedded form-row removals in sales, purchasing, inventory, and warehouse editors
- Persisted auth-session tracking with admin visibility into active, expired, and revoked sign-ins
- Admin-side session revocation controls plus server-side logout that invalidates the current JWT-backed session
- Shared shortage and readiness rollups across dashboard, planning, project detail, purchasing detail, and manufacturing detail
@@ -51,7 +54,9 @@ This file is the running release and change log for MRP Codex. Keep it updated w
### Changed
- Admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows now use explicit destructive-action confirmation and recovery messaging instead of immediate irreversible clicks
- `ROADMAP.md` now tracks remaining work only, and shipped phase history now lives in `SHIPPED.md`
- Admin diagnostics now summarizes sessions that need review, and startup now prunes old expired or revoked auth-session records
- Admin, sales, purchasing, shipping, inventory, manufacturing, project, warehouse, and attachment workflows now use explicit destructive-action confirmation and recovery messaging instead of immediate irreversible clicks
- Admin operations now combine user management with live session visibility so operators can inspect and revoke sign-ins without changing user records
- JWT authentication now validates against persisted session records and inactive users lose access immediately instead of waiting for token expiry
- The dashboard now treats Projects as a live first-class module alongside CRM, inventory, sales, and shipping
+5 -5
View File
@@ -3,7 +3,7 @@
## Documentation maintenance
- Keep [CHANGELOG.md](D:/CODING/mrp-codex/CHANGELOG.md) updated whenever shipped functionality, architecture expectations, deployment behavior, or user-facing workflows materially change.
- If a change invalidates [README.md](D:/CODING/mrp-codex/README.md), [STRUCTURE.md](D:/CODING/mrp-codex/STRUCTURE.md), [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md), or [UNRAID.md](D:/CODING/mrp-codex/UNRAID.md), update those files in the same change set.
- If a change invalidates [README.md](D:/CODING/mrp-codex/README.md), [STRUCTURE.md](D:/CODING/mrp-codex/STRUCTURE.md), [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md), [SHIPPED.md](D:/CODING/mrp-codex/SHIPPED.md), or [UNRAID.md](D:/CODING/mrp-codex/UNRAID.md), update those files in the same change set.
## Current milestone
@@ -32,8 +32,8 @@ This repository implements the platform foundation milestone:
- pegged work-order and purchase-order supply coverage tied back to sales demand, with preferred-vendor sourcing defaults
- shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing
- admin diagnostics with runtime footprint, storage visibility, record counts, and recent audit activity
- admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation
- safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows
- admin user management with account creation, activation, role assignment, role-permission editing, session visibility/revocation, and review filtering
- safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, projects, warehouse/form editors, and attachment workflows
- CRM/shipping audit coverage and startup validation surfaced through diagnostics
- backup/restore guidance, richer startup diagnostics, and exportable support bundles in diagnostics
- backup verification checklist and restore-drill runbook in diagnostics
@@ -73,5 +73,5 @@ This repository implements the platform foundation milestone:
## Next roadmap candidates
- deeper session history, filtering, and admin-side access review polish
- extend destructive-action safety coverage into remaining project and form-edit removal workflows
- support-log filtering, retention controls, and broader support-package polish
- revision comparison UX for changed sales and purchasing documents
+14 -9
View File
@@ -5,7 +5,7 @@ Foundation release for a modular Manufacturing Resource Planning platform built
## Documentation Maintenance
- Keep [CHANGELOG.md](D:/CODING/mrp-codex/CHANGELOG.md) updated for shipped features, workflow changes, and notable operational updates.
- Keep [README.md](D:/CODING/mrp-codex/README.md), [INSTRUCTIONS.md](D:/CODING/mrp-codex/INSTRUCTIONS.md), [STRUCTURE.md](D:/CODING/mrp-codex/STRUCTURE.md), [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md), and [UNRAID.md](D:/CODING/mrp-codex/UNRAID.md) aligned when changes affect their scope.
- Keep [README.md](D:/CODING/mrp-codex/README.md), [INSTRUCTIONS.md](D:/CODING/mrp-codex/INSTRUCTIONS.md), [STRUCTURE.md](D:/CODING/mrp-codex/STRUCTURE.md), [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md), [SHIPPED.md](D:/CODING/mrp-codex/SHIPPED.md), and [UNRAID.md](D:/CODING/mrp-codex/UNRAID.md) aligned when changes affect their scope.
Current foundation scope includes:
@@ -31,8 +31,8 @@ Current foundation scope includes:
- pegged WO/PO supply tracking back to sales demand with preferred-vendor sourcing on inventory items
- shared shortage and readiness rollups across dashboard, planning, projects, purchasing, and manufacturing
- admin diagnostics with runtime footprint, record counts, and recent audit-trail visibility
- admin user management with account creation, activation, role assignment, role-permission editing, and session visibility/revocation
- safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows
- admin user management with account creation, activation, role assignment, role-permission editing, session visibility/revocation, review filtering, and unusual-access cues
- safer destructive-action confirmations and recovery messaging across admin, sales, purchasing, shipping, inventory, manufacturing, projects, warehouse/form editors, and attachment workflows
- CRM and shipping audit coverage plus startup validation surfaced through the admin diagnostics page
- backup/restore guidance, richer startup diagnostics, and exportable support bundles in the admin diagnostics workflow
- backup verification checklist and restore-drill runbook surfaced in admin diagnostics
@@ -42,6 +42,8 @@ Current foundation scope includes:
## Product Map
Shipped phase history now lives in [SHIPPED.md](D:/CODING/mrp-codex/SHIPPED.md). [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md) now tracks remaining work only.
Current completed foundation areas:
- dashboard foundation
@@ -58,14 +60,14 @@ Current completed foundation areas:
Near-term priorities:
1. Deeper session history, filtering, and admin-side access review polish
2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows
1. Support-log filtering, retention controls, and broader support-package polish
2. Revision comparison UX for changed sales and purchasing documents
Revisit / deferred items:
- local Windows Prisma migration reliability
- deeper session history, filtering, and admin-side access review polish
- safer destructive-action confirmations and recovery messaging
- support-log filtering, retention controls, and broader support-package polish
- revision comparison UX for changed sales and purchasing documents
Dashboard direction:
@@ -354,6 +356,7 @@ As of March 15, 2026, the latest committed domain migrations include:
- inventory transfers and reservations
- audit trail and diagnostics foundation
- auth-session visibility and revocation
- session review filters, unusual-access cues, and startup pruning of stale expired/revoked session records
- supply pegging and preferred-vendor sourcing
Recent roadmap-driving migrations should always be applied before validating new CRM, inventory, sales, shipping, or purchasing features in a running environment.
@@ -368,8 +371,10 @@ The current admin operations slice supports:
- prefilled work-order and purchase-order draft launch paths from sales-order demand-planning recommendations
- shared shortage/readiness rollups across planning, project, purchasing, dashboard, and manufacturing views
- a dedicated user-management page for account creation, activation, role assignment, password reset-style updates, role-permission administration, and session visibility/revocation
- session review filters and flagged cues for stale activity, multi-session overlap, and multi-IP access patterns
- CRM customer/vendor changes and shipping mutations now flow into the shared audit trail
- startup validation now checks storage paths, writable storage readiness, database connectivity, client bundle readiness, Chromium availability, and risky production defaults during server boot
- startup now prunes stale expired or revoked auth-session records before serving requests
- backup and restore guidance now surfaces directly in diagnostics, along with exportable support bundles for support handoff
- support logs now capture startup warnings, HTTP failures, and server errors for admin-side debugging review
- backup verification items and restore-drill expected outcomes now live in the admin runbook surface
@@ -377,8 +382,8 @@ The current admin operations slice supports:
Current follow-up direction:
- deeper session history, filtering, and admin-side access review polish
- extend destructive-action safety coverage into remaining project and form-edit removal workflows
- support-log filtering, retention controls, and broader support-package polish
- revision comparison UX for changed sales and purchasing documents
## UI Notes
+46 -235
View File
@@ -3,193 +3,83 @@
## Documentation maintenance
- Keep [CHANGELOG.md](D:/CODING/mrp-codex/CHANGELOG.md) updated alongside roadmap-driving feature completion, priority shifts, and notable delivery milestones.
- Keep [SHIPPED.md](D:/CODING/mrp-codex/SHIPPED.md) updated when roadmap items move from planned to delivered.
- When roadmap changes affect implementation guidance or deployment expectations, update the companion docs in [README.md](D:/CODING/mrp-codex/README.md), [INSTRUCTIONS.md](D:/CODING/mrp-codex/INSTRUCTIONS.md), [STRUCTURE.md](D:/CODING/mrp-codex/STRUCTURE.md), and [UNRAID.md](D:/CODING/mrp-codex/UNRAID.md) in the same change set.
## Product direction
MRP Codex is being built as a streamlined, modular manufacturing resource planning platform with strong branding controls, fast operational workflows, and a single-container deployment model that is simple to back up and upgrade.
## Current status
This file tracks work that still needs to be completed. Shipped phase history and completed slices now live in [SHIPPED.md](D:/CODING/mrp-codex/SHIPPED.md).
### Completed: Foundation release
## Near-term priority order
- Monorepo-style workspace with `client`, `server`, and `shared`
- React + Vite + Tailwind frontend shell
- Express + TypeScript backend shell
- Prisma + SQLite schema foundation with committed initial migration
- Local authentication with JWT-based session flow plus persisted session visibility and revocation
- RBAC permission model and protected routes
- Central Company Settings with runtime branding controls
- Light and dark mode theme system
- Local file attachment storage under `/app/data/uploads`
- Puppeteer PDF service foundation with branded company-profile preview
- CRM reference entities for customers and vendors
- CRM customer and vendor create/edit/detail workflows
- CRM search, filters, and persisted status tagging
- CRM contact-history timeline with authored notes, calls, emails, and meetings
- CRM shared file attachments on customer and vendor records, including delete support
- CRM reseller hierarchy, parent-child customer structure, and reseller discount support
- CRM multi-contact records, commercial terms, lifecycle stages, operational flags, and activity rollups
- Inventory item master, BOM, warehouse, and stock-location foundation
- Inventory transactions, on-hand tracking, and item attachments
- Inventory transfers, reservations, available-stock visibility, and work-order-driven material reservation automation
- Sales quotes and sales orders with commercial totals logic
- Purchase orders with vendor lookup, item lines, totals, and quick status actions
- Purchase-order line selection restricted to inventory items flagged as purchasable
- Purchase receiving foundation with warehouse/location posting, receipt history, and per-line received quantity tracking
- Branded sales quote, sales order, and purchase-order PDF templates through the shared Puppeteer pipeline
- Shipping shipment records linked to sales orders
- Packing-slip, shipping-label, and bill-of-lading PDF rendering for shipments
- Logistics attachments directly on shipment records
- Projects foundation with customer, quote, sales-order, shipment, owner, due-date, notes, and attachment linkage
- Project list/detail/create/edit workflows and dashboard program widgets
- Manufacturing foundation with work orders, project linkage, material issue posting, completion posting, and work-order attachments
- Manufacturing stations, item routing templates, and automatic work-order operation planning for gantt scheduling
- Vendor invoice/supporting-document attachments directly on purchase orders
- Vendor-detail purchasing visibility with recent purchase-order activity
- Audit trail coverage across core settings, inventory, purchasing, project, sales, and manufacturing write flows
- Admin diagnostics screen with runtime footprint, record counts, storage-path visibility, and recent audit activity
- Dedicated user-management screen for account creation, activation, role assignment, and role-permission editing
- CRM customer/vendor changes and shipping mutations covered by the shared audit trail
- Startup validation during server boot with checks for storage paths, writable directories, database connectivity, client bundle readiness, Chromium availability, and risky production defaults
- Backup/restore guidance and exportable support bundles surfaced through the admin diagnostics workflow
- Backup verification checklist and restore-drill runbook surfaced through the admin diagnostics workflow
- Support-log viewing for startup warnings, HTTP failures, and server errors surfaced through the admin diagnostics workflow
- Route-level frontend code-splitting and vendor chunking to keep the initial client payload lighter
- SKU-searchable BOM component selection for inventory-scale datasets
- Theme persistence fixes and denser responsive workspace layouts
- Full-site density normalization pass across active CRM, inventory, settings, dashboard, and login screens
- Live planning gantt timelines driven by project and manufacturing data
- Sales-order demand planning with multi-level BOM explosion, stock/open-supply netting, and build/buy recommendations
- Multi-stage Docker packaging and migration-aware entrypoint
- Docker image validated locally with successful app startup and login flow
- Core project documentation in `README.md`, `INSTRUCTIONS.md`, and `STRUCTURE.md`
1. Support-log filtering, retention controls, and broader support-package polish
2. Revision comparison UX for changed sales and purchasing documents
3. Project milestones, project rollups, and deeper project-side execution visibility
4. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views
5. Dashboard KPI, alert, recent-activity, and exception-widget expansion
### Current known gaps in the foundation
## Active roadmap
- Prisma migration execution is committed and documented, but local Windows Node 24 schema-engine behavior remains inconsistent; use Node 22 or Docker for migration execution
- CRM reporting is now functional, but broader account-role depth and downstream document rollups can still evolve later
- The current sales/purchasing/shipping foundation now includes sales approvals and revision history, but still needs vendor exception handling, deeper carrier integration, and richer document comparison tooling
- The dashboard is now live-data driven, but still needs richer KPI widgets, alerts, recent-activity queues, and exception reporting as more transactional depth is added
- The new projects domain is foundational but still needs milestones, project rollups, and deeper inventory/purchasing/manufacturing tie-ins
- The new manufacturing domain is foundational but still needs routings, labor capture, work-center views, and capacity-aware planning tie-ins
- Auth sessions are now persisted and revocable, but the admin surface still needs richer filtering, history retention, and unusual-access review tooling
### Platform and operational docs
## Dashboard Plan
- Keep the Windows Prisma migration workflow clearer and less fragile for local contributors
- Continue tightening backup, restore, and support-runbook guidance as operations maturity grows
- Preserve the single-container deployment path while improving diagnostics and supportability
- Keep `Dashboard` as the primary landing surface for operators
- Expand it by modular panels rather than redesigning it for each new feature phase
- Prefer metric cards, exception queues, action shortcuts, and status summaries over static descriptive content
- Add future widgets for purchasing, shipping exceptions, inventory shortages, planning readiness, and audit/system health
- Continue expanding the new project widgets into milestone, blockage, and shipment-readiness views instead of creating a separate landing area
- Continue expanding the new manufacturing widgets into shortage, routing, and bottleneck views instead of creating a separate landing area
- Treat dashboard modules as upgradeable blocks that can be reordered or expanded without disturbing the shell
### Dashboard
## Planned feature phases
- Expand `Dashboard` by modular panels rather than redesigning it into a different shell
- Add richer KPI widgets, alerts, recent-activity queues, and exception reporting
- Add deeper project, manufacturing, purchasing, shipping, and audit/system-health widgets
### Phase 1: CRM and master data hardening
### CRM and master data
- Better seed/bootstrap strategy for non-development environments
- Additional CRM account-role depth if later sales/purchasing workflows need it
- More derived CRM rollups once quotes, orders, and purchasing documents exist
QOL subfeatures:
- More derived CRM rollups once downstream quote/order/purchasing/shipping data grows further
- Saved CRM filters and quick views
- Better hierarchy navigation between reseller parents and child accounts
- One-click contact actions for email and phone workflows
- Duplicate-account detection and merge workflow
- Cleaner attachment previews and richer record timelines
- More compact table controls for heavy CRM data-entry users
- CRM document rollups and broader account-role depth that were deferred until downstream modules matured
### Phase 2: Inventory and manufacturing core
### Inventory
- Item master and SKU structure foundation
- Warehouse and stock location foundation
- Inventory transactions and on-hand tracking foundation
- Bills of materials and custom assemblies foundation
- File attachments for BOM drawings and manufacturing support docs foundation
QOL subfeatures:
- Item master enrichment: categories, alternate part numbers, revisions, preferred vendor data, and reorder settings
- Stock transfers between warehouses and locations
- Reservation and allocation visibility against demand
- Faster SKU search and keyboard-heavy item/BOM entry flows refinement
- Better warehouse dashboards for on-hand, shortages, and recent movement
- Item master enrichment: categories, alternate part numbers, revisions, reorder settings, and broader sourcing metadata
- Faster keyboard-heavy item/BOM entry refinement beyond the current searchable pickers
- Better warehouse dashboards for on-hand, shortages, reservations, and recent movement
- BOM revision support and clearer where-used visibility
- Bulk item import/export and mass-update utilities
### Phase 3: Sales and purchasing documents
- Quotes, sales orders, and purchase orders
- Reusable line-item and totals model
- Purchase receiving flow tied to purchase-order lines and inventory receipts foundation
- Document states, approvals, and revision history
- Branded PDF templates rendered through Puppeteer
- Attachments for vendor invoices and supporting documents
Foundation slice shipped:
- Sales approval stamps and automatic revision history on quotes and sales orders
- Purchase-order supporting documents through the shared attachment pipeline
- Vendor-detail purchasing visibility for recent purchase-order activity
QOL subfeatures:
### Sales and purchasing
- Vendor exception handling for acknowledgements, invoice matching, receipt discrepancies, and related inbound follow-up
- Deeper carrier/commercial defaults where they improve order-entry speed
- Revision comparison UX for changed customer-facing and purchasing documents
- Line duplication, drag ordering, and keyboard-first line editing
- Saved customer defaults for tax, freight, and commercial terms
- Inline stock visibility while building quotes and orders
- Restrict purchase-order item entry to purchasable inventory only
- Richer dashboard widgets for recent quotes, open orders, purchasing queues, and shipping exceptions
- Better totals breakdown visibility on list pages and detail pages
- Revision comparison view for changed customer-facing documents
- Faster document cloning and quote-to-order style conversions across document types
### Phase 4: Shipping and logistics
### Shipping and logistics
- Shipment records linked to sales orders
- Bills of lading, packing slips, and shipping BOM PDFs
- Carrier, package, and tracking data
- Outbound shipment status workflow
- Scanned logistics-document attachment handling
QOL subfeatures:
- Printer-friendly reprint and history actions for logistics documents
- Partial shipment workflow and split-shipment visibility
- Better tracking-link UX and carrier-specific shortcuts
- Packing verification and ship-confirm checkpoints
- Shipment search by order, tracking, customer, and carrier from one screen
- Reprint and history actions for generated logistics PDFs
- Printer-friendly reprint/history actions for logistics documents
### Phase 5: Projects and program management
Foundation slice shipped:
- Project records with customer linkage, status, owner, priority, due dates, and notes
- Project-to-quote, sales-order, and shipment linkage for delivery context
- Project attachments through the shared file pipeline
- Project list/detail/create/edit flows and dashboard visibility
### Projects and program management
- Project document hub for drawings, support files, correspondence, and revision references
- Milestones, checkpoints, and non-manufacturing work packages for long-running execution tracking
- Project-level commercial, material, schedule, and delivery rollups
- Cross-functional visibility for engineering, purchasing, manufacturing, shipping, and customer communication
Module interactions:
- CRM: projects link to customer accounts, reseller-owned end customers, contacts, and account notes
- Sales: quotes and sales orders can spawn or attach to projects; project status should reflect commercial state where relevant
- Inventory: projects reference item/BOM scope, expose shortage/reservation pressure, and later roll up material readiness
- Purchasing: projects surface buyout demand and vendor receipts tied to project material needs
- Shipping: shipments should be visible from the project record when a project drives deliverables
- Dashboard: projects add live widgets for active programs, overdue milestones, shortages, and blocked delivery
- Manufacturing: manufacturing orders and shop execution should link back to projects, but remain their own subsystem
- Gantt/planning: project milestones and execution dates should feed planning views without collapsing projects into scheduling alone
QOL subfeatures:
- Project templates for repeatable build types
- Project-specific attachment bundles and revision snapshots
- One-screen project cockpit with commercial, material, schedule, and shipping summary
@@ -197,34 +87,13 @@ QOL subfeatures:
- Project filtering by customer, owner, status, due date, and risk
- Project activity timeline and audit-friendly milestone history
### Phase 6: Manufacturing execution
### Manufacturing execution
Foundation slice shipped:
- Work orders tied to manufactured or assembly items, with optional project linkage
- BOM-based material requirement visibility from the work-order record
- Material issue posting that creates real inventory issue transactions
- Production completion posting that creates finished-goods receipt transactions
- Work-order list/detail/create/edit flows, attachments, and dashboard visibility
- Work orders tied to projects, sales demand, or internal build demand
- Routing/work-center structure for manufacturing steps and handoffs
- Material issue, consumption, completion, and WIP tracking
- Work orders tied more explicitly to sales demand or internal build demand where appropriate
- Routing/work-center structure for manufacturing steps and handoffs beyond the current station templates
- Material consumption depth, WIP tracking, and execution traceability
- Labor and machine-time capture for production execution
- Manufacturing status workflow from release through completion
- Manufacturing rollups for open work, blockers, shortages, and throughput
Module interactions:
- Projects: manufacturing orders can be attached to projects, but projects remain the higher-level long-running record
- Inventory: manufacturing consumes components and produces finished/semi-finished stock
- Purchasing: shortages and buyout demand should be visible from manufacturing execution
- Shipping: completed manufacturing should feed shipment readiness, but shipping remains separate
- Dashboard: manufacturing adds live queues for open jobs, blocked work, overdue orders, and completion throughput
- Planning: manufacturing orders and routings become a major input into capacity and gantt scheduling
QOL subfeatures:
- Traveler/job packet output
- Partial completions and split-order execution visibility
- Better shortage and substitute-part handling
@@ -232,22 +101,12 @@ QOL subfeatures:
- Rework / hold / scrap tracking
- Work-center dashboards and operator-focused queues
### Phase 7: Planning and scheduling
### Planning and scheduling
Foundation slice shipped:
- Live gantt schedule backed by active projects and open manufacturing work orders
- Project due-date milestones, manufacturing sequencing links, and standalone work-queue visibility
- Planning exception queue for overdue or at-risk project/manufacturing schedule items
- Live project-backed SVAR gantt timelines
- Task dependencies, milestones, and progress updates
- Manufacturing calendar views and bottleneck visibility
- Labor and machine scheduling support
- Theme-compliant gantt customization for light/dark mode
QOL subfeatures:
- Collapsible schedule groupings and saved planner views
- Drag-and-drop rescheduling improvements
- Critical-path and overdue highlighting
@@ -255,68 +114,20 @@ QOL subfeatures:
- Better mobile and tablet behavior for shop-floor lookups
- Faster filtering by project, customer, work center, and status
### Phase 8: Demand planning and supply generation
### Demand planning and supply generation
Foundation slice shipped:
- Sales-order demand planning from approved or active demand records
- Multi-level BOM explosion from sales-order lines through manufactured and assembly children
- Netting against available stock, active reservations, open work orders, and open purchase orders
- Build and buy recommendations surfaced directly from the sales-order workflow
- Prefilled work-order and purchase-order draft generation launched from demand-planning recommendations
- Shared shortage and readiness rollups surfaced across dashboard, planning, project, purchasing, and manufacturing views
- Preferred-vendor sourcing on inventory items for buy-side planning defaults
- Pegged work-order and purchase-order supply links back to originating sales demand
- Planning recommendations now reduce against already-linked draft/open supply to avoid duplicate WO/PO generation
- Shared MRP demand engine across sales, inventory, purchasing, manufacturing, projects, and planning
- Planned work-order and purchase-order recommendation generation
- Coverage, shortage, and lateness rollups from customer demand down through supply layers
- Cross-module shortage visibility on sales orders, projects, work orders, purchasing, and dashboard widgets
QOL subfeatures:
- One-click conversion of planning recommendations into work orders and purchase orders
- Deeper planner drilldowns from demand source to buy/build action without re-keying data
- Better shortage and substitute-part guidance during planning review
- Saved planning views by customer, project, item family, and shortage state
- Planner-focused drilldowns from demand source to buy/build action without re-keying data
- Time-phased supply recommendations with vendor lead times and build timing
### Phase 9: Security, audit, and operations maturity
### Security, audit, and operations maturity
Foundation slice shipped:
- Audit trail coverage across core write flows for settings, inventory, sales, purchasing, projects, and manufacturing
- Admin diagnostics screen for runtime footprint, storage visibility, key record counts, and recent audit activity
- Expanded role-management UI with account creation, activation, role assignment, and permission administration
- Persisted auth-session tracking with admin visibility into active, expired, and revoked sign-ins
- Server-side logout and admin session revocation for JWT-backed access
- Shared destructive-action confirmation and recovery messaging for admin, sales, purchasing, shipping, inventory, manufacturing, and attachment workflows
- CRM customer/vendor changes and shipping mutations covered by the shared audit trail
- Startup validation during server boot with checks for storage paths, writable directories, database connectivity, client bundle readiness, Chromium availability, and risky production defaults
- Backup/restore guidance, support-bundle exports, and support-log viewing surfaced through the admin diagnostics workflow
- Expanded role management UI
- Permission assignment administration
- Audit trail coverage across critical records
- Backup/restore workflow documentation and scripts
- Health checks, startup diagnostics, and production readiness cleanup
QOL subfeatures:
- Admin diagnostics screen for permissions, migrations, storage, and PDF health
- Better session filtering, review history, and unusual-access cues for operational admins
- Extend destructive-action safety coverage into remaining project and form-edit removal workflows
- More explicit environment validation on startup
- Support-log filtering, retention controls, and broader support-package polish
- Backup verification checklist and restore drill guidance
## Revisit / Deferred Items
- Local Windows Prisma migration reliability still needs a cleaner documented workflow or tooling wrapper
- CRM document rollups and broader account-role depth were deferred until more downstream modules exist
- Some generated document and workflow screens still need additional polish for dense, keyboard-efficient operational use
- Dashboard cards now use live data, but richer recent-activity widgets and exception queues are still deferred
- Admin diagnostics depth for permissions, migrations, storage, and PDF health
- Longer-term session history and audit depth beyond the current review filtering and retention cleanup
- More explicit environment validation on startup
- Backup verification and restore-drill guidance should keep expanding as the system grows
## Cross-cutting improvements
@@ -327,7 +138,7 @@ QOL subfeatures:
- Consistent document-template system shared by sales, purchasing, and shipping
- Clear upgrade path for future module additions without refactoring the app shell
## Near-term priority order
## Revisit / Deferred Items
1. Better session filtering, review history, and unusual-access cues for operational admins
2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows
- Local Windows Prisma migration reliability still needs a cleaner documented workflow or tooling wrapper
- Some generated document and workflow screens still need additional polish for dense, keyboard-efficient operational use
+122
View File
@@ -0,0 +1,122 @@
# Shipped
This file tracks roadmap phases, slices, and major foundations that have already shipped. Remaining work lives in [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md).
## Foundation release
- Monorepo-style workspace with `client`, `server`, and `shared`
- React + Vite + Tailwind frontend shell
- Express + TypeScript backend shell
- Prisma + SQLite schema foundation with committed initial migration
- Local authentication with JWT-based session flow plus persisted session visibility and revocation
- RBAC permission model and protected routes
- Central Company Settings with runtime branding controls
- Light and dark mode theme system
- Local file attachment storage under `/app/data/uploads`
- Puppeteer PDF service foundation with branded company-profile preview
- CRM reference entities for customers and vendors
- CRM customer and vendor create/edit/detail workflows
- CRM search, filters, and persisted status tagging
- CRM contact-history timeline with authored notes, calls, emails, and meetings
- CRM shared file attachments on customer and vendor records, including delete support
- CRM reseller hierarchy, parent-child customer structure, and reseller discount support
- CRM multi-contact records, commercial terms, lifecycle stages, operational flags, and activity rollups
- Inventory item master, BOM, warehouse, and stock-location foundation
- Inventory transactions, on-hand tracking, and item attachments
- Inventory transfers, reservations, available-stock visibility, and work-order-driven material reservation automation
- Sales quotes and sales orders with commercial totals logic
- Purchase orders with vendor lookup, item lines, totals, and quick status actions
- Purchase-order line selection restricted to inventory items flagged as purchasable
- Purchase receiving foundation with warehouse/location posting, receipt history, and per-line received quantity tracking
- Branded sales quote, sales order, and purchase-order PDF templates through the shared Puppeteer pipeline
- Shipping shipment records linked to sales orders
- Packing-slip, shipping-label, and bill-of-lading PDF rendering for shipments
- Logistics attachments directly on shipment records
- Projects foundation with customer, quote, sales-order, shipment, owner, due-date, notes, and attachment linkage
- Project list/detail/create/edit workflows and dashboard program widgets
- Manufacturing foundation with work orders, project linkage, material issue posting, completion posting, and work-order attachments
- Manufacturing stations, item routing templates, and automatic work-order operation planning for gantt scheduling
- Vendor invoice/supporting-document attachments directly on purchase orders
- Vendor-detail purchasing visibility with recent purchase-order activity
- Audit trail coverage across core settings, inventory, purchasing, project, sales, and manufacturing write flows
- Admin diagnostics screen with runtime footprint, record counts, storage-path visibility, and recent audit activity
- Dedicated user-management screen for account creation, activation, role assignment, and role-permission editing
- CRM customer/vendor changes and shipping mutations covered by the shared audit trail
- Startup validation during server boot with checks for storage paths, writable directories, database connectivity, client bundle readiness, Chromium availability, and risky production defaults
- Backup/restore guidance and exportable support bundles surfaced through the admin diagnostics workflow
- Backup verification checklist and restore-drill runbook surfaced through the admin diagnostics workflow
- Support-log viewing for startup warnings, HTTP failures, and server errors surfaced through the admin diagnostics workflow
- Route-level frontend code-splitting and vendor chunking to keep the initial client payload lighter
- SKU-searchable BOM component selection for inventory-scale datasets
- Theme persistence fixes and denser responsive workspace layouts
- Full-site density normalization pass across active CRM, inventory, settings, dashboard, and login screens
- Live planning gantt timelines driven by project and manufacturing data
- Sales-order demand planning with multi-level BOM explosion, stock/open-supply netting, and build/buy recommendations
- Multi-stage Docker packaging and migration-aware entrypoint
- Docker image validated locally with successful app startup and login flow
- Core project documentation in `README.md`, `INSTRUCTIONS.md`, and `STRUCTURE.md`
## Shipped roadmap phases
### Phase 3: Sales and purchasing documents
- Sales approval stamps and automatic revision history on quotes and sales orders
- Purchase-order supporting documents through the shared attachment pipeline
- Vendor-detail purchasing visibility for recent purchase-order activity
### Phase 5: Projects and program management
- Project records with customer linkage, status, owner, priority, due dates, and notes
- Project-to-quote, sales-order, and shipment linkage for delivery context
- Project attachments through the shared file pipeline
- Project list/detail/create/edit flows and dashboard visibility
### Phase 6: Manufacturing execution
- Work orders tied to manufactured or assembly items, with optional project linkage
- BOM-based material requirement visibility from the work-order record
- Material issue posting that creates real inventory issue transactions
- Production completion posting that creates finished-goods receipt transactions
- Work-order list/detail/create/edit flows, attachments, and dashboard visibility
### Phase 7: Planning and scheduling
- Live gantt schedule backed by active projects and open manufacturing work orders
- Project due-date milestones, manufacturing sequencing links, and standalone work-queue visibility
- Planning exception queue for overdue or at-risk project/manufacturing schedule items
### Phase 8: Demand planning and supply generation
- Sales-order demand planning from approved or active demand records
- Multi-level BOM explosion from sales-order lines through manufactured and assembly children
- Netting against available stock, active reservations, open work orders, and open purchase orders
- Build and buy recommendations surfaced directly from the sales-order workflow
- Prefilled work-order and purchase-order draft generation launched from demand-planning recommendations
- Shared shortage and readiness rollups surfaced across dashboard, planning, project, purchasing, and manufacturing views
- Preferred-vendor sourcing on inventory items for buy-side planning defaults
- Pegged work-order and purchase-order supply links back to originating sales demand
- Planning recommendations now reduce against already-linked draft/open supply to avoid duplicate WO/PO generation
### Phase 9: Security, audit, and operations maturity
- Audit trail coverage across core write flows for settings, inventory, sales, purchasing, projects, and manufacturing
- Admin diagnostics screen for runtime footprint, storage visibility, key record counts, and recent audit activity
- Expanded role-management UI with account creation, activation, role assignment, and permission administration
- Persisted auth-session tracking with admin visibility into active, expired, and revoked sign-ins
- Server-side logout and admin session revocation for JWT-backed access
- Session review filtering, unusual-access cues, diagnostics rollups, and startup pruning of stale expired/revoked auth sessions
- Shared destructive-action confirmation and recovery messaging for admin, sales, purchasing, shipping, inventory, manufacturing, project, warehouse/form-editor, and attachment workflows
- CRM customer/vendor changes and shipping mutations covered by the shared audit trail
- Startup validation during server boot with checks for storage paths, writable directories, database connectivity, client bundle readiness, Chromium availability, and risky production defaults
- Backup/restore guidance, support-bundle exports, and support-log viewing surfaced through the admin diagnostics workflow
## Shipped quality-of-life slices
- Purchase-order item entry restricted to purchasable inventory items only
- Inventory transfers between warehouses and locations
- Manual and work-order-driven inventory reservations
- Reserved and available stock visibility on inventory item detail and stock-by-location views
- Searchable operational pickers for customers, vendors, SKUs, BOM components, and other dense record selectors
- Route-level lazy loading and vendor chunking for a lighter initial client payload
- Persisted auth-session review filtering and admin-side access review cues
- Destructive-action confirmation coverage expanded into project customer/document unlinking and form-row removals in sales, purchasing, inventory, and warehouse editors
@@ -4,6 +4,7 @@ import type { ManufacturingStationDto } from "@mrp/shared";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
import { emptyInventoryBomLineInput, emptyInventoryItemInput, emptyInventoryOperationInput, inventoryStatusOptions, inventoryTypeOptions, inventoryUnitOptions } from "./config";
@@ -26,6 +27,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
const [vendorPickerOpen, setVendorPickerOpen] = useState(false);
const [status, setStatus] = useState(mode === "create" ? "Create a new inventory item." : "Loading inventory item...");
const [isSaving, setIsSaving] = useState(false);
const [pendingRemoval, setPendingRemoval] = useState<{ kind: "operation" | "bom-line"; index: number } | null>(null);
function getComponentOption(componentItemId: string) {
return componentOptions.find((option) => option.id === componentItemId) ?? null;
@@ -192,6 +194,12 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
setActiveComponentPicker((current) => (current === index ? null : current != null && current > index ? current - 1 : current));
}
const pendingRemovalDetail = pendingRemoval
? pendingRemoval.kind === "operation"
? { label: form.operations[pendingRemoval.index]?.stationId || "this routing operation", typeLabel: "routing operation" }
: { label: getComponentSku(form.bomLines[pendingRemoval.index]?.componentItemId ?? "") || "this BOM line", typeLabel: "BOM line" }
: null;
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!token) {
@@ -472,7 +480,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
<input type="number" min={0} step={10} value={operation.position} onChange={(event) => updateOperation(index, { ...operation, position: Number(event.target.value) || 0 })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
</label>
<div className="flex items-end">
<button type="button" onClick={() => removeOperation(index)} className="rounded-2xl border border-rose-400/40 px-2 py-2 text-sm font-semibold text-rose-700 dark:text-rose-300">
<button type="button" onClick={() => setPendingRemoval({ kind: "operation", 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
</button>
</div>
@@ -619,7 +627,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
<div className="flex items-end">
<button
type="button"
onClick={() => removeBomLine(index)}
onClick={() => setPendingRemoval({ kind: "bom-line", 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
@@ -649,6 +657,31 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
</button>
</div>
</section>
<ConfirmActionDialog
open={pendingRemoval != null}
title={pendingRemoval?.kind === "operation" ? "Remove routing operation" : "Remove BOM line"}
description={
pendingRemoval && pendingRemovalDetail
? `Remove ${pendingRemovalDetail.label} from the item ${pendingRemovalDetail.typeLabel} draft.`
: "Remove this draft row."
}
impact={
pendingRemoval?.kind === "operation"
? "The operation will no longer be copied into new work orders from this item."
: "The component requirement will be removed from the BOM draft immediately."
}
recovery="Add the row back before saving if this change was accidental."
confirmLabel={pendingRemoval?.kind === "operation" ? "Remove operation" : "Remove BOM line"}
onClose={() => setPendingRemoval(null)}
onConfirm={() => {
if (pendingRemoval?.kind === "operation") {
removeOperation(pendingRemoval.index);
} else if (pendingRemoval?.kind === "bom-line") {
removeBomLine(pendingRemoval.index);
}
setPendingRemoval(null);
}}
/>
</form>
);
}
@@ -2,6 +2,7 @@ import type { WarehouseInput, WarehouseLocationInput } from "@mrp/shared/dist/in
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
import { emptyWarehouseInput, emptyWarehouseLocationInput } from "./config";
@@ -13,6 +14,7 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
const [form, setForm] = useState<WarehouseInput>(emptyWarehouseInput);
const [status, setStatus] = useState(mode === "create" ? "Create a new warehouse." : "Loading warehouse...");
const [isSaving, setIsSaving] = useState(false);
const [pendingLocationRemovalIndex, setPendingLocationRemovalIndex] = useState<number | null>(null);
useEffect(() => {
if (mode !== "edit" || !token || !warehouseId) {
@@ -67,6 +69,8 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
}));
}
const pendingLocationRemoval = pendingLocationRemovalIndex != null ? form.locations[pendingLocationRemovalIndex] : null;
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!token) {
@@ -147,7 +151,7 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
<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>
<div className="flex items-end">
<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">
<button type="button" onClick={() => setPendingLocationRemovalIndex(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
</button>
</div>
@@ -167,6 +171,21 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
</button>
</div>
</section>
<ConfirmActionDialog
open={pendingLocationRemoval != null}
title="Remove warehouse location"
description={pendingLocationRemoval ? `Remove location ${pendingLocationRemoval.code || pendingLocationRemoval.name || "from this warehouse draft"}.` : "Remove this location."}
impact="The location will be removed from the warehouse edit form immediately."
recovery="Add the location back before saving if it should remain part of this warehouse."
confirmLabel="Remove location"
onClose={() => setPendingLocationRemovalIndex(null)}
onConfirm={() => {
if (pendingLocationRemovalIndex != null) {
removeLocation(pendingLocationRemovalIndex);
}
setPendingLocationRemovalIndex(null);
}}
/>
</form>
);
}
+115 -16
View File
@@ -8,10 +8,17 @@ import type {
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
import { emptyProjectInput, projectPriorityOptions, projectStatusOptions } from "./config";
type ProjectPendingConfirmation =
| { kind: "change-customer"; customerId: string; customerName: string }
| { kind: "unlink-quote" }
| { kind: "unlink-order" }
| { kind: "unlink-shipment" };
export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
const { token, user } = useAuth();
const navigate = useNavigate();
@@ -34,6 +41,7 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
const [shipmentPickerOpen, setShipmentPickerOpen] = useState(false);
const [status, setStatus] = useState(mode === "create" ? "Create a new project." : "Loading project...");
const [isSaving, setIsSaving] = useState(false);
const [pendingConfirmation, setPendingConfirmation] = useState<ProjectPendingConfirmation | null>(null);
useEffect(() => {
if (!token) {
@@ -103,6 +111,43 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
}));
}
function hasLinkedCommercialRecords() {
return Boolean(form.salesQuoteId || form.salesOrderId || form.shipmentId);
}
function applyCustomerSelection(customerId: string, customerName: string) {
updateField("customerId", customerId);
setCustomerSearchTerm(customerName);
setCustomerPickerOpen(false);
}
function requestCustomerSelection(customerId: string, customerName: string) {
if (form.customerId && form.customerId !== customerId && hasLinkedCommercialRecords()) {
setPendingConfirmation({ kind: "change-customer", customerId, customerName });
return;
}
applyCustomerSelection(customerId, customerName);
}
function unlinkQuote() {
updateField("salesQuoteId", null);
setQuoteSearchTerm("");
setQuotePickerOpen(false);
}
function unlinkOrder() {
updateField("salesOrderId", null);
setOrderSearchTerm("");
setOrderPickerOpen(false);
}
function unlinkShipment() {
updateField("shipmentId", null);
setShipmentSearchTerm("");
setShipmentPickerOpen(false);
}
function restoreSearchTerms() {
const selectedCustomer = customerOptions.find((customer) => customer.id === form.customerId);
const selectedOwner = ownerOptions.find((owner) => owner.id === form.ownerId);
@@ -162,7 +207,6 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
value={customerSearchTerm}
onChange={(event) => {
setCustomerSearchTerm(event.target.value);
updateField("customerId", "");
setCustomerPickerOpen(true);
}}
onFocus={() => setCustomerPickerOpen(true)}
@@ -187,9 +231,7 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
.map((customer) => (
<button key={customer.id} type="button" onMouseDown={(event) => {
event.preventDefault();
updateField("customerId", customer.id);
setCustomerSearchTerm(customer.name);
setCustomerPickerOpen(false);
requestCustomerSelection(customer.id, customer.name);
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition last:border-b-0 hover:bg-page/70">
<div className="font-semibold text-text">{customer.name}</div>
<div className="mt-1 text-xs text-muted">{customer.email}</div>
@@ -278,7 +320,6 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
value={quoteSearchTerm}
onChange={(event) => {
setQuoteSearchTerm(event.target.value);
updateField("salesQuoteId", null);
setQuotePickerOpen(true);
}}
onFocus={() => setQuotePickerOpen(true)}
@@ -293,9 +334,11 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
<div className="absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel">
<button type="button" onMouseDown={(event) => {
event.preventDefault();
updateField("salesQuoteId", null);
setQuoteSearchTerm("");
setQuotePickerOpen(false);
if (form.salesQuoteId) {
setPendingConfirmation({ kind: "unlink-quote" });
} else {
unlinkQuote();
}
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition hover:bg-page/70">
<div className="font-semibold text-text">No linked quote</div>
</button>
@@ -330,7 +373,6 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
value={orderSearchTerm}
onChange={(event) => {
setOrderSearchTerm(event.target.value);
updateField("salesOrderId", null);
setOrderPickerOpen(true);
}}
onFocus={() => setOrderPickerOpen(true)}
@@ -345,9 +387,11 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
<div className="absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel">
<button type="button" onMouseDown={(event) => {
event.preventDefault();
updateField("salesOrderId", null);
setOrderSearchTerm("");
setOrderPickerOpen(false);
if (form.salesOrderId) {
setPendingConfirmation({ kind: "unlink-order" });
} else {
unlinkOrder();
}
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition hover:bg-page/70">
<div className="font-semibold text-text">No linked sales order</div>
</button>
@@ -382,7 +426,6 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
value={shipmentSearchTerm}
onChange={(event) => {
setShipmentSearchTerm(event.target.value);
updateField("shipmentId", null);
setShipmentPickerOpen(true);
}}
onFocus={() => setShipmentPickerOpen(true)}
@@ -397,9 +440,11 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
<div className="absolute z-20 mt-2 max-h-64 w-full overflow-y-auto rounded-2xl border border-line/70 bg-surface shadow-panel">
<button type="button" onMouseDown={(event) => {
event.preventDefault();
updateField("shipmentId", null);
setShipmentSearchTerm("");
setShipmentPickerOpen(false);
if (form.shipmentId) {
setPendingConfirmation({ kind: "unlink-shipment" });
} else {
unlinkShipment();
}
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition hover:bg-page/70">
<div className="font-semibold text-text">No linked shipment</div>
</button>
@@ -439,6 +484,60 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
</button>
</div>
</section>
<ConfirmActionDialog
open={pendingConfirmation != null}
title={
pendingConfirmation?.kind === "change-customer"
? "Change project customer"
: pendingConfirmation?.kind === "unlink-quote"
? "Remove linked quote"
: pendingConfirmation?.kind === "unlink-order"
? "Remove linked sales order"
: "Remove linked shipment"
}
description={
pendingConfirmation?.kind === "change-customer"
? `Switch this project to ${pendingConfirmation.customerName}. Existing quote, sales order, and shipment links will be cleared.`
: pendingConfirmation?.kind === "unlink-quote"
? "Remove the currently linked quote from this project draft."
: pendingConfirmation?.kind === "unlink-order"
? "Remove the currently linked sales order from this project draft."
: "Remove the currently linked shipment from this project draft."
}
impact={
pendingConfirmation?.kind === "change-customer"
? "Commercial and delivery linkage tied to the previous customer will be cleared immediately from the draft."
: "The project will no longer point to that related record after you save this edit."
}
recovery={
pendingConfirmation?.kind === "change-customer"
? "Re-link the correct quote, order, and shipment before saving if the customer change was accidental."
: "Pick the related record again before saving if this unlink was a mistake."
}
confirmLabel={
pendingConfirmation?.kind === "change-customer"
? "Change customer"
: "Remove link"
}
onClose={() => setPendingConfirmation(null)}
onConfirm={() => {
if (!pendingConfirmation) {
return;
}
if (pendingConfirmation.kind === "change-customer") {
applyCustomerSelection(pendingConfirmation.customerId, pendingConfirmation.customerName);
} else if (pendingConfirmation.kind === "unlink-quote") {
unlinkQuote();
} else if (pendingConfirmation.kind === "unlink-order") {
unlinkOrder();
} else if (pendingConfirmation.kind === "unlink-shipment") {
unlinkShipment();
}
setPendingConfirmation(null);
}}
/>
</form>
);
}
@@ -2,6 +2,7 @@ import type { InventoryItemOptionDto, PurchaseLineInput, PurchaseOrderInput, Pur
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
import { inventoryUnitOptions } from "../inventory/config";
@@ -24,6 +25,7 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
const [lineSearchTerms, setLineSearchTerms] = useState<string[]>([]);
const [activeLinePicker, setActiveLinePicker] = useState<number | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [pendingLineRemovalIndex, setPendingLineRemovalIndex] = useState<number | null>(null);
function collectRecommendedPurchaseNodes(node: SalesOrderPlanningNodeDto): SalesOrderPlanningNodeDto[] {
const nodes = node.recommendedPurchaseQuantity > 0 ? [node] : [];
@@ -212,6 +214,15 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
setLineSearchTerms((current) => current.filter((_term, termIndex) => termIndex !== index));
}
const pendingLineRemoval =
pendingLineRemovalIndex != null
? {
index: pendingLineRemovalIndex,
line: form.lines[pendingLineRemovalIndex],
sku: lineSearchTerms[pendingLineRemovalIndex] ?? "",
}
: null;
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!token) {
@@ -425,7 +436,7 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
<input type="number" min={0} step={0.01} value={line.unitCost} onChange={(event) => updateLine(index, { ...line, unitCost: Number(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>
<div className="flex items-end"><div className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-sm text-text">${(line.quantity * line.unitCost).toFixed(2)}</div></div>
<div className="flex items-end"><button type="button" onClick={() => removeLine(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</button></div>
<div className="flex items-end"><button type="button" onClick={() => setPendingLineRemovalIndex(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</button></div>
</div>
</div>
))}
@@ -444,6 +455,25 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
</button>
</div>
</section>
<ConfirmActionDialog
open={pendingLineRemoval != null}
title="Remove purchase line"
description={
pendingLineRemoval
? `Remove ${pendingLineRemoval.sku || pendingLineRemoval.line?.description || "this line"} from the purchase order draft.`
: "Remove this purchase line."
}
impact="The line will be removed from the draft immediately and purchasing totals will recalculate."
recovery="Re-add the line before saving if the removal was accidental."
confirmLabel="Remove line"
onClose={() => setPendingLineRemovalIndex(null)}
onConfirm={() => {
if (pendingLineRemoval) {
removeLine(pendingLineRemoval.index);
}
setPendingLineRemovalIndex(null);
}}
/>
</form>
);
}
+32 -1
View File
@@ -3,6 +3,7 @@ import type { SalesCustomerOptionDto, SalesDocumentDetailDto, SalesDocumentInput
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
import { useAuth } from "../../auth/AuthProvider";
import { api, ApiError } from "../../lib/api";
import { inventoryUnitOptions } from "../inventory/config";
@@ -23,6 +24,7 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
const [lineSearchTerms, setLineSearchTerms] = useState<string[]>([]);
const [activeLinePicker, setActiveLinePicker] = useState<number | null>(null);
const [isSaving, setIsSaving] = useState(false);
const [pendingLineRemovalIndex, setPendingLineRemovalIndex] = useState<number | null>(null);
const subtotal = form.lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0);
const discountAmount = subtotal * (form.discountPercent / 100);
@@ -129,6 +131,15 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
setLineSearchTerms((current: string[]) => current.filter((_term: string, termIndex: number) => termIndex !== index));
}
const pendingLineRemoval =
pendingLineRemovalIndex != null
? {
index: pendingLineRemovalIndex,
line: form.lines[pendingLineRemovalIndex],
sku: lineSearchTerms[pendingLineRemovalIndex] ?? "",
}
: null;
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!token) {
@@ -431,7 +442,7 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
</div>
</div>
<div className="flex items-end">
<button type="button" onClick={() => removeLine(index)} className="rounded-2xl border border-rose-400/40 px-2 py-2 text-sm font-semibold text-rose-700 dark:text-rose-300">
<button type="button" onClick={() => setPendingLineRemovalIndex(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
</button>
</div>
@@ -465,6 +476,26 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
</button>
</div>
</section>
<ConfirmActionDialog
open={pendingLineRemoval != null}
title={`Remove ${config.singularLabel.toLowerCase()} line`}
description={
pendingLineRemoval
? `Remove ${pendingLineRemoval.sku || pendingLineRemoval.line?.description || "this line"} from the ${config.singularLabel.toLowerCase()}.`
: "Remove this line."
}
impact="The line will be dropped from the document draft immediately and totals will recalculate."
recovery="Add the line back manually before saving if this removal was a mistake."
confirmLabel="Remove line"
isConfirming={false}
onClose={() => setPendingLineRemovalIndex(null)}
onConfirm={() => {
if (pendingLineRemoval) {
removeLine(pendingLineRemoval.index);
}
setPendingLineRemovalIndex(null);
}}
/>
</form>
);
}
@@ -96,6 +96,7 @@ export function AdminDiagnosticsPage() {
["Audit events", diagnostics.auditEventCount.toString()],
["Support logs", diagnostics.supportLogCount.toString()],
["Active users", `${diagnostics.activeUserCount} / ${diagnostics.userCount}`],
["Sessions to review", diagnostics.reviewSessionCount.toString()],
["Sales docs", diagnostics.salesDocumentCount.toString()],
["Work orders", diagnostics.workOrderCount.toString()],
["Projects", diagnostics.projectCount.toString()],
@@ -108,6 +109,7 @@ export function AdminDiagnosticsPage() {
["Uploads directory", diagnostics.uploadsDir],
["Client origin", diagnostics.clientOrigin],
["Company profile", diagnostics.companyProfilePresent ? "Present" : "Missing"],
["Active sessions", diagnostics.activeSessionCount.toString()],
["Roles / permissions", `${diagnostics.roleCount} / ${diagnostics.permissionCount}`],
["Customers / vendors", `${diagnostics.customerCount} / ${diagnostics.vendorCount}`],
["Inventory / warehouses", `${diagnostics.inventoryItemCount} / ${diagnostics.warehouseCount}`],
@@ -37,6 +37,9 @@ export function UserManagementPage() {
const [selectedUserId, setSelectedUserId] = useState<string>("new");
const [selectedRoleId, setSelectedRoleId] = useState<string>("new");
const [sessionUserFilter, setSessionUserFilter] = useState<string>("all");
const [sessionStatusFilter, setSessionStatusFilter] = useState<"ALL" | AdminAuthSessionDto["status"]>("ALL");
const [sessionReviewFilter, setSessionReviewFilter] = useState<"ALL" | AdminAuthSessionDto["reviewState"]>("ALL");
const [sessionQuery, setSessionQuery] = useState("");
const [userForm, setUserForm] = useState<AdminUserInput>(emptyUserForm);
const [roleForm, setRoleForm] = useState<AdminRoleInput>(emptyRoleForm);
const [status, setStatus] = useState("Loading admin access controls...");
@@ -224,10 +227,36 @@ export function UserManagementPage() {
await refreshData("Revoked session. The user must sign in again to restore access unless their account is inactive.");
}
const filteredSessions = sessions.filter((session) => sessionUserFilter === "all" || session.userId === sessionUserFilter);
const normalizedSessionQuery = sessionQuery.trim().toLowerCase();
const filteredSessions = sessions.filter((session) => {
if (sessionUserFilter !== "all" && session.userId !== sessionUserFilter) {
return false;
}
if (sessionStatusFilter !== "ALL" && session.status !== sessionStatusFilter) {
return false;
}
if (sessionReviewFilter !== "ALL" && session.reviewState !== sessionReviewFilter) {
return false;
}
if (!normalizedSessionQuery) {
return true;
}
return (
session.userName.toLowerCase().includes(normalizedSessionQuery) ||
session.userEmail.toLowerCase().includes(normalizedSessionQuery) ||
(session.ipAddress ?? "").toLowerCase().includes(normalizedSessionQuery) ||
(session.userAgent ?? "").toLowerCase().includes(normalizedSessionQuery) ||
session.reviewReasons.some((reason) => reason.toLowerCase().includes(normalizedSessionQuery))
);
});
const activeSessionCount = sessions.filter((session) => session.status === "ACTIVE").length;
const revokedSessionCount = sessions.filter((session) => session.status === "REVOKED").length;
const expiredSessionCount = sessions.filter((session) => session.status === "EXPIRED").length;
const reviewSessionCount = sessions.filter((session) => session.reviewState === "REVIEW").length;
return (
<div className="space-y-6">
@@ -432,12 +461,22 @@ export function UserManagementPage() {
Review recent authenticated sessions, see their current state, and revoke stale or risky access without changing the user record.
</p>
</div>
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Filter by user</span>
<span className="mb-2 block text-sm font-semibold text-text">Search</span>
<input
value={sessionQuery}
onChange={(event) => setSessionQuery(event.target.value)}
placeholder="User, email, IP, agent, review reason"
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none"
/>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">User</span>
<select
value={sessionUserFilter}
onChange={(event) => setSessionUserFilter(event.target.value)}
className="rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none"
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none"
>
<option value="all">All users</option>
{users.map((user) => (
@@ -447,9 +486,35 @@ export function UserManagementPage() {
))}
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Status</span>
<select
value={sessionStatusFilter}
onChange={(event) => setSessionStatusFilter(event.target.value as "ALL" | AdminAuthSessionDto["status"])}
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none"
>
<option value="ALL">All statuses</option>
<option value="ACTIVE">Active</option>
<option value="EXPIRED">Expired</option>
<option value="REVOKED">Revoked</option>
</select>
</label>
<label className="block">
<span className="mb-2 block text-sm font-semibold text-text">Review</span>
<select
value={sessionReviewFilter}
onChange={(event) => setSessionReviewFilter(event.target.value as "ALL" | AdminAuthSessionDto["reviewState"])}
className="w-full rounded-2xl border border-line/70 bg-page px-3 py-2 text-sm text-text outline-none"
>
<option value="ALL">All sessions</option>
<option value="REVIEW">Needs review</option>
<option value="NORMAL">Normal</option>
</select>
</label>
</div>
</div>
<div className="mt-5 grid gap-3 md:grid-cols-3">
<div className="mt-5 grid gap-3 md:grid-cols-4">
<div className="rounded-2xl border border-line/70 bg-page/70 px-3 py-3">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted">Active</p>
<p className="mt-2 text-2xl font-bold text-text">{activeSessionCount}</p>
@@ -462,6 +527,10 @@ export function UserManagementPage() {
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-muted">Expired</p>
<p className="mt-2 text-2xl font-bold text-text">{expiredSessionCount}</p>
</div>
<div className="rounded-2xl border border-amber-300/60 bg-amber-50 px-3 py-3">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-amber-700">Needs Review</p>
<p className="mt-2 text-2xl font-bold text-amber-900">{reviewSessionCount}</p>
</div>
</div>
<div className="mt-5 grid gap-3">
@@ -474,6 +543,11 @@ export function UserManagementPage() {
<span className="rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
{session.status}
</span>
{session.reviewState === "REVIEW" ? (
<span className="rounded-full border border-amber-300/70 bg-amber-50 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-amber-800">
Review
</span>
) : null}
{session.isCurrent ? (
<span className="rounded-full bg-brand px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white">
Current
@@ -488,6 +562,15 @@ export function UserManagementPage() {
<p>IP: {session.ipAddress || "Unknown"}</p>
</div>
<p className="mt-2 text-xs text-muted">Agent: {session.userAgent || "Unknown"}</p>
{session.reviewReasons.length > 0 ? (
<div className="mt-3 flex flex-wrap gap-2">
{session.reviewReasons.map((reason) => (
<span key={reason} className="rounded-full border border-amber-300/70 bg-amber-50 px-2 py-1 text-[11px] font-semibold text-amber-800">
{reason}
</span>
))}
</div>
) : null}
{session.revokedAt ? (
<p className="mt-2 text-xs text-muted">
Revoked {new Date(session.revokedAt).toLocaleString()}
+29
View File
@@ -1,6 +1,7 @@
import { prisma } from "./prisma.js";
const SESSION_DURATION_MS = 12 * 60 * 60 * 1000;
const SESSION_RETENTION_DAYS = 30;
export interface AuthSessionContext {
id: string;
@@ -12,6 +13,10 @@ export function getSessionExpiryDate(now = new Date()) {
return new Date(now.getTime() + SESSION_DURATION_MS);
}
export function getSessionRetentionCutoff(now = new Date()) {
return new Date(now.getTime() - SESSION_RETENTION_DAYS * 24 * 60 * 60 * 1000);
}
export async function createAuthSession(input: { userId: string; ipAddress?: string | null; userAgent?: string | null }) {
return prisma.authSession.create({
data: {
@@ -69,3 +74,27 @@ export async function revokeAuthSession(sessionId: string, input: { revokedById?
},
});
}
export async function pruneOldAuthSessions() {
const cutoff = getSessionRetentionCutoff();
const result = await prisma.authSession.deleteMany({
where: {
OR: [
{
revokedAt: {
lt: cutoff,
},
},
{
revokedAt: null,
expiresAt: {
lt: cutoff,
},
},
],
},
});
return result.count;
}
+67 -2
View File
@@ -146,6 +146,10 @@ function mapAuthSession(
lastName: string;
} | null;
},
reviewContext: {
reviewState: "NORMAL" | "REVIEW";
reviewReasons: string[];
},
currentSessionId?: string
): AdminAuthSessionDto {
const now = Date.now();
@@ -157,6 +161,8 @@ function mapAuthSession(
userEmail: record.user.email,
userName: `${record.user.firstName} ${record.user.lastName}`.trim(),
status,
reviewState: reviewContext.reviewState,
reviewReasons: reviewContext.reviewReasons,
isCurrent: record.id === currentSessionId,
createdAt: record.createdAt.toISOString(),
lastSeenAt: record.lastSeenAt.toISOString(),
@@ -404,7 +410,63 @@ export async function listAdminAuthSessions(currentSessionId?: string | null): P
take: 200,
});
return sessions.map((session) => mapAuthSession(session, currentSessionId ?? undefined));
const now = Date.now();
const activeSessionsByUser = new Map<
string,
Array<{
id: string;
ipAddress: string | null;
userAgent: string | null;
lastSeenAt: Date;
}>
>();
for (const session of sessions) {
const isActive = !session.revokedAt && session.expiresAt.getTime() > now;
if (!isActive) {
continue;
}
const existing = activeSessionsByUser.get(session.userId) ?? [];
existing.push({
id: session.id,
ipAddress: session.ipAddress,
userAgent: session.userAgent,
lastSeenAt: session.lastSeenAt,
});
activeSessionsByUser.set(session.userId, existing);
}
return sessions.map((session) => {
const reviewReasons: string[] = [];
const activeUserSessions = activeSessionsByUser.get(session.userId) ?? [];
const isActive = !session.revokedAt && session.expiresAt.getTime() > now;
const staleThresholdMs = 7 * 24 * 60 * 60 * 1000;
if (isActive && activeUserSessions.length > 1) {
reviewReasons.push("Multiple active sessions");
}
if (isActive) {
const distinctIps = new Set(activeUserSessions.map((entry) => entry.ipAddress).filter(Boolean));
if (distinctIps.size > 1) {
reviewReasons.push("Multiple active IP addresses");
}
if (now - session.lastSeenAt.getTime() > staleThresholdMs) {
reviewReasons.push("Stale active session");
}
}
return mapAuthSession(
session,
{
reviewState: reviewReasons.length > 0 ? "REVIEW" : "NORMAL",
reviewReasons,
},
currentSessionId ?? undefined
);
});
}
export async function revokeAdminAuthSession(sessionId: string, actorId?: string | null) {
@@ -596,6 +658,8 @@ export async function updateAdminUser(userId: string, payload: AdminUserInput, a
export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
const startupReport = getLatestStartupReport();
const recentSupportLogs = listSupportLogs(50);
const now = new Date();
const reviewSessions = await listAdminAuthSessions();
const [
companyProfile,
userCount,
@@ -624,7 +688,7 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
where: {
revokedAt: null,
expiresAt: {
gt: new Date(),
gt: now,
},
},
}),
@@ -667,6 +731,7 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
userCount,
activeUserCount,
activeSessionCount,
reviewSessionCount: reviewSessions.filter((session) => session.reviewState === "REVIEW").length,
roleCount,
permissionCount,
customerCount,
+3
View File
@@ -1,5 +1,6 @@
import { createApp } from "./app.js";
import { env } from "./config/env.js";
import { pruneOldAuthSessions } from "./lib/auth-sessions.js";
import { bootstrapAppData } from "./lib/bootstrap.js";
import { prisma } from "./lib/prisma.js";
import { setLatestStartupReport } from "./lib/startup-state.js";
@@ -8,6 +9,7 @@ import { recordSupportLog } from "./lib/support-log.js";
async function start() {
await bootstrapAppData();
const prunedSessionCount = await pruneOldAuthSessions();
const startupReport = await assertStartupReadiness();
setLatestStartupReport(startupReport);
@@ -21,6 +23,7 @@ async function start() {
passCount: startupReport.passCount,
warnCount: startupReport.warnCount,
failCount: startupReport.failCount,
prunedSessionCount,
},
});
+3
View File
@@ -59,6 +59,8 @@ export interface AdminAuthSessionDto {
userEmail: string;
userName: string;
status: "ACTIVE" | "EXPIRED" | "REVOKED";
reviewState: "NORMAL" | "REVIEW";
reviewReasons: string[];
isCurrent: boolean;
createdAt: string;
lastSeenAt: string;
@@ -142,6 +144,7 @@ export interface AdminDiagnosticsDto {
userCount: number;
activeUserCount: number;
activeSessionCount: number;
reviewSessionCount: number;
roleCount: number;
permissionCount: number;
customerCount: number;