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