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

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.

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

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

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

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
SHIPPED.md Normal file
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

View File

@@ -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>
);
}

View File

@@ -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>
);
}

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>
);
}

View File

@@ -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>
);
}

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>
);
}

View File

@@ -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}`],

View File

@@ -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()}

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;
}

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,

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,
},
});

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;