cleanup
This commit is contained in:
@@ -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
|
- 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
|
- 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 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
|
- 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, and attachment workflows
|
- 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
|
- 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/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
|
- 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)
|
- [INSTRUCTIONS.md](D:/CODING/mrp-codex/INSTRUCTIONS.md)
|
||||||
- [STRUCTURE.md](D:/CODING/mrp-codex/STRUCTURE.md)
|
- [STRUCTURE.md](D:/CODING/mrp-codex/STRUCTURE.md)
|
||||||
- [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.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)
|
- [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.
|
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:
|
Near-term priorities are:
|
||||||
|
|
||||||
1. Deeper session history, filtering, and admin-side access review polish
|
1. Support-log filtering, retention controls, and broader support-package polish
|
||||||
2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows
|
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.
|
When adding new modules, preserve the ability to extend the system without refactoring the existing app shell.
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ This file is the running release and change log for MRP Codex. Keep it updated w
|
|||||||
|
|
||||||
### Added
|
### 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
|
- 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
|
- 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 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
|
- 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
|
- 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
|
- 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
|
### 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
|
- 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
|
- 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
|
- The dashboard now treats Projects as a live first-class module alongside CRM, inventory, sales, and shipping
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
## Documentation maintenance
|
## 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.
|
- 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
|
## 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
|
- 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
|
- 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 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
|
- 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, and attachment workflows
|
- 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
|
- CRM/shipping audit coverage and startup validation surfaced through diagnostics
|
||||||
- backup/restore guidance, richer startup diagnostics, and exportable support bundles in diagnostics
|
- backup/restore guidance, richer startup diagnostics, and exportable support bundles in diagnostics
|
||||||
- backup verification checklist and restore-drill runbook 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
|
## Next roadmap candidates
|
||||||
|
|
||||||
- deeper session history, filtering, and admin-side access review polish
|
- support-log filtering, retention controls, and broader support-package polish
|
||||||
- extend destructive-action safety coverage into remaining project and form-edit removal workflows
|
- revision comparison UX for changed sales and purchasing documents
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -5,7 +5,7 @@ Foundation release for a modular Manufacturing Resource Planning platform built
|
|||||||
## Documentation Maintenance
|
## Documentation Maintenance
|
||||||
|
|
||||||
- Keep [CHANGELOG.md](D:/CODING/mrp-codex/CHANGELOG.md) updated for shipped features, workflow changes, and notable operational updates.
|
- 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:
|
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
|
- 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
|
- 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 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
|
- 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, and attachment workflows
|
- 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
|
- 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/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
|
- backup verification checklist and restore-drill runbook surfaced in admin diagnostics
|
||||||
@@ -42,6 +42,8 @@ Current foundation scope includes:
|
|||||||
|
|
||||||
## Product Map
|
## 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:
|
Current completed foundation areas:
|
||||||
|
|
||||||
- dashboard foundation
|
- dashboard foundation
|
||||||
@@ -58,14 +60,14 @@ Current completed foundation areas:
|
|||||||
|
|
||||||
Near-term priorities:
|
Near-term priorities:
|
||||||
|
|
||||||
1. Deeper session history, filtering, and admin-side access review polish
|
1. Support-log filtering, retention controls, and broader support-package polish
|
||||||
2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows
|
2. Revision comparison UX for changed sales and purchasing documents
|
||||||
|
|
||||||
Revisit / deferred items:
|
Revisit / deferred items:
|
||||||
|
|
||||||
- local Windows Prisma migration reliability
|
- local Windows Prisma migration reliability
|
||||||
- deeper session history, filtering, and admin-side access review polish
|
- support-log filtering, retention controls, and broader support-package polish
|
||||||
- safer destructive-action confirmations and recovery messaging
|
- revision comparison UX for changed sales and purchasing documents
|
||||||
|
|
||||||
Dashboard direction:
|
Dashboard direction:
|
||||||
|
|
||||||
@@ -354,6 +356,7 @@ As of March 15, 2026, the latest committed domain migrations include:
|
|||||||
- inventory transfers and reservations
|
- inventory transfers and reservations
|
||||||
- audit trail and diagnostics foundation
|
- audit trail and diagnostics foundation
|
||||||
- auth-session visibility and revocation
|
- 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
|
- 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.
|
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
|
- 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
|
- 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
|
- 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
|
- 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 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
|
- 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
|
- 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
|
- 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:
|
Current follow-up direction:
|
||||||
|
|
||||||
- deeper session history, filtering, and admin-side access review polish
|
- support-log filtering, retention controls, and broader support-package polish
|
||||||
- extend destructive-action safety coverage into remaining project and form-edit removal workflows
|
- revision comparison UX for changed sales and purchasing documents
|
||||||
|
|
||||||
## UI Notes
|
## UI Notes
|
||||||
|
|
||||||
|
|||||||
281
ROADMAP.md
281
ROADMAP.md
@@ -3,193 +3,83 @@
|
|||||||
## Documentation maintenance
|
## Documentation maintenance
|
||||||
|
|
||||||
- Keep [CHANGELOG.md](D:/CODING/mrp-codex/CHANGELOG.md) updated alongside roadmap-driving feature completion, priority shifts, and notable delivery milestones.
|
- 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.
|
- 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
|
## 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.
|
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`
|
1. Support-log filtering, retention controls, and broader support-package polish
|
||||||
- React + Vite + Tailwind frontend shell
|
2. Revision comparison UX for changed sales and purchasing documents
|
||||||
- Express + TypeScript backend shell
|
3. Project milestones, project rollups, and deeper project-side execution visibility
|
||||||
- Prisma + SQLite schema foundation with committed initial migration
|
4. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views
|
||||||
- Local authentication with JWT-based session flow plus persisted session visibility and revocation
|
5. Dashboard KPI, alert, recent-activity, and exception-widget expansion
|
||||||
- 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`
|
|
||||||
|
|
||||||
### 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
|
### Platform and operational docs
|
||||||
- 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
|
|
||||||
|
|
||||||
## 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
|
### Dashboard
|
||||||
- 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
|
|
||||||
|
|
||||||
## 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
|
- Better seed/bootstrap strategy for non-development environments
|
||||||
- Additional CRM account-role depth if later sales/purchasing workflows need it
|
- Additional CRM account-role depth if later sales/purchasing workflows need it
|
||||||
- More derived CRM rollups once quotes, orders, and purchasing documents exist
|
- More derived CRM rollups once downstream quote/order/purchasing/shipping data grows further
|
||||||
|
|
||||||
QOL subfeatures:
|
|
||||||
|
|
||||||
- Saved CRM filters and quick views
|
- Saved CRM filters and quick views
|
||||||
- Better hierarchy navigation between reseller parents and child accounts
|
- Better hierarchy navigation between reseller parents and child accounts
|
||||||
- One-click contact actions for email and phone workflows
|
- One-click contact actions for email and phone workflows
|
||||||
- Duplicate-account detection and merge workflow
|
- Duplicate-account detection and merge workflow
|
||||||
- Cleaner attachment previews and richer record timelines
|
- Cleaner attachment previews and richer record timelines
|
||||||
- More compact table controls for heavy CRM data-entry users
|
- 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
|
- Item master enrichment: categories, alternate part numbers, revisions, reorder settings, and broader sourcing metadata
|
||||||
- Warehouse and stock location foundation
|
- Faster keyboard-heavy item/BOM entry refinement beyond the current searchable pickers
|
||||||
- Inventory transactions and on-hand tracking foundation
|
- Better warehouse dashboards for on-hand, shortages, reservations, and recent movement
|
||||||
- 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
|
|
||||||
- BOM revision support and clearer where-used visibility
|
- BOM revision support and clearer where-used visibility
|
||||||
- Bulk item import/export and mass-update utilities
|
- Bulk item import/export and mass-update utilities
|
||||||
|
|
||||||
### Phase 3: Sales and purchasing documents
|
### Sales and purchasing
|
||||||
|
|
||||||
- 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:
|
|
||||||
|
|
||||||
|
- 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
|
- Line duplication, drag ordering, and keyboard-first line editing
|
||||||
- Saved customer defaults for tax, freight, and commercial terms
|
- 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
|
- Richer dashboard widgets for recent quotes, open orders, purchasing queues, and shipping exceptions
|
||||||
- Better totals breakdown visibility on list pages and detail pages
|
- 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
|
- 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
|
- Partial shipment workflow and split-shipment visibility
|
||||||
- Better tracking-link UX and carrier-specific shortcuts
|
- Better tracking-link UX and carrier-specific shortcuts
|
||||||
- Packing verification and ship-confirm checkpoints
|
- Packing verification and ship-confirm checkpoints
|
||||||
- Shipment search by order, tracking, customer, and carrier from one screen
|
- 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
|
### 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
|
|
||||||
|
|
||||||
- Project document hub for drawings, support files, correspondence, and revision references
|
- Project document hub for drawings, support files, correspondence, and revision references
|
||||||
- Milestones, checkpoints, and non-manufacturing work packages for long-running execution tracking
|
- Milestones, checkpoints, and non-manufacturing work packages for long-running execution tracking
|
||||||
- Project-level commercial, material, schedule, and delivery rollups
|
- Project-level commercial, material, schedule, and delivery rollups
|
||||||
- Cross-functional visibility for engineering, purchasing, manufacturing, shipping, and customer communication
|
- 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 templates for repeatable build types
|
||||||
- Project-specific attachment bundles and revision snapshots
|
- Project-specific attachment bundles and revision snapshots
|
||||||
- One-screen project cockpit with commercial, material, schedule, and shipping summary
|
- 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 filtering by customer, owner, status, due date, and risk
|
||||||
- Project activity timeline and audit-friendly milestone history
|
- Project activity timeline and audit-friendly milestone history
|
||||||
|
|
||||||
### Phase 6: Manufacturing execution
|
### Manufacturing execution
|
||||||
|
|
||||||
Foundation slice shipped:
|
- 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
|
||||||
- Work orders tied to manufactured or assembly items, with optional project linkage
|
- Material consumption depth, WIP tracking, and execution traceability
|
||||||
- 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
|
|
||||||
- Labor and machine-time capture for production execution
|
- Labor and machine-time capture for production execution
|
||||||
- Manufacturing status workflow from release through completion
|
|
||||||
- Manufacturing rollups for open work, blockers, shortages, and throughput
|
- 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
|
- Traveler/job packet output
|
||||||
- Partial completions and split-order execution visibility
|
- Partial completions and split-order execution visibility
|
||||||
- Better shortage and substitute-part handling
|
- Better shortage and substitute-part handling
|
||||||
@@ -232,22 +101,12 @@ QOL subfeatures:
|
|||||||
- Rework / hold / scrap tracking
|
- Rework / hold / scrap tracking
|
||||||
- Work-center dashboards and operator-focused queues
|
- 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
|
- Task dependencies, milestones, and progress updates
|
||||||
- Manufacturing calendar views and bottleneck visibility
|
- Manufacturing calendar views and bottleneck visibility
|
||||||
- Labor and machine scheduling support
|
- Labor and machine scheduling support
|
||||||
- Theme-compliant gantt customization for light/dark mode
|
- Theme-compliant gantt customization for light/dark mode
|
||||||
|
|
||||||
QOL subfeatures:
|
|
||||||
|
|
||||||
- Collapsible schedule groupings and saved planner views
|
- Collapsible schedule groupings and saved planner views
|
||||||
- Drag-and-drop rescheduling improvements
|
- Drag-and-drop rescheduling improvements
|
||||||
- Critical-path and overdue highlighting
|
- Critical-path and overdue highlighting
|
||||||
@@ -255,68 +114,20 @@ QOL subfeatures:
|
|||||||
- Better mobile and tablet behavior for shop-floor lookups
|
- Better mobile and tablet behavior for shop-floor lookups
|
||||||
- Faster filtering by project, customer, work center, and status
|
- Faster filtering by project, customer, work center, and status
|
||||||
|
|
||||||
### Phase 8: Demand planning and supply generation
|
### Demand planning and supply generation
|
||||||
|
|
||||||
Foundation slice shipped:
|
- Deeper planner drilldowns from demand source to buy/build action without re-keying data
|
||||||
|
|
||||||
- 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
|
|
||||||
- Better shortage and substitute-part guidance during planning review
|
- Better shortage and substitute-part guidance during planning review
|
||||||
- Saved planning views by customer, project, item family, and shortage state
|
- 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
|
- 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
|
- Support-log filtering, retention controls, and broader support-package polish
|
||||||
- Backup verification checklist and restore drill guidance
|
- 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
|
||||||
## Revisit / Deferred Items
|
- More explicit environment validation on startup
|
||||||
|
- Backup verification and restore-drill guidance should keep expanding as the system grows
|
||||||
- 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
|
|
||||||
|
|
||||||
## Cross-cutting improvements
|
## Cross-cutting improvements
|
||||||
|
|
||||||
@@ -327,7 +138,7 @@ QOL subfeatures:
|
|||||||
- Consistent document-template system shared by sales, purchasing, and shipping
|
- Consistent document-template system shared by sales, purchasing, and shipping
|
||||||
- Clear upgrade path for future module additions without refactoring the app shell
|
- 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
|
- Local Windows Prisma migration reliability still needs a cleaner documented workflow or tooling wrapper
|
||||||
2. Extend destructive-action safety coverage into remaining project and form-edit removal workflows
|
- Some generated document and workflow screens still need additional polish for dense, keyboard-efficient operational use
|
||||||
|
|||||||
122
SHIPPED.md
Normal file
122
SHIPPED.md
Normal 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 { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
|
||||||
import { useAuth } from "../../auth/AuthProvider";
|
import { useAuth } from "../../auth/AuthProvider";
|
||||||
import { api, ApiError } from "../../lib/api";
|
import { api, ApiError } from "../../lib/api";
|
||||||
import { emptyInventoryBomLineInput, emptyInventoryItemInput, emptyInventoryOperationInput, inventoryStatusOptions, inventoryTypeOptions, inventoryUnitOptions } from "./config";
|
import { emptyInventoryBomLineInput, emptyInventoryItemInput, emptyInventoryOperationInput, inventoryStatusOptions, inventoryTypeOptions, inventoryUnitOptions } from "./config";
|
||||||
@@ -26,6 +27,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
|||||||
const [vendorPickerOpen, setVendorPickerOpen] = useState(false);
|
const [vendorPickerOpen, setVendorPickerOpen] = useState(false);
|
||||||
const [status, setStatus] = useState(mode === "create" ? "Create a new inventory item." : "Loading inventory item...");
|
const [status, setStatus] = useState(mode === "create" ? "Create a new inventory item." : "Loading inventory item...");
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [pendingRemoval, setPendingRemoval] = useState<{ kind: "operation" | "bom-line"; index: number } | null>(null);
|
||||||
|
|
||||||
function getComponentOption(componentItemId: string) {
|
function getComponentOption(componentItemId: string) {
|
||||||
return componentOptions.find((option) => option.id === componentItemId) ?? null;
|
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));
|
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>) {
|
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!token) {
|
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" />
|
<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>
|
</label>
|
||||||
<div className="flex items-end">
|
<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
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -619,7 +627,7 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
|||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<button
|
<button
|
||||||
type="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"
|
className="rounded-2xl border border-rose-400/40 px-2 py-2 text-sm font-semibold text-rose-700 dark:text-rose-300"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
@@ -649,6 +657,31 @@ export function InventoryFormPage({ mode }: InventoryFormPageProps) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { WarehouseInput, WarehouseLocationInput } from "@mrp/shared/dist/in
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
|
||||||
import { useAuth } from "../../auth/AuthProvider";
|
import { useAuth } from "../../auth/AuthProvider";
|
||||||
import { api, ApiError } from "../../lib/api";
|
import { api, ApiError } from "../../lib/api";
|
||||||
import { emptyWarehouseInput, emptyWarehouseLocationInput } from "./config";
|
import { emptyWarehouseInput, emptyWarehouseLocationInput } from "./config";
|
||||||
@@ -13,6 +14,7 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
const [form, setForm] = useState<WarehouseInput>(emptyWarehouseInput);
|
const [form, setForm] = useState<WarehouseInput>(emptyWarehouseInput);
|
||||||
const [status, setStatus] = useState(mode === "create" ? "Create a new warehouse." : "Loading warehouse...");
|
const [status, setStatus] = useState(mode === "create" ? "Create a new warehouse." : "Loading warehouse...");
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [pendingLocationRemovalIndex, setPendingLocationRemovalIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== "edit" || !token || !warehouseId) {
|
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>) {
|
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!token) {
|
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" />
|
<input value={location.name} onChange={(event) => updateLocation(index, { ...location, name: event.target.value })} className="w-full rounded-2xl border border-line/70 bg-surface px-2 py-2 text-text outline-none transition focus:border-brand" />
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-end">
|
<div className="flex items-end">
|
||||||
<button type="button" onClick={() => removeLocation(index)} className="rounded-2xl border border-rose-400/40 px-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
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -167,6 +171,21 @@ export function WarehouseFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,17 @@ import type {
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
|
||||||
import { useAuth } from "../../auth/AuthProvider";
|
import { useAuth } from "../../auth/AuthProvider";
|
||||||
import { api, ApiError } from "../../lib/api";
|
import { api, ApiError } from "../../lib/api";
|
||||||
import { emptyProjectInput, projectPriorityOptions, projectStatusOptions } from "./config";
|
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" }) {
|
export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
|
||||||
const { token, user } = useAuth();
|
const { token, user } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -34,6 +41,7 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
const [shipmentPickerOpen, setShipmentPickerOpen] = useState(false);
|
const [shipmentPickerOpen, setShipmentPickerOpen] = useState(false);
|
||||||
const [status, setStatus] = useState(mode === "create" ? "Create a new project." : "Loading project...");
|
const [status, setStatus] = useState(mode === "create" ? "Create a new project." : "Loading project...");
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [pendingConfirmation, setPendingConfirmation] = useState<ProjectPendingConfirmation | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!token) {
|
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() {
|
function restoreSearchTerms() {
|
||||||
const selectedCustomer = customerOptions.find((customer) => customer.id === form.customerId);
|
const selectedCustomer = customerOptions.find((customer) => customer.id === form.customerId);
|
||||||
const selectedOwner = ownerOptions.find((owner) => owner.id === form.ownerId);
|
const selectedOwner = ownerOptions.find((owner) => owner.id === form.ownerId);
|
||||||
@@ -162,7 +207,6 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
value={customerSearchTerm}
|
value={customerSearchTerm}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setCustomerSearchTerm(event.target.value);
|
setCustomerSearchTerm(event.target.value);
|
||||||
updateField("customerId", "");
|
|
||||||
setCustomerPickerOpen(true);
|
setCustomerPickerOpen(true);
|
||||||
}}
|
}}
|
||||||
onFocus={() => setCustomerPickerOpen(true)}
|
onFocus={() => setCustomerPickerOpen(true)}
|
||||||
@@ -187,9 +231,7 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
.map((customer) => (
|
.map((customer) => (
|
||||||
<button key={customer.id} type="button" onMouseDown={(event) => {
|
<button key={customer.id} type="button" onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateField("customerId", customer.id);
|
requestCustomerSelection(customer.id, customer.name);
|
||||||
setCustomerSearchTerm(customer.name);
|
|
||||||
setCustomerPickerOpen(false);
|
|
||||||
}} 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">
|
}} 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="font-semibold text-text">{customer.name}</div>
|
||||||
<div className="mt-1 text-xs text-muted">{customer.email}</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}
|
value={quoteSearchTerm}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setQuoteSearchTerm(event.target.value);
|
setQuoteSearchTerm(event.target.value);
|
||||||
updateField("salesQuoteId", null);
|
|
||||||
setQuotePickerOpen(true);
|
setQuotePickerOpen(true);
|
||||||
}}
|
}}
|
||||||
onFocus={() => 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">
|
<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) => {
|
<button type="button" onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateField("salesQuoteId", null);
|
if (form.salesQuoteId) {
|
||||||
setQuoteSearchTerm("");
|
setPendingConfirmation({ kind: "unlink-quote" });
|
||||||
setQuotePickerOpen(false);
|
} else {
|
||||||
|
unlinkQuote();
|
||||||
|
}
|
||||||
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition hover:bg-page/70">
|
}} 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>
|
<div className="font-semibold text-text">No linked quote</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -330,7 +373,6 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
value={orderSearchTerm}
|
value={orderSearchTerm}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setOrderSearchTerm(event.target.value);
|
setOrderSearchTerm(event.target.value);
|
||||||
updateField("salesOrderId", null);
|
|
||||||
setOrderPickerOpen(true);
|
setOrderPickerOpen(true);
|
||||||
}}
|
}}
|
||||||
onFocus={() => 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">
|
<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) => {
|
<button type="button" onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateField("salesOrderId", null);
|
if (form.salesOrderId) {
|
||||||
setOrderSearchTerm("");
|
setPendingConfirmation({ kind: "unlink-order" });
|
||||||
setOrderPickerOpen(false);
|
} else {
|
||||||
|
unlinkOrder();
|
||||||
|
}
|
||||||
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition hover:bg-page/70">
|
}} 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>
|
<div className="font-semibold text-text">No linked sales order</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -382,7 +426,6 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
value={shipmentSearchTerm}
|
value={shipmentSearchTerm}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setShipmentSearchTerm(event.target.value);
|
setShipmentSearchTerm(event.target.value);
|
||||||
updateField("shipmentId", null);
|
|
||||||
setShipmentPickerOpen(true);
|
setShipmentPickerOpen(true);
|
||||||
}}
|
}}
|
||||||
onFocus={() => 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">
|
<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) => {
|
<button type="button" onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
updateField("shipmentId", null);
|
if (form.shipmentId) {
|
||||||
setShipmentSearchTerm("");
|
setPendingConfirmation({ kind: "unlink-shipment" });
|
||||||
setShipmentPickerOpen(false);
|
} else {
|
||||||
|
unlinkShipment();
|
||||||
|
}
|
||||||
}} className="block w-full border-b border-line/50 px-2 py-2 text-left text-sm transition hover:bg-page/70">
|
}} 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>
|
<div className="font-semibold text-text">No linked shipment</div>
|
||||||
</button>
|
</button>
|
||||||
@@ -439,6 +484,60 @@ export function ProjectFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { InventoryItemOptionDto, PurchaseLineInput, PurchaseOrderInput, Pur
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
|
||||||
import { useAuth } from "../../auth/AuthProvider";
|
import { useAuth } from "../../auth/AuthProvider";
|
||||||
import { api, ApiError } from "../../lib/api";
|
import { api, ApiError } from "../../lib/api";
|
||||||
import { inventoryUnitOptions } from "../inventory/config";
|
import { inventoryUnitOptions } from "../inventory/config";
|
||||||
@@ -24,6 +25,7 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
const [lineSearchTerms, setLineSearchTerms] = useState<string[]>([]);
|
const [lineSearchTerms, setLineSearchTerms] = useState<string[]>([]);
|
||||||
const [activeLinePicker, setActiveLinePicker] = useState<number | null>(null);
|
const [activeLinePicker, setActiveLinePicker] = useState<number | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [pendingLineRemovalIndex, setPendingLineRemovalIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
function collectRecommendedPurchaseNodes(node: SalesOrderPlanningNodeDto): SalesOrderPlanningNodeDto[] {
|
function collectRecommendedPurchaseNodes(node: SalesOrderPlanningNodeDto): SalesOrderPlanningNodeDto[] {
|
||||||
const nodes = node.recommendedPurchaseQuantity > 0 ? [node] : [];
|
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));
|
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>) {
|
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!token) {
|
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" />
|
<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>
|
</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"><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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -444,6 +455,25 @@ export function PurchaseFormPage({ mode }: { mode: "create" | "edit" }) {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { SalesCustomerOptionDto, SalesDocumentDetailDto, SalesDocumentInput
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { ConfirmActionDialog } from "../../components/ConfirmActionDialog";
|
||||||
import { useAuth } from "../../auth/AuthProvider";
|
import { useAuth } from "../../auth/AuthProvider";
|
||||||
import { api, ApiError } from "../../lib/api";
|
import { api, ApiError } from "../../lib/api";
|
||||||
import { inventoryUnitOptions } from "../inventory/config";
|
import { inventoryUnitOptions } from "../inventory/config";
|
||||||
@@ -23,6 +24,7 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
|
|||||||
const [lineSearchTerms, setLineSearchTerms] = useState<string[]>([]);
|
const [lineSearchTerms, setLineSearchTerms] = useState<string[]>([]);
|
||||||
const [activeLinePicker, setActiveLinePicker] = useState<number | null>(null);
|
const [activeLinePicker, setActiveLinePicker] = useState<number | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
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 subtotal = form.lines.reduce((sum, line) => sum + line.quantity * line.unitPrice, 0);
|
||||||
const discountAmount = subtotal * (form.discountPercent / 100);
|
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));
|
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>) {
|
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@@ -431,7 +442,7 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<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
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -465,6 +476,26 @@ export function SalesFormPage({ entity, mode }: { entity: SalesDocumentEntity; m
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
["Audit events", diagnostics.auditEventCount.toString()],
|
["Audit events", diagnostics.auditEventCount.toString()],
|
||||||
["Support logs", diagnostics.supportLogCount.toString()],
|
["Support logs", diagnostics.supportLogCount.toString()],
|
||||||
["Active users", `${diagnostics.activeUserCount} / ${diagnostics.userCount}`],
|
["Active users", `${diagnostics.activeUserCount} / ${diagnostics.userCount}`],
|
||||||
|
["Sessions to review", diagnostics.reviewSessionCount.toString()],
|
||||||
["Sales docs", diagnostics.salesDocumentCount.toString()],
|
["Sales docs", diagnostics.salesDocumentCount.toString()],
|
||||||
["Work orders", diagnostics.workOrderCount.toString()],
|
["Work orders", diagnostics.workOrderCount.toString()],
|
||||||
["Projects", diagnostics.projectCount.toString()],
|
["Projects", diagnostics.projectCount.toString()],
|
||||||
@@ -108,6 +109,7 @@ export function AdminDiagnosticsPage() {
|
|||||||
["Uploads directory", diagnostics.uploadsDir],
|
["Uploads directory", diagnostics.uploadsDir],
|
||||||
["Client origin", diagnostics.clientOrigin],
|
["Client origin", diagnostics.clientOrigin],
|
||||||
["Company profile", diagnostics.companyProfilePresent ? "Present" : "Missing"],
|
["Company profile", diagnostics.companyProfilePresent ? "Present" : "Missing"],
|
||||||
|
["Active sessions", diagnostics.activeSessionCount.toString()],
|
||||||
["Roles / permissions", `${diagnostics.roleCount} / ${diagnostics.permissionCount}`],
|
["Roles / permissions", `${diagnostics.roleCount} / ${diagnostics.permissionCount}`],
|
||||||
["Customers / vendors", `${diagnostics.customerCount} / ${diagnostics.vendorCount}`],
|
["Customers / vendors", `${diagnostics.customerCount} / ${diagnostics.vendorCount}`],
|
||||||
["Inventory / warehouses", `${diagnostics.inventoryItemCount} / ${diagnostics.warehouseCount}`],
|
["Inventory / warehouses", `${diagnostics.inventoryItemCount} / ${diagnostics.warehouseCount}`],
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export function UserManagementPage() {
|
|||||||
const [selectedUserId, setSelectedUserId] = useState<string>("new");
|
const [selectedUserId, setSelectedUserId] = useState<string>("new");
|
||||||
const [selectedRoleId, setSelectedRoleId] = useState<string>("new");
|
const [selectedRoleId, setSelectedRoleId] = useState<string>("new");
|
||||||
const [sessionUserFilter, setSessionUserFilter] = useState<string>("all");
|
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 [userForm, setUserForm] = useState<AdminUserInput>(emptyUserForm);
|
||||||
const [roleForm, setRoleForm] = useState<AdminRoleInput>(emptyRoleForm);
|
const [roleForm, setRoleForm] = useState<AdminRoleInput>(emptyRoleForm);
|
||||||
const [status, setStatus] = useState("Loading admin access controls...");
|
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.");
|
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 activeSessionCount = sessions.filter((session) => session.status === "ACTIVE").length;
|
||||||
const revokedSessionCount = sessions.filter((session) => session.status === "REVOKED").length;
|
const revokedSessionCount = sessions.filter((session) => session.status === "REVOKED").length;
|
||||||
const expiredSessionCount = sessions.filter((session) => session.status === "EXPIRED").length;
|
const expiredSessionCount = sessions.filter((session) => session.status === "EXPIRED").length;
|
||||||
|
const reviewSessionCount = sessions.filter((session) => session.reviewState === "REVIEW").length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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.
|
Review recent authenticated sessions, see their current state, and revoke stale or risky access without changing the user record.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-4">
|
||||||
<label className="block">
|
<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
|
<select
|
||||||
value={sessionUserFilter}
|
value={sessionUserFilter}
|
||||||
onChange={(event) => setSessionUserFilter(event.target.value)}
|
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>
|
<option value="all">All users</option>
|
||||||
{users.map((user) => (
|
{users.map((user) => (
|
||||||
@@ -447,9 +486,35 @@ export function UserManagementPage() {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
<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">
|
<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="text-xs font-semibold uppercase tracking-[0.2em] text-muted">Active</p>
|
||||||
<p className="mt-2 text-2xl font-bold text-text">{activeSessionCount}</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="text-xs font-semibold uppercase tracking-[0.2em] text-muted">Expired</p>
|
||||||
<p className="mt-2 text-2xl font-bold text-text">{expiredSessionCount}</p>
|
<p className="mt-2 text-2xl font-bold text-text">{expiredSessionCount}</p>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="mt-5 grid gap-3">
|
<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">
|
<span className="rounded-full border border-line/70 px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-muted">
|
||||||
{session.status}
|
{session.status}
|
||||||
</span>
|
</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 ? (
|
{session.isCurrent ? (
|
||||||
<span className="rounded-full bg-brand px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white">
|
<span className="rounded-full bg-brand px-2 py-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-white">
|
||||||
Current
|
Current
|
||||||
@@ -488,6 +562,15 @@ export function UserManagementPage() {
|
|||||||
<p>IP: {session.ipAddress || "Unknown"}</p>
|
<p>IP: {session.ipAddress || "Unknown"}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs text-muted">Agent: {session.userAgent || "Unknown"}</p>
|
<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 ? (
|
{session.revokedAt ? (
|
||||||
<p className="mt-2 text-xs text-muted">
|
<p className="mt-2 text-xs text-muted">
|
||||||
Revoked {new Date(session.revokedAt).toLocaleString()}
|
Revoked {new Date(session.revokedAt).toLocaleString()}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { prisma } from "./prisma.js";
|
import { prisma } from "./prisma.js";
|
||||||
|
|
||||||
const SESSION_DURATION_MS = 12 * 60 * 60 * 1000;
|
const SESSION_DURATION_MS = 12 * 60 * 60 * 1000;
|
||||||
|
const SESSION_RETENTION_DAYS = 30;
|
||||||
|
|
||||||
export interface AuthSessionContext {
|
export interface AuthSessionContext {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -12,6 +13,10 @@ export function getSessionExpiryDate(now = new Date()) {
|
|||||||
return new Date(now.getTime() + SESSION_DURATION_MS);
|
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 }) {
|
export async function createAuthSession(input: { userId: string; ipAddress?: string | null; userAgent?: string | null }) {
|
||||||
return prisma.authSession.create({
|
return prisma.authSession.create({
|
||||||
data: {
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ function mapAuthSession(
|
|||||||
lastName: string;
|
lastName: string;
|
||||||
} | null;
|
} | null;
|
||||||
},
|
},
|
||||||
|
reviewContext: {
|
||||||
|
reviewState: "NORMAL" | "REVIEW";
|
||||||
|
reviewReasons: string[];
|
||||||
|
},
|
||||||
currentSessionId?: string
|
currentSessionId?: string
|
||||||
): AdminAuthSessionDto {
|
): AdminAuthSessionDto {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -157,6 +161,8 @@ function mapAuthSession(
|
|||||||
userEmail: record.user.email,
|
userEmail: record.user.email,
|
||||||
userName: `${record.user.firstName} ${record.user.lastName}`.trim(),
|
userName: `${record.user.firstName} ${record.user.lastName}`.trim(),
|
||||||
status,
|
status,
|
||||||
|
reviewState: reviewContext.reviewState,
|
||||||
|
reviewReasons: reviewContext.reviewReasons,
|
||||||
isCurrent: record.id === currentSessionId,
|
isCurrent: record.id === currentSessionId,
|
||||||
createdAt: record.createdAt.toISOString(),
|
createdAt: record.createdAt.toISOString(),
|
||||||
lastSeenAt: record.lastSeenAt.toISOString(),
|
lastSeenAt: record.lastSeenAt.toISOString(),
|
||||||
@@ -404,7 +410,63 @@ export async function listAdminAuthSessions(currentSessionId?: string | null): P
|
|||||||
take: 200,
|
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) {
|
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> {
|
export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
||||||
const startupReport = getLatestStartupReport();
|
const startupReport = getLatestStartupReport();
|
||||||
const recentSupportLogs = listSupportLogs(50);
|
const recentSupportLogs = listSupportLogs(50);
|
||||||
|
const now = new Date();
|
||||||
|
const reviewSessions = await listAdminAuthSessions();
|
||||||
const [
|
const [
|
||||||
companyProfile,
|
companyProfile,
|
||||||
userCount,
|
userCount,
|
||||||
@@ -624,7 +688,7 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
|||||||
where: {
|
where: {
|
||||||
revokedAt: null,
|
revokedAt: null,
|
||||||
expiresAt: {
|
expiresAt: {
|
||||||
gt: new Date(),
|
gt: now,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
@@ -667,6 +731,7 @@ export async function getAdminDiagnostics(): Promise<AdminDiagnosticsDto> {
|
|||||||
userCount,
|
userCount,
|
||||||
activeUserCount,
|
activeUserCount,
|
||||||
activeSessionCount,
|
activeSessionCount,
|
||||||
|
reviewSessionCount: reviewSessions.filter((session) => session.reviewState === "REVIEW").length,
|
||||||
roleCount,
|
roleCount,
|
||||||
permissionCount,
|
permissionCount,
|
||||||
customerCount,
|
customerCount,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { createApp } from "./app.js";
|
import { createApp } from "./app.js";
|
||||||
import { env } from "./config/env.js";
|
import { env } from "./config/env.js";
|
||||||
|
import { pruneOldAuthSessions } from "./lib/auth-sessions.js";
|
||||||
import { bootstrapAppData } from "./lib/bootstrap.js";
|
import { bootstrapAppData } from "./lib/bootstrap.js";
|
||||||
import { prisma } from "./lib/prisma.js";
|
import { prisma } from "./lib/prisma.js";
|
||||||
import { setLatestStartupReport } from "./lib/startup-state.js";
|
import { setLatestStartupReport } from "./lib/startup-state.js";
|
||||||
@@ -8,6 +9,7 @@ import { recordSupportLog } from "./lib/support-log.js";
|
|||||||
|
|
||||||
async function start() {
|
async function start() {
|
||||||
await bootstrapAppData();
|
await bootstrapAppData();
|
||||||
|
const prunedSessionCount = await pruneOldAuthSessions();
|
||||||
const startupReport = await assertStartupReadiness();
|
const startupReport = await assertStartupReadiness();
|
||||||
setLatestStartupReport(startupReport);
|
setLatestStartupReport(startupReport);
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ async function start() {
|
|||||||
passCount: startupReport.passCount,
|
passCount: startupReport.passCount,
|
||||||
warnCount: startupReport.warnCount,
|
warnCount: startupReport.warnCount,
|
||||||
failCount: startupReport.failCount,
|
failCount: startupReport.failCount,
|
||||||
|
prunedSessionCount,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ export interface AdminAuthSessionDto {
|
|||||||
userEmail: string;
|
userEmail: string;
|
||||||
userName: string;
|
userName: string;
|
||||||
status: "ACTIVE" | "EXPIRED" | "REVOKED";
|
status: "ACTIVE" | "EXPIRED" | "REVOKED";
|
||||||
|
reviewState: "NORMAL" | "REVIEW";
|
||||||
|
reviewReasons: string[];
|
||||||
isCurrent: boolean;
|
isCurrent: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
lastSeenAt: string;
|
lastSeenAt: string;
|
||||||
@@ -142,6 +144,7 @@ export interface AdminDiagnosticsDto {
|
|||||||
userCount: number;
|
userCount: number;
|
||||||
activeUserCount: number;
|
activeUserCount: number;
|
||||||
activeSessionCount: number;
|
activeSessionCount: number;
|
||||||
|
reviewSessionCount: number;
|
||||||
roleCount: number;
|
roleCount: number;
|
||||||
permissionCount: number;
|
permissionCount: number;
|
||||||
customerCount: number;
|
customerCount: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user