commit 3d05e3929d46c118dfa0f68a0c594fc41b1a516f Author: jason Date: Mon Mar 16 14:38:00 2026 -0500 init diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c51e2c6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitignore +node_modules +client/node_modules +server/node_modules +shared/node_modules +client/dist +server/dist +data +coverage +*.log + diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..fe9cf3b --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +PORT=3000 +JWT_SECRET=change-me +DATABASE_URL="file:../../data/prisma/app.db" +DATA_DIR="./data" +CLIENT_ORIGIN="http://localhost:5173" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3be3afa --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +node_modules +dist +build +.vite +.turbo +coverage +.tsbuildinfo +*.tsbuildinfo +.env +.env.* +!.env.example +data +uploads +client/dist +server/dist +server/prisma/dev.db +server/prisma/dev.db-journal +client/src/**/*.js +client/src/**/*.d.ts +server/tests/**/*.js +client/tailwind.config.js +client/tailwind.config.d.ts +client/vite.config.js +client/vite.config.d.ts +*.log diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..785c42f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,168 @@ +# AGENTS.md + +## Purpose + +This file defines project-specific guidance for future contributors and coding agents working in this repository. Follow it alongside the main project docs. + +## Project overview + +CODEXIUM is a modular Manufacturing Resource Planning platform intended to be a lighter, sleeker alternative to Odoo. The current repository contains the foundation release: + +- React + Vite + Tailwind frontend +- Express + TypeScript backend +- Prisma + SQLite persistence +- local JWT auth and RBAC +- Company Settings and runtime branding +- filesystem-backed attachments +- CRM customers/vendors, hierarchy, contacts, lifecycle metadata, and attachments +- inventory items, BOMs, warehouses, locations, transactions, item attachments, and item pricing +- inventory transfers, reservations, available-stock visibility, and work-order reservation automation +- inventory SKU master builder with family-scoped sequence generation and branch-aware taxonomy management +- inventory thumbnail image staging on create/edit and dedicated thumbnail display on item detail +- sales quotes, sales orders, approvals, revision history/comparison, and purchase orders +- purchase-order revision history and revision comparison across document and receipt changes +- purchase-order supporting documents and vendor-side purchasing visibility +- shipping shipments, packing-slip PDFs, shipping labels, bills of lading, and logistics attachments +- projects with customer/commercial/shipment linkage, owners, due dates, notes, and attachments +- manufacturing work orders with project linkage, station master data, item operation templates, auto-generated work-order operations, and attachments +- planning gantt timelines backed by live project and manufacturing schedule data +- sales-order demand planning with multi-level BOM explosion, stock/open-supply netting, and build/buy recommendations +- planner-assisted conversion of demand-planning recommendations into prefilled work-order and purchase-order drafts +- 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, 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 +- support-log viewing, filtering, retention cleanup, and support debugging helpers in the admin diagnostics workflow +- startup brand-theme hydration so Company Settings colors persist across refresh +- Puppeteer PDF foundation +- single-container Docker deployment + +## Source of truth documents + +Read these before major work: + +- [CHANGELOG.md](D:/CODING/mrp-codex/CHANGELOG.md) +- [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) +- [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. + +## Architecture rules + +### Keep the app modular by domain + +- Backend modules belong under `server/src/modules/` +- Frontend modules belong under `client/src/modules/` +- Shared contracts belong under `shared/src` +- Do not collapse unrelated business logic into the app shell or generic utility folders + +### Backend boundaries + +- Keep routers thin +- Put business logic in services +- Put persistence access behind Prisma usage in module services or focused helpers +- Keep auth, RBAC, storage, and Prisma setup in `server/src/lib` +- Keep environment and path configuration in `server/src/config` + +### Frontend boundaries + +- Shared UI primitives go in `client/src/components` +- Theme logic goes in `client/src/theme` +- Authentication state goes in `client/src/auth` +- Route-level business pages go in `client/src/modules` +- Do not mix PDF template concerns into normal UI pages + +### Shared package constraints + +- `shared` must stay framework-agnostic +- Use explicit `.js` relative exports/imports in `shared/src` because it ships as Node ESM +- Keep DTOs, permission keys, and cross-app types there + +## Data and persistence rules + +- SQLite database must live under `/app/data/prisma/app.db` +- Uploaded files must live under `/app/data/uploads` +- Never store file blobs in SQLite +- Store metadata and relative paths only +- Any persisted schema change must include a Prisma migration in `server/prisma/migrations` + +## Prisma rules + +- Run `npm run prisma:generate` after Prisma schema changes +- Use committed migrations as the source of truth +- Prefer Node 22 or Docker for Prisma migration execution +- Prisma client generation for Docker must continue to support the runtime binary target: + - `debian-openssl-3.0.x` +- Do not remove the current `binaryTargets` setting from `server/prisma/schema.prisma` unless the base image changes and the runtime target is updated intentionally + +## Docker rules + +- The Dockerfile is designed for command-line builds from the repo root +- Do not reintroduce Puppeteer browser downloads during image build +- The runtime image uses system Chromium at `/usr/bin/chromium` +- Container startup must continue to apply Prisma migrations before launching the app +- If Docker/runtime dependency handling changes, verify: + - Prisma binary is present + - Prisma client is generated in the runtime image + - shared ESM output resolves correctly in Node + +## UI and product rules + +- The application must remain brandable through centralized theme tokens and Company Settings +- Light and dark mode must remain first-class, not bolted on later +- New UI should respect the theme system and avoid hardcoded one-off colors where possible +- Keep the interface intentional and operational, not generic admin-template filler +- Non-filter operational lookups must use searchable pickers/autocomplete instead of long static dropdowns +- Keep the denser UI baseline on active screens unless a specific workflow needs more space +- Inventory items maintain both cost and price; sales entry should default from item price +- Purchase-order item lookup must only expose inventory items flagged as purchasable +- Customer-facing and logistics PDFs should continue to use the backend documents module and Puppeteer pipeline +- The landing experience should remain `Dashboard`, not `Overview`, and should evolve as a modular metric-first operational surface +- Projects are a first-class domain that anchors long-running program execution across CRM, sales, inventory, purchasing, shipping, and planning, and future work should continue extending that module rather than scattering project state elsewhere +- Manufacturing is now a first-class domain for work orders and inventory-backed execution, and future work should keep expanding it as a separate subsystem for routings, labor, and shop-floor control +- Planning should remain the scheduling/visibility layer over projects and manufacturing, not a replacement for either +- New top-level modules added to shell navigation should ship with a matching SVG icon, not text-only nav entries + +## Feature expectations + +Near-term priorities are: + +1. Project milestones and project-side rollup visibility +2. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views + +When adding new modules, preserve the ability to extend the system without refactoring the existing app shell. + +## Testing and verification + +Before closing substantial work: + +- run `npm run build` +- run `npm run test` +- if Docker-related code changed, rebuild the image if the environment allows it +- if Prisma schema changed, regenerate the client and confirm migrations are present + +If you cannot run one of those checks, say so explicitly. + +## Git and workflow expectations + +- Keep commits focused and source-only; do not commit generated local build artifacts +- Update roadmap/docs and `CHANGELOG.md` when major work shifts priorities, architecture, or shipped functionality +- Do not remove or overwrite user changes without explicit instruction +- If a task reveals a persistent operational issue, document it rather than leaving it tribal knowledge + +## Known pitfalls already encountered + +- `npx prisma` from `/app` did not resolve correctly in the container; the entrypoint uses the server workspace binary directly +- Prisma client must be generated in the production dependency stage of the Docker build +- Prisma runtime on Debian bookworm requires `debian-openssl-3.0.x` +- `shared` package exports must use Node ESM-compatible `.js` specifiers +- Local Docker validation may fail if the Docker daemon is unavailable; distinguish daemon issues from image issues + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..53ac2af --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,151 @@ +# Changelog + +This file is the running release and change log for CODEXIUM. Keep it updated whenever shipped functionality, architecture expectations, deployment behavior, or operator-facing workflows materially change. + +## Unreleased + +### Added + +- Inventory SKU master builder with family-level sequence codes, branch-aware taxonomy management, and generated SKU previews on the item form +- Thumbnail image attachment staging on inventory item create/edit pages, with upload-on-save and replacement/removal support +- Dedicated inventory-item thumbnail display on the item detail page +- Revision comparison views for sales quotes, sales orders, and purchase orders with field- and line-level before/after diffs +- Purchase-order revision snapshots covering document edits, status changes, and receipt posting +- 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 +- Support-log filtering by severity, source, search text, and retention window in admin diagnostics +- Support-log export and support-snapshot export now carry filter context, summary counts, available sources, and retention metadata +- 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 +- Prefilled work-order draft launch for build recommendations and prefilled purchase-order draft launch for buy recommendations from sales-order demand planning +- Sales-order demand planning with multi-level BOM explosion across manufactured and assembly children +- Netting of sales-order demand against available stock, active reservations, open work orders, and open purchase orders +- Build and buy recommendations surfaced directly on sales-order detail pages +- Pegged work-order and purchase-order supply tracking back to sales demand so reopened planning views do not overstate remaining recommendations +- Preferred-vendor sourcing on inventory items, used to preseed buy-side planning workflows +- Demand-planning recommendations now reduce against existing linked WO/PO supply and support safer partial conversion behavior +- Support-log capture for startup warnings, HTTP failures, and server errors, surfaced through admin diagnostics +- Exportable support bundles that now include backup guidance and recent support logs for support handoff +- Deeper startup diagnostics with writable-path checks, database-file validation, startup timing, and pass/warn/fail rollups +- Backup verification checklist and restore-drill runbook surfaced in admin diagnostics +- Backup/restore guidance surfaced in admin diagnostics with exportable support snapshot JSON for support handoff +- CRM customer/vendor changes and shipping mutations now feed the shared audit trail +- Startup validation now runs during server boot and surfaces readiness checks in admin diagnostics +- Admin user-management screen with account creation, activation control, role assignment, and role-permission editing +- Route-level lazy loading and vendor chunking across the client so major operational modules no longer ship in the initial bundle +- Persisted audit events for core settings, inventory, purchasing, sales, project, and manufacturing write actions +- Admin diagnostics page with runtime footprint, storage-path visibility, key record counts, and recent audit activity +- Inventory transfers with paired physical stock movement posting between warehouses and locations +- Manual inventory reservations plus automatic work-order-driven component reservations +- Reserved and available stock visibility on inventory item detail and stock-by-location views +- Manufacturing stations with queue-day definitions and item-level station/time operation templates +- Automatic work-order operation plans copied from buildable item routing into planning/gantt +- Live planning gantt timelines backed by active projects and open manufacturing work orders +- Planning summary metrics and exception cards for overdue or at-risk project/manufacturing schedule items +- Sales approval actions with approved-by/approved-at stamps on quotes and sales orders +- Automatic sales-document revision history with authored reasons and per-revision snapshots +- Projects domain foundation with customer, owner, due date, priority, notes, and attachment support +- Project linkage to sales quotes, sales orders, and shipments for cross-module delivery tracking +- Project list/detail/create/edit workflows and app-shell navigation entry +- Dashboard project widgets for active, at-risk, and overdue program visibility +- Manufacturing foundation with work orders, optional project linkage, work-order attachments, and app-shell navigation entry +- BOM-based manufacturing requirement visibility plus material issue and completion posting through inventory transactions +- Dashboard manufacturing widgets for released, active, and overdue work visibility +- Purchase-order supporting documents for vendor invoices, acknowledgements, certifications, and procurement backup files +- Vendor-detail purchasing visibility with recent purchase-order activity and PO launch shortcuts + +### Changed + +- Company theme colors and font now persist correctly across refresh through startup brand-profile hydration in the frontend theme provider +- Demand-planning purchase-order draft generation now links sales-order lines only when the purchase item matches the originating sales item +- Admin user edits now normalize blank passwords to null and surface save failures instead of appearing unresponsive +- Fresh bootstrap now creates only minimal system records; operational domains no longer start with seeded demo customers, vendors, inventory, BOMs, or warehouses +- Sales and purchasing detail pages now expose revision comparison directly alongside chronological revision history +- `ROADMAP.md` now tracks remaining work only, and shipped phase history now lives in `SHIPPED.md` +- Support logs now prune retained entries by age instead of only trimming by count, and admin diagnostics now reviews filtered support-log summaries instead of an unbounded flat dump +- 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 +- The dashboard now treats Manufacturing as a live first-class module alongside CRM, inventory, sales, shipping, and projects +- The dashboard now treats Planning as a live first-class module with direct gantt access from the landing page +- Inventory control now distinguishes on-hand, reserved, and available stock instead of treating all positive stock as fully free +- Manufacturing and inventory now share a routing-driven workflow where assemblies/manufactured parts define station/time templates and work orders inherit them automatically +- Sales quote and sales-order detail pages now surface approval state and revision history directly in the operational workflow +- Project editing now uses searchable pickers for customer, owner, quote, sales-order, and shipment linkage instead of static operational dropdowns +- Project detail now surfaces linked work orders and can launch pre-seeded manufacturing records +- Purchase-order detail now links back to the vendor CRM record and supports direct supporting-document management on the PO itself +- Vendor CRM detail now exposes purchasing activity and can launch pre-seeded purchase orders +- The client entry bundle now stays lighter by loading major modules on demand instead of importing all route pages eagerly in `main.tsx` +- Company settings now acts as the staging area for admin surfaces while user administration lives on its own dedicated page instead of inside the company-profile form +- Admin diagnostics now includes startup-readiness status alongside runtime footprint and recent audit activity +- Admin diagnostics now includes structured startup summaries and a dedicated support-log view for faster debugging +- Roadmap and project docs now treat demand planning and supply generation as its own phase ahead of the deferred admin QOL work +- Roadmap and project docs now treat backup verification checklist and restore drill guidance as the next active priority after the backup/support-tooling slice +- Manufacturing work-order material requirements now include live available/shortage visibility instead of only required-versus-issued math +- Roadmap and project docs now move back to admin session visibility and destructive-action safety after the demand-planning rollout + +## 2026-03-15 + +### Added + +- Dashboard foundation followed by live-data dashboard cards and operational summary widgets +- Purchase-order domain foundation with searchable vendor and SKU workflows +- Purchase receiving foundation tied to purchase orders, including warehouse/location receipt posting, receipt history, and per-line received and remaining quantity tracking +- Branded quote, sales-order, and purchase-order PDFs through the shared backend Puppeteer document pipeline +- Navigation icon support for top-level modules + +### Changed + +- Purchasing detail workflows now support operational receiving directly from the purchase-order record +- Sales and purchasing detail pages now expose one-click PDF rendering actions +- Dashboard direction and project instructions were tightened through new documentation guidance +- Roadmap and project docs now treat shipping/logistics documents as the next active priority after receiving and commercial PDFs + +### Fixed + +- Purchase-order follow-up fixes after the initial purchasing rollout +- PDF rendering and document-pipeline fixes after the first document commits +- General refinement pass across active workflows after the initial March 15 feature wave + +## 2026-03-14 + +### Added + +- Initial MRP foundation scaffold +- Docker build and runtime foundation, plus Unraid deployment guidance +- Prisma setup, migration fixes, npm/workspace fixes, and supporting JS/runtime cleanup +- Auth/RBAC foundation, dark mode support, and early UI cleanup +- CRM foundation through customer/vendor records, hierarchy, contacts, lifecycle metadata, images, and final CRM polish +- Inventory foundation through item master, BOMs, warehouses, stock locations, searchable SKU flows, and inventory detail refinements +- Dense workspace and compact UI passes across the active product surface +- Sales foundation with searchable document entry, conversion actions, default cost/price support, and totals/commercial logic +- Shipping foundation with sales-order linkage and packing-slip PDFs +- Company-profile and document PDF foundation with subsequent documentation updates + +### Changed + +- Inventory default-price support flowing into sales documents +- Purchase-order line restrictions to purchasable inventory items only +- Search and focus behavior received follow-up fixes during CRM/inventory/sales rollout +- Documentation was expanded repeatedly as the foundation scope grew + +### Fixed + +- Prisma reliability and workspace setup issues discovered during the first foundation build-out +- SKU picker and autocomplete issues during inventory rollout +- Focus and autosuggest regressions during dense operational form work + +### Known follow-up areas + +- Shipping labels, bills of lading, and logistics attachments +- Vendor invoice/supporting-document attachments +- Sales approvals and document revision history +- Projects, manufacturing execution, and planning depth + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..7f7e5b1 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,95 @@ +# syntax=docker/dockerfile:1.7 +ARG NODE_VERSION=22 + +FROM node:${NODE_VERSION}-bookworm-slim AS base +WORKDIR /app +ENV PUPPETEER_SKIP_DOWNLOAD=true + +FROM base AS deps +COPY package.json package-lock.json ./ +COPY client/package.json client/package.json +COPY server/package.json server/package.json +COPY shared/package.json shared/package.json +RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund + +FROM deps AS build +COPY . . +RUN npm run prisma:generate +RUN npm run build + +FROM base AS prod-deps +COPY package.json package-lock.json ./ +COPY client/package.json client/package.json +COPY server/package.json server/package.json +COPY shared/package.json shared/package.json +COPY server/prisma server/prisma +RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev --no-audit --no-fund +RUN npm run prisma:generate -w server +RUN test -x /app/server/node_modules/.bin/prisma + +FROM node:${NODE_VERSION}-bookworm-slim AS runtime +WORKDIR /app + +ENV NODE_ENV=production +ENV PORT=3000 +ENV DATA_DIR=/app/data +ENV DATABASE_URL=file:../../data/prisma/app.db +ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium +ENV PUPPETEER_SKIP_DOWNLOAD=true + +RUN apt-get update && apt-get install -y --no-install-recommends \ + chromium \ + ca-certificates \ + fonts-liberation \ + libasound2 \ + libatk-bridge2.0-0 \ + libatk1.0-0 \ + libc6 \ + libcairo2 \ + libcups2 \ + libdbus-1-3 \ + libexpat1 \ + libfontconfig1 \ + libgbm1 \ + libgcc1 \ + libglib2.0-0 \ + libgtk-3-0 \ + libnspr4 \ + libnss3 \ + libpango-1.0-0 \ + libpangocairo-1.0-0 \ + libstdc++6 \ + libx11-6 \ + libx11-xcb1 \ + libxcb1 \ + libxcomposite1 \ + libxcursor1 \ + libxdamage1 \ + libxext6 \ + libxfixes3 \ + libxi6 \ + libxrandr2 \ + libxrender1 \ + libxss1 \ + libxtst6 \ + xdg-utils \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=prod-deps /app/node_modules /app/node_modules +COPY --from=prod-deps /app/server/node_modules /app/server/node_modules +COPY --from=build /app/client/dist /app/client/dist +COPY --from=build /app/server/dist /app/server/dist +COPY --from=build /app/shared/dist /app/shared/dist +COPY --from=build /app/server/package.json /app/server/package.json +COPY --from=build /app/shared/package.json /app/shared/package.json +COPY --from=build /app/server/prisma /app/server/prisma +COPY --from=build /app/docker-entrypoint.sh /app/docker-entrypoint.sh +COPY package.json package-lock.json ./ +COPY README.md INSTRUCTIONS.md STRUCTURE.md ROADMAP.md UNRAID.md ./ + +RUN chmod +x /app/docker-entrypoint.sh + +VOLUME ["/app/data"] +EXPOSE 3000 + +ENTRYPOINT ["/app/docker-entrypoint.sh"] diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md new file mode 100644 index 0000000..c4e7711 --- /dev/null +++ b/INSTRUCTIONS.md @@ -0,0 +1,82 @@ +# Development Instructions + +## 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), [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 + +This repository implements the platform foundation milestone: + +- workspace scaffolding +- local auth and RBAC +- company settings and branding +- file attachment storage +- CRM foundation through reseller hierarchy, contacts, attachments, and lifecycle metadata +- inventory master data, BOM, warehouse, stock-location, transactions, and item attachments +- inventory transfers, reservations, available-stock visibility, and work-order reservation automation +- inventory SKU master builder with family-scoped sequence generation and branch-aware taxonomy management +- inventory thumbnail image staging on create/edit and dedicated thumbnail display on item detail +- sales quotes and sales orders with quick actions and quote conversion +- sales approvals, approval stamps, automatic revision history, and revision comparison on quotes and sales orders +- purchase orders with quick actions and searchable vendor/SKU entry +- purchase orders restricted to inventory items flagged as purchasable +- purchase receiving foundation with inventory posting and receipt history +- purchase-order revision history and revision comparison across document and receipt changes +- branded sales and purchasing PDFs through the shared Puppeteer document pipeline +- purchase-order supporting documents and vendor-side purchasing visibility +- shipping shipments linked to sales orders with packing slips, shipping labels, bills of lading, and logistics attachments +- projects with customer/commercial/shipment linkage, owners, due dates, notes, attachments, and dashboard visibility +- manufacturing work orders with project linkage, station master data, item operation templates, auto-generated work-order operations, attachments, and dashboard visibility +- planning gantt timelines backed by live project and manufacturing schedule data +- sales-order demand planning with multi-level BOM explosion, net stock/open-supply coverage, and build/buy recommendations +- planner-assisted conversion of demand-planning recommendations into prefilled work-order and purchase-order drafts +- 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, 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 +- support-log viewing, filtering, retention cleanup, and support debugging helpers in diagnostics +- startup brand-theme hydration so Company Settings colors persist correctly across refresh +- Dockerized single-container deployment +- Puppeteer PDF pipeline foundation + +## Workflow + +1. Update the roadmap before starting large features. +2. Keep backend and frontend modules grouped by domain. +3. Add Prisma models and migrations for all persisted schema changes. +4. Keep uploaded files on disk under `/app/data/uploads`; never store blobs in SQLite. +5. Reuse shared DTOs and permission keys from the `shared` package. +6. Any non-filter UI that looks up records or items must use a searchable picker/autocomplete, not a long static dropdown. +7. Inventory items must carry both `defaultCost` and `defaultPrice`; sales documents should default line pricing from the selected item `defaultPrice`. +8. Maintain the denser UI baseline on active screens; avoid reintroducing oversized `px-4 py-3` style controls, tall action bars, or overly loose card spacing without a specific reason. +9. Treat the landing page as `Dashboard`: a metric-oriented, modular command surface that should accumulate reusable operational panels over time. +10. Purchase-order item selection must be restricted to inventory items where `isPurchasable = true`. +11. Treat `Projects` as a first-class cross-module domain tying together CRM, sales, inventory, purchasing, shipping, and planning; do not bury it as a one-off manufacturing subfeature. +12. Keep `Projects`, `Manufacturing`, and `Planning` distinct: projects are long-running program records, manufacturing is execution, and planning is scheduling/visibility. +13. New top-level modules added to the app shell should include a matching SVG icon in navigation so the module list remains visually scannable. + +## Operational notes + +- Run `npm run prisma:generate` after schema changes. +- Run `npm run prisma:migrate` during development to create versioned migrations. +- Use `npm run prisma:deploy` in production environments. +- Prefer Node 22 locally when running Prisma migration commands to match the Docker runtime. +- Branding defaults live in the frontend theme token layer and are overridden by the persisted company profile. +- Back up the whole `/app/data` volume to capture both the database and attachments. +- Treat searchable lookup as a standing UX requirement for inventory, BOM, sales, purchasing, manufacturing, customer, vendor, and other operational record-picking flows. Filter-only controls can still use dropdowns. +- Extend the existing Puppeteer document pipeline when adding customer-facing or logistics PDFs instead of creating a parallel export mechanism. +- Add future dashboard features as modular metric/action panels instead of one-off hero sections or static marketing-style content. +- When implementing projects, model the relationships explicitly so project records can anchor execution across customer, order, material, schedule, and shipment workflows. +- When implementing manufacturing, keep work orders, routings, labor, and shop-floor execution in their own domain rather than collapsing them into projects. + +## Next roadmap candidates + +- project milestones and project-side rollup visibility +- manufacturing routing/work-center depth, labor capture, and capacity-aware execution views + diff --git a/README.md b/README.md new file mode 100644 index 0000000..7622d8f --- /dev/null +++ b/README.md @@ -0,0 +1,401 @@ +# CODEXIUM + +Foundation release for a modular Manufacturing Resource Planning platform built with React, Express, Prisma, SQLite, and a single-container Docker deployment. + +## 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), [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: + +- authentication and RBAC +- company branding and theme settings, including startup brand-theme hydration across refresh +- CRM customers and vendors with create/edit/detail workflows +- CRM search, filtering, status tagging, and reseller hierarchy +- CRM contact history, account contacts, and shared attachments +- inventory item master, BOM, warehouse, stock-location, and stock-transaction flows +- inventory transfers, reservations, and available-stock visibility +- inventory SKU master builder with family-scoped sequence generation and branch-aware taxonomy management +- staged thumbnail image attachment on inventory item create/edit workflows, with detail-page thumbnail display +- sales quotes and sales orders with searchable customer and SKU entry +- sales approvals, approval stamps, automatic revision history, and revision comparison on quotes and sales orders +- purchase orders with searchable vendor and SKU entry, restricted to purchasable inventory items +- purchase-order revision history and revision comparison across commercial and receipt changes +- purchase receiving with warehouse/location posting and receipt history against purchase orders +- branded quote, sales-order, and purchase-order PDFs through the shared backend document pipeline +- purchase-order supporting documents for vendor invoices, acknowledgements, certifications, and backup files +- shipping shipments linked to sales orders with packing slips, shipping labels, bills of lading, and logistics attachments +- projects with customer/commercial/shipment linkage, owners, due dates, notes, and attachments +- manufacturing work orders with project linkage, station-based operation templates, material issue posting, completion posting, and work-order attachments +- planning gantt timelines with live project and manufacturing schedule data +- sales-order demand planning with multi-level BOM explosion, stock/open-supply netting, and build/buy recommendations +- planner-assisted conversion of demand-planning recommendations into prefilled work-order and purchase-order drafts +- 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, 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 +- support-log viewing, filtering, retention cleanup, and richer support-debug export helpers in admin diagnostics +- route-level code-splitting and vendor chunking for lighter initial client loads +- file storage and PDF rendering + +## 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 +- CRM foundation +- inventory foundation +- sales and purchasing foundation +- shipping foundation +- projects foundation +- manufacturing foundation +- planning foundation +- audit and diagnostics foundation +- user and role administration foundation +- branding, attachments, auth/RBAC, and PDF infrastructure + +Near-term priorities: + +1. Project milestones and project-side rollup visibility +2. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views + +Revisit / deferred items: + +- local Windows Prisma migration reliability +- project milestones and project-side rollup visibility + +Dashboard direction: + +- the landing page is now `Dashboard`, not `Overview` +- it should remain a metric-oriented operational surface rather than a generic welcome page +- new modules should add reusable dashboard cards/panels instead of replacing the whole layout +- future additions should emphasize relevant metrics, next actions, alerts, and workflow shortcuts +- richer recent-activity widgets and exception queues are a planned QOL follow-up, not a separate landing-page redesign +- projects now feed dashboard widgets for active programs, overdue work, and risk +- manufacturing now feeds dashboard widgets for released work, overdue orders, and execution load +- planning now feeds live gantt scheduling from project and manufacturing records +- future project widgets should deepen milestones, shortages, and shipment readiness + +Navigation direction: + +- module navigation now uses inline SVG icons alongside labels +- new modules should add a clear, domain-appropriate SVG icon when they are added to the shell +- icons should stay lightweight, theme-aware, and dependency-free unless there is a strong reason to introduce a shared icon package + +## Projects Direction + +Projects are now the long-running program and delivery layer for cross-module execution. The current slice ships project records with customer linkage, owner assignment, priority, due dates, notes, commercial document links, shipment links, attachments, and dashboard visibility. + +Current interactions: + +- CRM: each project should link to a customer account and relevant contacts +- Sales: quotes and sales orders can already attach to projects +- Shipping: shipments tied to project deliverables are visible from the project record +- Dashboard: projects now contribute status, risk, backlog, and overdue widgets + +Next expansion areas: + +- Inventory: projects should reference item/BOM scope and later expose shortages or allocations +- Purchasing: project material demand should be visible to purchasing and receiving workflows +- Manufacturing: work orders should link back to projects without turning projects into the manufacturing module +- Planning: project milestones and execution dates should feed gantt scheduling and dependency views + +## Manufacturing Direction + +Manufacturing is now a separate execution subsystem rather than being collapsed into Projects. The current slice ships work-order records with build-item linkage, optional project linkage, warehouse/location output posting, BOM-based material requirement visibility, station master data, item-level operation templates, automatic work-order operation plans, material issue posting, completion posting, work-order attachments, and dashboard visibility. + +Current interactions: + +- Projects: manufacturing orders may belong to a project, but projects remain the higher-level long-running record +- Inventory: manufacturing consumes components and produces stock through real issue/receipt transactions +- Dashboard: manufacturing now contributes released/open/overdue load widgets + +Next expansion areas: + +- Purchasing: shortages and buyout demand should surface from manufacturing execution +- Shipping: completed manufacturing should feed shipment readiness +- Planning: manufacturing orders, routings, and work centers should drive capacity and schedule views + +## Planning Direction + +Planning is now the live scheduling and visibility layer over projects and manufacturing instead of a placeholder wrapper. The current slice ships a gantt surface backed by active projects, due-date milestones, linked work orders, standalone manufacturing queue visibility, and exception cards for overdue or at-risk schedule items. + +Current interactions: + +- Projects: project timelines and due dates anchor the top-level planning rows +- Manufacturing: open work orders feed task rows, sequencing links, and execution progress +- Dashboard: planning now appears as a first-class module with schedule visibility links + +Next expansion areas: + +- Purchasing: shortages, late receipts, and vendor risk should surface directly in planning +- Manufacturing: routings, work centers, and capacity should deepen the schedule model +- Projects: richer milestones and dependency editing should extend the project-level timeline + +## Workspace + +- `client`: React, Vite, Tailwind frontend +- `server`: Express API, Prisma, auth/RBAC, file storage, PDF rendering +- `shared`: shared TypeScript contracts and constants + +## Local Development + +1. Use Node.js 22 for local development if you want Prisma migration commands to behave the same way as Docker. +2. Install dependencies with `npm.cmd install`. +3. Copy [`.env.example`](D:\CODING\mrp-codex\.env.example) to `.env` and adjust values if needed. +4. Generate Prisma client with `npm run prisma:generate`. +5. Apply committed migrations with `npm run prisma:deploy`. +6. Start the workspace with `npm run dev`. + +The frontend runs through Vite in development and is served statically by the API in production. + +Seeded admin credentials for first login: + +- email: `admin@mrp.local` +- password: `ChangeMe123!` + +## Docker + +Build and run: + +```bash +docker build -t mrp-codex . +docker run -p 3000:3000 -v mrp_data:/app/data mrp-codex +``` + +Command-line build notes: + +- The Dockerfile is intended to be built directly from the repo root with `docker build` +- `puppeteer` browser download is disabled during image build because the runtime image installs system Chromium +- You can override the Node base image version if needed: + +```bash +docker build --build-arg NODE_VERSION=22 -t mrp-codex . +``` + +The container startup script runs the server workspace Prisma binary directly: + +```bash +/app/server/node_modules/.bin/prisma migrate deploy --schema /app/server/prisma/schema.prisma +``` + +This Docker path is currently the most reliable way to ensure the database schema matches the latest CRM and inventory migrations on Windows. + +## Persistence And Backup + +- SQLite database path: `/app/data/prisma/app.db` +- Uploaded files: `/app/data/uploads` +- Backup the entire mounted `/app/data` volume to preserve both records and attachments. + +## CRM + +The current CRM foundation supports: + +- customer and vendor list, detail, create, and edit flows +- search by text plus status and state/province filters +- customer reseller flag, reseller discount, and parent-child hierarchy +- contact-history timeline entries for notes, calls, emails, and meetings +- multiple account contacts with role and primary-contact tracking +- shared file attachments on customer and vendor records +- commercial terms fields including payment terms, currency, tax exempt, and credit hold + +QOL direction: + +- saved filters and quick views +- cleaner hierarchy navigation for reseller/customer trees +- richer downstream rollups once purchasing and shipping grow further + +Recent CRM features depend on the committed Prisma migrations being applied. If you update the code and do not run migrations, the UI may render fields that are not yet present in the database. + +## Inventory + +The current inventory foundation supports: + +- protected item master list, detail, create, and edit flows +- SKU, description, type, status, unit-of-measure, sellable/purchasable, default cost, default price, and notes fields +- BOM header and BOM line editing directly on the item form +- searchable component lookup for BOM lines, designed for large item catalogs +- SKU-first searchable component lookup for BOM lines, with SKU shown in the picker and description kept separate in the selected row +- BOM detail display with component SKU, name, quantity, unit, notes, and position +- protected warehouse list, detail, create, and edit flows +- nested stock-location management inside each warehouse record +- inventory transaction posting for receipts, issues, and adjustments +- inventory transfers with paired source/destination movement posting +- manual reservations plus automatic work-order component reservations +- item on-hand quantity, stock-by-location balances, and recent stock history +- reserved and available quantity visibility by location +- item-level file attachments for drawings and support documents +- fresh bootstrap starts inventory and warehouse data empty so first-run environments do not include demo operational records + +QOL direction: + +- clearer warehouse dashboards and shortage views +- BOM revisions and where-used visibility + +This module introduces `inventory.read` and `inventory.write` permissions. After updating the code, restart the server against the migrated database so bootstrap can upsert the new permissions onto the default administrator role. + +`defaultPrice` now flows from inventory items into quote and sales-order line entry when a SKU is selected. Users can still override the line price after selection. + +## Sales + +The current sales foundation supports: + +- quote and sales-order list, detail, create, and edit flows +- searchable customer lookup instead of static customer dropdowns +- SKU-searchable line entry for quote and order lines +- document-level discount, tax, freight, subtotal, and total calculations +- reseller discount defaulting from customer records into sales documents +- status quick actions directly from quote and order detail pages +- quote conversion into a sales order +- line-level unit prices populated from the selected inventory item default price +- branded quote and sales-order PDFs through the shared document pipeline +- approval stamps and revision history directly on quote and sales-order detail pages +- revision-reason capture when editing customer-facing sales documents + +QOL direction: + +- line duplication and faster keyboard-heavy line editing +- revision comparison view and restore-style workflows +- richer PDF output for quotes and sales orders + +This module introduces `sales.read` and `sales.write` permissions. After updating the code, restart the server against the migrated database so bootstrap can upsert the new permissions onto the default administrator role. + +## Purchasing + +The current purchasing foundation supports: + +- purchase-order list, detail, create, and edit flows +- searchable vendor lookup instead of a static vendor dropdown +- SKU-searchable line entry for purchase-order lines +- purchase-order item lookup restricted to inventory items flagged as purchasable +- receiving workflow tied to purchase orders, with receipt history and inventory posting +- document-level tax, freight, subtotal, and total calculations +- quick status actions directly from purchase-order detail pages +- vendor payment terms and currency surfaced on purchase-order forms and details +- branded purchase-order PDFs through the shared document pipeline + +QOL direction: + +- richer dashboard widgets for vendor queues and inbound material exceptions +- vendor-side exception tracking around acknowledgements, invoice matching, and receipt discrepancies + +This module introduces `purchasing.read` and `purchasing.write` permissions. After updating the code, restart the server against the migrated database so bootstrap can upsert the new permissions onto the default administrator role. + +## Shipping + +The current shipping foundation supports: + +- shipment list, detail, create, and edit flows +- searchable sales-order lookup instead of a static order dropdown +- shipment records linked directly to sales orders +- carrier, service level, tracking number, package count, notes, and ship date fields +- shipment quick status actions from the shipment detail page +- related-shipment visibility from the sales-order detail page +- branded packing-slip PDF rendering from shipment detail pages +- branded shipping-label and bill-of-lading PDF rendering from shipment detail pages +- logistics attachments directly on shipment records + +QOL direction: + +- reprint/history actions for generated logistics PDFs +- partial-shipment and split-shipment UX + +This module introduces `shipping.read` and `shipping.write` permissions. After updating the code, restart the server against the migrated database so bootstrap can upsert the new permissions onto the default administrator role. + +Moving forward, any UI that requires searching for records or items should use a searchable picker/autocomplete rather than a static dropdown. Filter controls can remain dropdowns, but non-filter lookup fields such as SKU pickers, customer selectors, vendor selectors, and similar operational search inputs should not be implemented as long static selects. + +## UI Density + +The active client screens have been normalized toward a denser workspace layout: + +- form controls and action bars use tighter padding +- CRM, inventory, warehouse, settings, dashboard, and login screens use reduced card spacing +- headings, metric cards, empty states, and long-text blocks were tightened for better data density + +This denser layout is now the baseline for future screens. New pages should avoid reverting to oversized card padding, tall action bars, or long static dropdowns for operational datasets. + +## Branding + +Brand colors and typography are configured through the Company Settings page and the frontend theme token layer. Update runtime branding in-app, or adjust defaults in the theme config if you need a new baseline brand. + +Logo uploads are stored through the authenticated file pipeline and are rendered back into the settings UI through an authenticated blob fetch, so image preview works after save and refresh. + +## Migrations + +- Create a local migration: `npm run prisma:migrate` +- Apply committed migrations in production: `npm run prisma:deploy` +- If Prisma migration commands fail on a local Node 24 Windows environment, use Node 22 or Docker for migration execution. The committed migration files in `server/prisma/migrations` remain the source of truth. + +As of March 15, 2026, the latest committed domain migrations include: + +- CRM status and list filters +- CRM contact-history timeline +- reseller hierarchy and reseller discount support +- CRM commercial terms and account contacts +- CRM lifecycle stages and operational metadata +- inventory item master and BOM foundation +- warehouse and stock-location foundation +- inventory transactions and on-hand tracking +- sales quote and sales-order foundation +- purchase-order foundation +- purchase receiving foundation +- branded sales and purchasing PDF templates +- sales approvals and document revision history +- inventory default price support +- sales totals and commercial fields +- shipping foundation +- projects foundation +- manufacturing foundation +- manufacturing stations and operation templates +- 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. + +## Audit And Diagnostics + +The current admin operations slice supports: + +- persisted audit events for core settings, inventory, purchasing, sales, project, and manufacturing write actions +- an admin diagnostics page for runtime footprint, data/storage path visibility, key record counts, and recent audit activity +- a sales-order demand-planning view with multi-level BOM netting and build/buy recommendations +- prefilled work-order and purchase-order draft launch paths from sales-order demand-planning recommendations +- shared shortage/readiness rollups across planning, project, purchasing, dashboard, and manufacturing views +- 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 +- support logs now support admin-side filtering by severity, source, search text, and retention window, and exports include summary metadata +- backup verification items and restore-drill expected outcomes now live in the admin runbook surface +- operator-facing review of recent high-impact changes without direct database access + +Current follow-up direction: + +- revision comparison UX for changed sales and purchasing documents +- project milestones and project-side rollup visibility + +## UI Notes + +- Dark mode persistence is handled through the frontend theme provider and should remain stable across page navigation. +- The shell layout is tuned for wider desktop use than the original foundation build, and now exposes Dashboard, CRM, inventory, sales, purchasing, shipping, projects, manufacturing, settings, and planning modules from the same app shell. +- The active module screens now follow a tighter density baseline for forms, tables, and detail cards. +- The dashboard should continue evolving as a modular metric board for future purchasing, shipping, planning, and audit data. +- The client now ships with route-level lazy loading and vendor chunking, so future frontend work should preserve that split instead of re-centralizing module imports in `main.tsx`. + +## PDF Generation + +Puppeteer is used by the backend to render HTML templates into professional PDFs. The current PDF surface includes the branded company-profile preview, sales quotes, sales orders, purchase orders, shipment packing slips, shipping labels, and bills of lading. The Docker image includes Chromium runtime dependencies required for headless execution. + diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..0772274 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,142 @@ +# Roadmap + +## 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 + +CODEXIUM 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. + +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). + +## Near-term priority order + +1. Project milestones, project rollups, and deeper project-side execution visibility +2. Manufacturing routing/work-center depth, labor capture, and capacity-aware execution views +3. Dashboard KPI, alert, recent-activity, and exception-widget expansion +4. Longer-term session history and audit depth beyond the current review filtering and retention cleanup + +## Active roadmap + +### Platform and operational docs + +- 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 + +### Dashboard + +- 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 + +### 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 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 + +### Inventory + +- 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 + +### 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 +- Line duplication, drag ordering, and keyboard-first line editing +- Saved customer defaults for tax, freight, and commercial terms +- Richer dashboard widgets for recent quotes, open orders, purchasing queues, and shipping exceptions +- Better totals breakdown visibility on list pages and detail pages +- Faster document cloning and quote-to-order style conversions across document types + +### Shipping and logistics + +- 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 +- Printer-friendly reprint/history actions for logistics documents + +### 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 +- Project templates for repeatable build types +- Project-specific attachment bundles and revision snapshots +- One-screen project cockpit with commercial, material, schedule, and shipping summary +- Better cross-links between project, customer, order, shipment, and inventory records +- Project filtering by customer, owner, status, due date, and risk +- Project activity timeline and audit-friendly milestone history + +### Manufacturing execution + +- 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 rollups for open work, blockers, shortages, and throughput +- Traveler/job packet output +- Partial completions and split-order execution visibility +- Better shortage and substitute-part handling +- Shop-floor quick actions and dense tablet-friendly execution views +- Rework / hold / scrap tracking +- Work-center dashboards and operator-focused queues + +### Planning and scheduling + +- 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 +- Collapsible schedule groupings and saved planner views +- Drag-and-drop rescheduling improvements +- Critical-path and overdue highlighting +- Capacity warnings for overloaded work centers +- Better mobile and tablet behavior for shop-floor lookups +- Faster filtering by project, customer, work center, and status + +### Demand planning and supply generation + +- 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 +- Time-phased supply recommendations with vendor lead times and build timing + +### Security, audit, and operations maturity + +- 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 + +- Stronger validation and error reporting across all APIs +- More automated tests for auth, settings, files, PDFs, and workflow modules +- Better mobile behavior in module-level pages +- Ongoing responsive-density tuning for module-level layouts and data-entry screens +- Consistent document-template system shared by sales, purchasing, and shipping +- Clear upgrade path for future module additions without refactoring the app shell + +## Revisit / Deferred Items + +- 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 + diff --git a/SHIPPED.md b/SHIPPED.md new file mode 100644 index 0000000..aaf4e90 --- /dev/null +++ b/SHIPPED.md @@ -0,0 +1,129 @@ +# 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 +- Revision comparison UX for changed sales and purchasing documents, including purchase-order revision persistence +- 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 +- Startup brand-theme hydration so Company Settings colors and font persist correctly across refresh +- 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 +- Support-log filtering, retention cleanup, and richer filtered support-bundle exports in admin diagnostics +- Inventory thumbnail staging on item create/edit plus dedicated detail-page thumbnail display +- Demand-planning PO draft generation no longer applies mismatched sales-order line links to BOM child buy recommendations +- User-management saves now accept blank-password edits for existing users and surface errors in-page + diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000..b093757 --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,55 @@ +# Project Structure + +## Documentation maintenance + +- Keep [CHANGELOG.md](D:/CODING/mrp-codex/CHANGELOG.md) updated when structural or implementation changes materially affect shipped behavior. +- If structure guidance changes, update the related source-of-truth docs in [README.md](D:/CODING/mrp-codex/README.md), [INSTRUCTIONS.md](D:/CODING/mrp-codex/INSTRUCTIONS.md), [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md), [UNRAID.md](D:/CODING/mrp-codex/UNRAID.md), and [AGENTS.md](D:/CODING/mrp-codex/AGENTS.md) as needed. + +## Top-level layout + +- `client/`: frontend application +- `server/`: backend application +- `shared/`: shared TypeScript contracts, permissions, and utility types +- `Dockerfile`: production container build +- `docker-entrypoint.sh`: migration-aware startup script + +## Frontend rules + +- Organize code by domain under `src/modules`. +- Keep app-shell concerns in `src/app`. +- Keep reusable UI primitives in `src/components`. +- Theme state and brand tokens belong in `src/theme`. +- PDF screen components must remain separate from API-rendered document templates. +- Treat `src/modules/dashboard` as a long-lived operational module. New high-level KPI, alert, queue, and shortcut surfaces should compose into it rather than spawning disconnected landing pages. +- Any non-filter lookup UI must be implemented as a searchable picker or autocomplete; do not use long static dropdowns for operational datasets such as items, customers, vendors, or document-linked records. +- Inventory items expose both cost and sell price. Downstream sales document entry should default from the item price field rather than requiring duplicate price maintenance. +- Future vendor-facing purchasing flows should follow the same searchable-lookup rule and shared document/totals model already used by sales. +- Purchase-order item pickers must only surface inventory items flagged as purchasable. +- Shipping, sales, and future purchasing PDFs should be rendered through the backend documents module and shared Puppeteer pipeline rather than ad hoc frontend-only exports. +- Preserve the current dense operations UI style on active module pages: compact controls, tighter card padding, and shorter empty states unless a screen has a clear reason to be more spacious. +- Treat `projects` as its own long-lived domain under both client and server. It should continue integrating with CRM, sales, inventory, purchasing, shipping, and planning rather than living inside only one of those modules. +- Treat `manufacturing` as a separate long-lived domain from `projects`; work orders, routings, labor capture, WIP, and shop-floor execution should not be modeled only as project fields. +- Treat `planning` as the scheduling/visibility layer that consumes project and manufacturing data rather than replacing either domain. +- When adding a new top-level module to the shell, add a lightweight SVG icon in the navigation config so desktop and mobile nav stay aligned. + +## Backend rules + +- Organize domain modules under `src/modules/`. +- Keep HTTP routers thin; place business logic in services. +- Centralize Prisma access, auth middleware, persisted session helpers, file storage utilities, startup validation, and support logging in `src/lib`. +- Store persistence-related constants under `src/config`. +- Serve the built frontend from the API layer in production. + +## Shared package rules + +- Place cross-app DTOs, permission keys, enums, and document interfaces in `shared/src`. +- Keep shared code free of runtime framework dependencies. + +## Adding a new domain + +1. Add backend routes, service, and repository/module files under `server/src/modules/`. +2. Add Prisma models and a migration if the module needs persistence. +3. Add permission keys in `shared/src/auth`. +4. Add frontend route/module under `client/src/modules/`. +5. Register navigation and route guards through the app shell without refactoring existing modules. + diff --git a/UNRAID.md b/UNRAID.md new file mode 100644 index 0000000..1f38b55 --- /dev/null +++ b/UNRAID.md @@ -0,0 +1,195 @@ +# Unraid Install Guide + +## Documentation maintenance + +- Keep [CHANGELOG.md](D:/CODING/mrp-codex/CHANGELOG.md) updated when deployment behavior, startup flow, persistence expectations, or operator-facing install steps materially change. +- If Unraid deployment guidance changes, update the companion project docs in [README.md](D:/CODING/mrp-codex/README.md), [INSTRUCTIONS.md](D:/CODING/mrp-codex/INSTRUCTIONS.md), and [ROADMAP.md](D:/CODING/mrp-codex/ROADMAP.md) in the same change set when relevant. + +## Purpose + +This guide explains how to deploy CODEXIUM on an Unraid server using the Unraid Docker GUI rather than command-line Docker management. + +## What this container expects + +- One published web port for the application +- One persistent app data path mounted to `/app/data` +- Environment variables for the server port, JWT secret, and SQLite path +- Automatic Prisma migration execution during container startup + +CODEXIUM stores both the SQLite database and uploaded files inside the same persistent container path: + +- database: `/app/data/prisma/app.db` +- uploads: `/app/data/uploads` + +## Before you start + +- Build and push the image to a registry your Unraid server can access, or import it manually if you are self-hosting images +- Choose an Unraid share for persistent app data, for example: + - `/mnt/user/appdata/codexium` +- Choose the port you want to expose in Unraid, for example: + - host port `3000` + - container port `3000` + +## Recommended Unraid paths + +- App data share: `/mnt/user/appdata/codexium` +- Optional backup target: `/mnt/user/backups/codexium` + +Keep the entire app data folder together so backups capture both the SQLite database and file attachments. + +## Add the container in the Unraid GUI + +### 1. Open the Docker tab + +- In Unraid, go to `Docker` +- Click `Add Container` + +### 2. Basic container settings + +Use these values as the starting point: + +- Name: `mrp-codex` +- Repository: your built image name, for example `your-registry/mrp-codex:latest` +- Network Type: `bridge` +- Console shell command: leave default +- Privileged: `Off` + +### 3. Port mapping + +Add a port mapping: + +- Name: `WebUI` +- Container Port: `3000` +- Host Port: `3000` +- Connection Type: `TCP` + +If port `3000` is already used on the Unraid host, change only the host port. + +### 4. Path mapping + +Add a single path mapping: + +- Name: `data` +- Container Path: `/app/data` +- Host Path: `/mnt/user/appdata/mrp-codex` +- Access Mode: `Read/Write` + +This is the critical mapping. Do not split the database and uploads into separate container paths unless you also update the app configuration. + +### 5. Environment variables + +Add these variables in the Unraid GUI: + +- `NODE_ENV` = `production` +- `PORT` = `3000` +- `JWT_SECRET` = `replace-with-a-long-random-secret` +- `DATA_DIR` = `/app/data` +- `DATABASE_URL` = `file:../../data/prisma/app.db` +- `PUPPETEER_EXECUTABLE_PATH` = `/usr/bin/chromium` +- `CLIENT_ORIGIN` = `http://YOUR-UNRAID-IP:3000` + +If you use a reverse proxy and external hostname, set `CLIENT_ORIGIN` to the final browser URL instead, for example: + +- `CLIENT_ORIGIN` = `https://mrp.yourdomain.com` + +### 6. Optional first-admin overrides + +You can set the seeded admin login explicitly on first deployment: + +- `ADMIN_EMAIL` = `admin@yourcompany.com` +- `ADMIN_PASSWORD` = `use-a-strong-initial-password` + +If you do not set them, the defaults from the app bootstrapping logic are used. + +## First start behavior + +On first container start, the entrypoint will: + +1. Ensure `/app/data/prisma` and `/app/data/uploads` exist +2. Run `/app/server/node_modules/.bin/prisma migrate deploy --schema /app/server/prisma/schema.prisma` +3. Start the Node.js server + +The frontend is served by the same container as the API, so there is only one exposed web port. + +## First login + +If you did not override the admin credentials with environment variables, use: + +- email: `admin@mrp.local` +- password: `ChangeMe123!` + +Change the admin password after first login if you keep the default bootstrap user. + +## Updating the container in Unraid + +When you publish a new image: + +1. Open the `Docker` tab +2. Apply the update from the Unraid GUI +3. Start the container + +Because CODEXIUM runs `prisma migrate deploy` during startup, committed migrations are applied automatically before the app launches. + +This is especially important now that recent releases added CRM expansion, inventory transactions, sales and purchasing documents, shipping/logistics documents, the inventory `defaultPrice` field, purchasable-only purchase-order item selection, the new projects domain, manufacturing work orders, audit tooling, and persisted auth sessions. Let the container complete startup migrations before testing new screens. + +## Backup guidance + +Back up the host directory mapped to `/app/data`, typically: + +- `/mnt/user/appdata/mrp-codex` + +That captures: + +- the SQLite database +- uploaded CAD drawings and supporting files +- logos and other persisted attachments + +For consistent backups, stop the container before copying the appdata directory. + +## Reverse proxy notes + +If you place CODEXIUM behind Nginx Proxy Manager, Traefik, or another reverse proxy: + +- keep the Unraid container on `bridge` or your preferred proxy-compatible network +- point the proxy at the Unraid host IP and chosen host port +- set `CLIENT_ORIGIN` to the final external URL + +## Troubleshooting + +### Container starts then exits + +Check the container logs in Unraid. Common causes: + +- invalid `DATABASE_URL` +- missing write access to the host path mapped to `/app/data` +- bad or missing environment values + +### PDFs fail to generate + +The image already includes Chromium and the required runtime libraries. If PDF generation still fails: + +- confirm `PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium` +- inspect the container logs for Puppeteer launch errors + +### Application boots but data does not persist + +Verify the `/app/data` path mapping in Unraid. If `/app/data` is not mapped to a persistent Unraid share, the database and uploads will be lost when the container is recreated. + +### Browser login or CORS issues + +Set `CLIENT_ORIGIN` to the exact URL used by the browser, including protocol and port. + +## Suggested Unraid template summary + +- Repository: `your-registry/mrp-codex:latest` +- Network Type: `bridge` +- Port: `3000` -> `3000` +- Path: `/mnt/user/appdata/mrp-codex` -> `/app/data` +- Env: `NODE_ENV=production` +- Env: `PORT=3000` +- Env: `JWT_SECRET=` +- Env: `DATA_DIR=/app/data` +- Env: `DATABASE_URL=file:../../data/prisma/app.db` +- Env: `PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium` +- Env: `CLIENT_ORIGIN=http://YOUR-UNRAID-IP:3000` + diff --git a/client/index.html b/client/index.html new file mode 100644 index 0000000..ff34a99 --- /dev/null +++ b/client/index.html @@ -0,0 +1,13 @@ + + + + + + MRP Codex + + +
+ + + + diff --git a/client/package.json b/client/package.json new file mode 100644 index 0000000..06cd5c7 --- /dev/null +++ b/client/package.json @@ -0,0 +1,32 @@ +{ + "name": "@mrp/client", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "test": "vitest run", + "lint": "tsc -b --pretty false" + }, + "dependencies": { + "@mrp/shared": "0.1.0", + "@svar-ui/react-gantt": "^2.5.2", + "@tanstack/react-query": "^5.90.2", + "react": "^19.2.0", + "react-dom": "^19.2.0", + "react-router-dom": "^7.9.3" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "autoprefixer": "^10.4.27", + "jsdom": "^28.1.0", + "postcss": "^8.5.8", + "tailwindcss": "^3.4.17", + "vite": "^8.0.0" + } +} diff --git a/client/postcss.config.cjs b/client/postcss.config.cjs new file mode 100644 index 0000000..c21c076 --- /dev/null +++ b/client/postcss.config.cjs @@ -0,0 +1,7 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; + diff --git a/client/src/auth/AuthProvider.tsx b/client/src/auth/AuthProvider.tsx new file mode 100644 index 0000000..92a12ac --- /dev/null +++ b/client/src/auth/AuthProvider.tsx @@ -0,0 +1,76 @@ +import type { AuthUser } from "@mrp/shared"; +import { createContext, useContext, useEffect, useMemo, useState } from "react"; + +import { api } from "../lib/api"; + +interface AuthContextValue { + token: string | null; + user: AuthUser | null; + isReady: boolean; + login: (email: string, password: string) => Promise; + logout: () => Promise; +} + +const AuthContext = createContext(null); +const tokenKey = "mrp.auth.token"; + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [token, setToken] = useState(() => window.localStorage.getItem(tokenKey)); + const [user, setUser] = useState(null); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + if (!token) { + setUser(null); + setIsReady(true); + return; + } + + api.me(token) + .then((nextUser) => { + setUser(nextUser); + }) + .catch(() => { + window.localStorage.removeItem(tokenKey); + setToken(null); + }) + .finally(() => setIsReady(true)); + }, [token]); + + const value = useMemo( + () => ({ + token, + user, + isReady, + async login(email, password) { + const result = await api.login({ email, password }); + setToken(result.token); + setUser(result.user); + window.localStorage.setItem(tokenKey, result.token); + }, + async logout() { + if (token) { + try { + await api.logout(token); + } catch { + // Clearing local auth state still signs the user out on the client. + } + } + window.localStorage.removeItem(tokenKey); + setToken(null); + setUser(null); + }, + }), + [token, user, isReady] + ); + + return {children}; +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used within AuthProvider"); + } + return context; +} diff --git a/client/src/components/AppShell.tsx b/client/src/components/AppShell.tsx new file mode 100644 index 0000000..2a62475 --- /dev/null +++ b/client/src/components/AppShell.tsx @@ -0,0 +1,257 @@ +import { NavLink, Outlet } from "react-router-dom"; +import type { ReactNode } from "react"; + +import { useAuth } from "../auth/AuthProvider"; +import { ThemeToggle } from "./ThemeToggle"; + +const links = [ + { to: "/", label: "Dashboard", icon: }, + { to: "/settings/company", label: "Company Settings", icon: }, + { to: "/crm/customers", label: "Customers", icon: }, + { to: "/crm/vendors", label: "Vendors", icon: }, + { to: "/inventory/items", label: "Inventory", icon: }, + { to: "/inventory/warehouses", label: "Warehouses", icon: }, + { to: "/sales/quotes", label: "Quotes", icon: }, + { to: "/sales/orders", label: "Sales Orders", icon: }, + { to: "/purchasing/orders", label: "Purchase Orders", icon: }, + { to: "/shipping/shipments", label: "Shipments", icon: }, + { to: "/projects", label: "Projects", icon: }, + { to: "/manufacturing/work-orders", label: "Manufacturing", icon: }, + { to: "/planning/gantt", label: "Gantt", icon: }, +]; + +function NavIcon({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); +} + +function DashboardIcon() { + return ( + + + + + + + ); +} + +function CompanyIcon() { + return ( + + + + + + + + ); +} + +function CustomersIcon() { + return ( + + + + + + + ); +} + +function VendorsIcon() { + return ( + + + + + + + + + + + ); +} + +function InventoryIcon() { + return ( + + + + + + ); +} + +function WarehouseIcon() { + return ( + + + + + + ); +} + +function QuoteIcon() { + return ( + + + + + + + + ); +} + +function SalesOrderIcon() { + return ( + + + + + + + + ); +} + +function PurchaseOrderIcon() { + return ( + + + + + + + + ); +} + +function ShipmentIcon() { + return ( + + + + + + + ); +} + +function GanttIcon() { + return ( + + + + + + + + ); +} + +function ProjectsIcon() { + return ( + + + + + + + + + ); +} + +function ManufacturingIcon() { + return ( + + + + + + + + ); +} + +export function AppShell() { + const { user, logout } = useAuth(); + + return ( +
+
+ +
+ +
+ +
+ +
+
+
+ ); +} diff --git a/client/src/components/ConfirmActionDialog.tsx b/client/src/components/ConfirmActionDialog.tsx new file mode 100644 index 0000000..ee7b6d7 --- /dev/null +++ b/client/src/components/ConfirmActionDialog.tsx @@ -0,0 +1,108 @@ +import { useEffect, useState } from "react"; + +interface ConfirmActionDialogProps { + open: boolean; + title: string; + description: string; + impact?: string; + recovery?: string; + confirmLabel?: string; + cancelLabel?: string; + intent?: "danger" | "primary"; + confirmationLabel?: string; + confirmationValue?: string; + isConfirming?: boolean; + onConfirm: () => void | Promise; + onClose: () => void; +} + +export function ConfirmActionDialog({ + open, + title, + description, + impact, + recovery, + confirmLabel = "Confirm", + cancelLabel = "Cancel", + intent = "danger", + confirmationLabel, + confirmationValue, + isConfirming = false, + onConfirm, + onClose, +}: ConfirmActionDialogProps) { + const [typedValue, setTypedValue] = useState(""); + + useEffect(() => { + if (open) { + setTypedValue(""); + } + }, [open]); + + if (!open) { + return null; + } + + const requiresTypedConfirmation = Boolean(confirmationLabel && confirmationValue); + const isConfirmDisabled = isConfirming || (requiresTypedConfirmation && typedValue.trim() !== confirmationValue); + const confirmButtonClass = + intent === "danger" + ? "bg-red-600 text-white hover:bg-red-700" + : "bg-brand text-white hover:brightness-110"; + + return ( +
+
+

Confirm Action

+

{title}

+

{description}

+ {impact ? ( +
+ Impact + {impact} +
+ ) : null} + {recovery ? ( +
+ Recovery + {recovery} +
+ ) : null} + {requiresTypedConfirmation ? ( + + ) : null} +
+ + +
+
+
+ ); +} + diff --git a/client/src/components/DocumentRevisionComparison.tsx b/client/src/components/DocumentRevisionComparison.tsx new file mode 100644 index 0000000..4d3c28c --- /dev/null +++ b/client/src/components/DocumentRevisionComparison.tsx @@ -0,0 +1,279 @@ +import { useEffect, useState } from "react"; + +type RevisionOption = { + id: string; + label: string; + meta: string; +}; + +type ComparisonField = { + label: string; + value: string; +}; + +type ComparisonLine = { + key: string; + title: string; + subtitle?: string; + quantity: string; + unitLabel: string; + amountLabel: string; + totalLabel?: string; + extraLabel?: string; +}; + +type ComparisonDocument = { + title: string; + subtitle: string; + status: string; + metaFields: ComparisonField[]; + totalFields: ComparisonField[]; + notes: string; + lines: ComparisonLine[]; +}; + +type DiffRow = { + key: string; + status: "ADDED" | "REMOVED" | "CHANGED"; + left?: ComparisonLine; + right?: ComparisonLine; +}; + +function buildLineMap(lines: ComparisonLine[]) { + return new Map(lines.map((line) => [line.key, line])); +} + +function lineSignature(line?: ComparisonLine) { + if (!line) { + return ""; + } + + return [line.title, line.subtitle ?? "", line.quantity, line.unitLabel, line.amountLabel, line.totalLabel ?? "", line.extraLabel ?? ""].join("|"); +} + +function buildDiffRows(left: ComparisonDocument, right: ComparisonDocument): DiffRow[] { + const leftLines = buildLineMap(left.lines); + const rightLines = buildLineMap(right.lines); + const orderedKeys = [...new Set([...left.lines.map((line) => line.key), ...right.lines.map((line) => line.key)])]; + const rows: DiffRow[] = []; + + for (const key of orderedKeys) { + const leftLine = leftLines.get(key); + const rightLine = rightLines.get(key); + + if (leftLine && !rightLine) { + rows.push({ key, status: "REMOVED", left: leftLine }); + continue; + } + + if (!leftLine && rightLine) { + rows.push({ key, status: "ADDED", right: rightLine }); + continue; + } + + if (lineSignature(leftLine) !== lineSignature(rightLine)) { + rows.push({ key, status: "CHANGED", left: leftLine, right: rightLine }); + } + } + + return rows; +} + +function buildFieldChanges(left: ComparisonField[], right: ComparisonField[]): Array<{ label: string; leftValue: string; rightValue: string }> { + const rightByLabel = new Map(right.map((field) => [field.label, field.value])); + + return left.flatMap((field) => { + const rightValue = rightByLabel.get(field.label); + if (rightValue == null || rightValue === field.value) { + return []; + } + + return [ + { + label: field.label, + leftValue: field.value, + rightValue, + }, + ]; + }); +} + +function ComparisonCard({ label, document }: { label: string; document: ComparisonDocument }) { + return ( +
+
+
+

{label}

+

{document.title}

+

{document.subtitle}

+
+ + {document.status} + +
+
+ {document.metaFields.map((field) => ( +
+
{field.label}
+
{field.value}
+
+ ))} +
+
+ {document.totalFields.map((field) => ( +
+
{field.label}
+
{field.value}
+
+ ))} +
+
+
Notes
+

{document.notes || "No notes recorded."}

+
+
+ ); +} + +export function DocumentRevisionComparison({ + title, + description, + currentLabel, + currentDocument, + revisions, + getRevisionDocument, +}: { + title: string; + description: string; + currentLabel: string; + currentDocument: ComparisonDocument; + revisions: RevisionOption[]; + getRevisionDocument: (revisionId: string | "current") => ComparisonDocument; +}) { + const [leftRevisionId, setLeftRevisionId] = useState(revisions[0]?.id ?? "current"); + const [rightRevisionId, setRightRevisionId] = useState("current"); + + useEffect(() => { + setLeftRevisionId((current) => (current === "current" || revisions.some((revision) => revision.id === current) ? current : revisions[0]?.id ?? "current")); + }, [revisions]); + + const leftDocument = getRevisionDocument(leftRevisionId); + const rightDocument = getRevisionDocument(rightRevisionId); + const diffRows = buildDiffRows(leftDocument, rightDocument); + const metaChanges = buildFieldChanges(leftDocument.metaFields, rightDocument.metaFields); + const totalChanges = buildFieldChanges(leftDocument.totalFields, rightDocument.totalFields); + + return ( +
+
+
+

{title}

+

{description}

+
+
+ + +
+
+
+ + +
+
+
+

Field Changes

+ {metaChanges.length === 0 && totalChanges.length === 0 ? ( +
No header or total changes between the selected revisions.
+ ) : ( +
+ {[...metaChanges, ...totalChanges].map((change) => ( +
+
{change.label}
+
+ {change.leftValue} {"->"} {change.rightValue} +
+
+ ))} +
+ )} +
+
+

Line Changes

+
+
+
Added
+
{diffRows.filter((row) => row.status === "ADDED").length}
+
+
+
Removed
+
{diffRows.filter((row) => row.status === "REMOVED").length}
+
+
+
Changed
+
{diffRows.filter((row) => row.status === "CHANGED").length}
+
+
+ {diffRows.length === 0 ? ( +
No line-level changes between the selected revisions.
+ ) : ( +
+ {diffRows.map((row) => ( +
+
+
{row.right?.title ?? row.left?.title}
+ {row.status} +
+
+
+
Baseline
+
+ {row.left ? `${row.left.quantity} | ${row.left.amountLabel}${row.left.totalLabel ? ` | ${row.left.totalLabel}` : ""}` : "Not present"} +
+ {row.left?.subtitle ?
{row.left.subtitle}
: null} + {row.left?.extraLabel ?
{row.left.extraLabel}
: null} +
+
+
Compare To
+
+ {row.right ? `${row.right.quantity} | ${row.right.amountLabel}${row.right.totalLabel ? ` | ${row.right.totalLabel}` : ""}` : "Not present"} +
+ {row.right?.subtitle ?
{row.right.subtitle}
: null} + {row.right?.extraLabel ?
{row.right.extraLabel}
: null} +
+
+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/client/src/components/FileAttachmentsPanel.tsx b/client/src/components/FileAttachmentsPanel.tsx new file mode 100644 index 0000000..98ffda2 --- /dev/null +++ b/client/src/components/FileAttachmentsPanel.tsx @@ -0,0 +1,221 @@ +import type { FileAttachmentDto } from "@mrp/shared"; +import { permissions } from "@mrp/shared"; +import { useEffect, useState } from "react"; + +import { useAuth } from "../auth/AuthProvider"; +import { api, ApiError } from "../lib/api"; +import { ConfirmActionDialog } from "./ConfirmActionDialog"; + +interface FileAttachmentsPanelProps { + ownerType: string; + ownerId: string; + eyebrow: string; + title: string; + description: string; + emptyMessage: string; + onAttachmentCountChange?: (count: number) => void; +} + +function formatFileSize(sizeBytes: number) { + if (sizeBytes < 1024) { + return `${sizeBytes} B`; + } + + if (sizeBytes < 1024 * 1024) { + return `${(sizeBytes / 1024).toFixed(1)} KB`; + } + + return `${(sizeBytes / (1024 * 1024)).toFixed(1)} MB`; +} + +export function FileAttachmentsPanel({ + ownerType, + ownerId, + eyebrow, + title, + description, + emptyMessage, + onAttachmentCountChange, +}: FileAttachmentsPanelProps) { + const { token, user } = useAuth(); + const [attachments, setAttachments] = useState([]); + const [status, setStatus] = useState("Loading attachments..."); + const [isUploading, setIsUploading] = useState(false); + const [deletingAttachmentId, setDeletingAttachmentId] = useState(null); + const [attachmentPendingDelete, setAttachmentPendingDelete] = useState(null); + + const canReadFiles = user?.permissions.includes(permissions.filesRead) ?? false; + const canWriteFiles = user?.permissions.includes(permissions.filesWrite) ?? false; + + useEffect(() => { + if (!token || !canReadFiles) { + return; + } + + api + .getAttachments(token, ownerType, ownerId) + .then((nextAttachments) => { + setAttachments(nextAttachments); + onAttachmentCountChange?.(nextAttachments.length); + setStatus(nextAttachments.length === 0 ? "No attachments uploaded yet." : `${nextAttachments.length} attachment(s) available.`); + }) + .catch((error: unknown) => { + const message = error instanceof ApiError ? error.message : "Unable to load attachments."; + setStatus(message); + }); + }, [canReadFiles, onAttachmentCountChange, ownerId, ownerType, token]); + + async function handleUpload(event: React.ChangeEvent) { + const file = event.target.files?.[0]; + if (!file || !token || !canWriteFiles) { + return; + } + + setIsUploading(true); + setStatus("Uploading attachment..."); + + try { + const attachment = await api.uploadFile(token, file, ownerType, ownerId); + setAttachments((current) => { + const nextAttachments = [attachment, ...current]; + onAttachmentCountChange?.(nextAttachments.length); + return nextAttachments; + }); + setStatus("Attachment uploaded."); + } catch (error: unknown) { + const message = error instanceof ApiError ? error.message : "Unable to upload attachment."; + setStatus(message); + } finally { + setIsUploading(false); + event.target.value = ""; + } + } + + async function handleOpen(attachment: FileAttachmentDto) { + if (!token) { + return; + } + + try { + const blob = await api.getFileContentBlob(token, attachment.id); + const objectUrl = window.URL.createObjectURL(blob); + window.open(objectUrl, "_blank", "noopener,noreferrer"); + window.setTimeout(() => window.URL.revokeObjectURL(objectUrl), 60_000); + } catch (error: unknown) { + const message = error instanceof ApiError ? error.message : "Unable to open attachment."; + setStatus(message); + } + } + + async function handleDelete(attachment: FileAttachmentDto) { + if (!token || !canWriteFiles) { + return; + } + + setDeletingAttachmentId(attachment.id); + setStatus(`Deleting ${attachment.originalName}...`); + + try { + await api.deleteAttachment(token, attachment.id); + setAttachments((current) => { + const nextAttachments = current.filter((item) => item.id !== attachment.id); + onAttachmentCountChange?.(nextAttachments.length); + return nextAttachments; + }); + setStatus("Attachment deleted. Upload a replacement file if this document is still required for the record."); + } catch (error: unknown) { + const message = error instanceof ApiError ? error.message : "Unable to delete attachment."; + setStatus(message); + } finally { + setDeletingAttachmentId(null); + setAttachmentPendingDelete(null); + } + } + + return ( +
+
+
+

{eyebrow}

+

{title}

+

{description}

+
+ {canWriteFiles ? ( + + ) : null} +
+
{status}
+ {!canReadFiles ? ( +
+ You do not have permission to view file attachments. +
+ ) : attachments.length === 0 ? ( +
+ {emptyMessage} +
+ ) : ( +
+ {attachments.map((attachment) => ( +
+
+

{attachment.originalName}

+

+ {attachment.mimeType} · {formatFileSize(attachment.sizeBytes)} · {new Date(attachment.createdAt).toLocaleString()} +

+
+
+ + {canWriteFiles ? ( + + ) : null} +
+
+ ))} +
+ )} + { + if (!deletingAttachmentId) { + setAttachmentPendingDelete(null); + } + }} + onConfirm={async () => { + if (attachmentPendingDelete) { + await handleDelete(attachmentPendingDelete); + } + }} + /> +
+ ); +} + diff --git a/client/src/components/ProtectedRoute.tsx b/client/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..1791994 --- /dev/null +++ b/client/src/components/ProtectedRoute.tsx @@ -0,0 +1,22 @@ +import type { PermissionKey } from "@mrp/shared"; +import { Navigate, Outlet } from "react-router-dom"; + +import { useAuth } from "../auth/AuthProvider"; + +export function ProtectedRoute({ requiredPermissions = [] }: { requiredPermissions?: PermissionKey[] }) { + const { isReady, token, user } = useAuth(); + + if (!isReady) { + return
Loading workspace...
; + } + + if (!token || !user) { + return ; + } + + const permissionSet = new Set(user.permissions); + const allowed = requiredPermissions.every((permission) => permissionSet.has(permission)); + + return allowed ? : ; +} + diff --git a/client/src/components/ThemeToggle.tsx b/client/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..9aa8115 --- /dev/null +++ b/client/src/components/ThemeToggle.tsx @@ -0,0 +1,15 @@ +import { useTheme } from "../theme/ThemeProvider"; + +export function ThemeToggle() { + const { mode, toggleMode } = useTheme(); + + return ( + + ); +} diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 0000000..32759cd --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,102 @@ +@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap"); + +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + color-scheme: light; + --font-family: "Manrope"; + --color-brand: 24 90 219; + --color-accent: 0 166 166; + --color-surface-brand: 244 247 251; + --color-surface: var(--color-surface-brand); + --color-page: 248 250 252; + --color-text: 15 23 42; + --color-muted: 90 106 133; + --color-line: 215 222 235; +} + +.dark { + color-scheme: dark; + --color-brand: 63 140 255; + --color-accent: 34 211 238; + --color-surface: 30 41 59; + --color-page: 2 6 23; + --color-text: 226 232 240; + --color-muted: 148 163 184; + --color-line: 51 65 85; +} + +html, +body, +#root { + min-height: 100%; +} + +body { + background: + radial-gradient(circle at top left, rgb(var(--color-brand) / 0.18), transparent 32%), + radial-gradient(circle at top right, rgb(var(--color-accent) / 0.16), transparent 25%), + rgb(var(--color-page)); + color: rgb(var(--color-text)); + font-family: var(--font-family), sans-serif; +} + +input, +textarea, +select, +button { + font: inherit; +} + +input:not([type="color"]), +textarea, +select { + color: rgb(var(--color-text)); +} + +input::placeholder, +textarea::placeholder { + color: rgb(var(--color-muted)); +} + +.gantt-theme .wx-bar, +.gantt-theme .wx-task { + fill: rgb(var(--color-brand)); +} + +.gantt-theme { + --wx-font-family: var(--font-family), sans-serif; + --wx-background: rgb(var(--color-page)); + --wx-background-alt: rgb(var(--color-surface)); + --wx-color-font: rgb(var(--color-text)); + --wx-color-secondary-font: rgb(var(--color-muted)); + --wx-color-font-disabled: rgb(var(--color-muted)); + --wx-color-link: rgb(var(--color-brand)); + --wx-color-primary: rgb(var(--color-brand)); + --wx-icon-color: rgb(var(--color-muted)); + --wx-border: 1px solid rgb(var(--color-line)); + --wx-box-shadow: 0 24px 60px rgba(15, 23, 42, 0.14); +} + +.gantt-theme .wx-layout, +.gantt-theme .wx-scale, +.gantt-theme .wx-gantt, +.gantt-theme .wx-table-container { + background-color: rgb(var(--color-page)); + color: rgb(var(--color-text)); +} + +.gantt-theme .wx-grid, +.gantt-theme .wx-cell, +.gantt-theme .wx-row, +.gantt-theme .wx-text, +.gantt-theme .wx-task-name { + color: rgb(var(--color-text)); +} + +.gantt-theme .wx-cell, +.gantt-theme .wx-row { + border-color: rgb(var(--color-line)); +} diff --git a/client/src/lib/api.ts b/client/src/lib/api.ts new file mode 100644 index 0000000..7928f0c --- /dev/null +++ b/client/src/lib/api.ts @@ -0,0 +1,896 @@ +import type { + AdminDiagnosticsDto, + AdminAuthSessionDto, + BackupGuidanceDto, + AdminPermissionOptionDto, + AdminRoleDto, + AdminRoleInput, + SupportLogEntryDto, + SupportLogFiltersDto, + SupportLogListDto, + SupportSnapshotDto, + AdminUserDto, + AdminUserInput, + ApiResponse, + CompanyProfileDto, + CompanyProfileInput, + FileAttachmentDto, + PlanningTimelineDto, + LoginRequest, + LoginResponse, + LogoutResponse, +} from "@mrp/shared"; +import type { + CrmContactDto, + CrmContactInput, + CrmContactEntryDto, + CrmContactEntryInput, + CrmCustomerHierarchyOptionDto, + CrmRecordDetailDto, + CrmRecordInput, + CrmLifecycleStage, + CrmRecordStatus, + CrmRecordSummaryDto, +} from "@mrp/shared/dist/crm/types.js"; +import type { + InventoryItemDetailDto, + InventoryItemInput, + InventoryItemOptionDto, + InventorySkuBuilderPreviewDto, + InventorySkuCatalogTreeDto, + InventorySkuFamilyDto, + InventorySkuFamilyInput, + InventorySkuNodeDto, + InventorySkuNodeInput, + InventoryReservationInput, + InventoryItemStatus, + InventoryItemSummaryDto, + InventoryTransferInput, + InventoryTransactionInput, + InventoryItemType, + WarehouseDetailDto, + WarehouseInput, + WarehouseLocationOptionDto, + WarehouseSummaryDto, +} from "@mrp/shared/dist/inventory/types.js"; +import type { + ManufacturingStationDto, + ManufacturingStationInput, + ManufacturingItemOptionDto, + ManufacturingProjectOptionDto, + WorkOrderCompletionInput, + WorkOrderDetailDto, + WorkOrderInput, + WorkOrderMaterialIssueInput, + WorkOrderStatus, + WorkOrderSummaryDto, +} from "@mrp/shared"; +import type { + ProjectCustomerOptionDto, + ProjectDetailDto, + ProjectDocumentOptionDto, + ProjectInput, + ProjectOwnerOptionDto, + ProjectPriority, + ProjectShipmentOptionDto, + ProjectStatus, + ProjectSummaryDto, +} from "@mrp/shared/dist/projects/types.js"; +import type { + SalesCustomerOptionDto, + DemandPlanningRollupDto, + SalesDocumentDetailDto, + SalesDocumentInput, + SalesOrderPlanningDto, + SalesDocumentRevisionDto, + SalesDocumentStatus, + SalesDocumentSummaryDto, +} from "@mrp/shared/dist/sales/types.js"; +import type { + PurchaseOrderDetailDto, + PurchaseOrderInput, + PurchaseOrderRevisionDto, + PurchaseOrderStatus, + PurchaseOrderSummaryDto, + PurchaseVendorOptionDto, +} from "@mrp/shared"; +import type { PurchaseReceiptInput } from "@mrp/shared/dist/purchasing/types.js"; +import type { + ShipmentDetailDto, + ShipmentInput, + ShipmentOrderOptionDto, + ShipmentStatus, + ShipmentSummaryDto, +} from "@mrp/shared/dist/shipping/types.js"; + +export class ApiError extends Error { + constructor(message: string, public readonly code: string) { + super(message); + } +} + +async function request(input: string, init?: RequestInit, token?: string): Promise { + const response = await fetch(input, { + ...init, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...(init?.headers ?? {}), + }, + }); + + const json = (await response.json()) as ApiResponse; + if (!json.ok) { + throw new ApiError(json.error.message, json.error.code); + } + + return json.data; +} + +function buildQueryString(params: Record) { + const searchParams = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (value) { + searchParams.set(key, value); + } + } + + const queryString = searchParams.toString(); + return queryString ? `?${queryString}` : ""; +} + +export const api = { + login(payload: LoginRequest) { + return request("/api/v1/auth/login", { + method: "POST", + body: JSON.stringify(payload), + }); + }, + me(token: string) { + return request("/api/v1/auth/me", undefined, token); + }, + logout(token: string) { + return request("/api/v1/auth/logout", { method: "POST" }, token); + }, + getAdminDiagnostics(token: string) { + return request("/api/v1/admin/diagnostics", undefined, token); + }, + getBackupGuidance(token: string) { + return request("/api/v1/admin/backup-guidance", undefined, token); + }, + getSupportSnapshot(token: string) { + return request("/api/v1/admin/support-snapshot", undefined, token); + }, + getSupportSnapshotWithFilters(token: string, filters?: SupportLogFiltersDto) { + return request( + `/api/v1/admin/support-snapshot${buildQueryString({ + level: filters?.level, + source: filters?.source, + query: filters?.query, + start: filters?.start, + end: filters?.end, + limit: filters?.limit?.toString(), + })}`, + undefined, + token + ); + }, + getSupportLogs(token: string, filters?: SupportLogFiltersDto) { + return request( + `/api/v1/admin/support-logs${buildQueryString({ + level: filters?.level, + source: filters?.source, + query: filters?.query, + start: filters?.start, + end: filters?.end, + limit: filters?.limit?.toString(), + })}`, + undefined, + token + ); + }, + getAdminPermissions(token: string) { + return request("/api/v1/admin/permissions", undefined, token); + }, + getAdminRoles(token: string) { + return request("/api/v1/admin/roles", undefined, token); + }, + createAdminRole(token: string, payload: AdminRoleInput) { + return request("/api/v1/admin/roles", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateAdminRole(token: string, roleId: string, payload: AdminRoleInput) { + return request(`/api/v1/admin/roles/${roleId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + getAdminUsers(token: string) { + return request("/api/v1/admin/users", undefined, token); + }, + getAdminSessions(token: string) { + return request("/api/v1/admin/sessions", undefined, token); + }, + revokeAdminSession(token: string, sessionId: string) { + return request(`/api/v1/admin/sessions/${sessionId}/revoke`, { method: "POST" }, token); + }, + createAdminUser(token: string, payload: AdminUserInput) { + return request("/api/v1/admin/users", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateAdminUser(token: string, userId: string, payload: AdminUserInput) { + return request(`/api/v1/admin/users/${userId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + getCompanyProfile(token: string) { + return request("/api/v1/company-profile", undefined, token); + }, + updateCompanyProfile(token: string, payload: CompanyProfileInput) { + return request( + "/api/v1/company-profile", + { + method: "PUT", + body: JSON.stringify(payload), + }, + token + ); + }, + async uploadFile(token: string, file: File, ownerType: string, ownerId: string) { + const formData = new FormData(); + formData.append("file", file); + formData.append("ownerType", ownerType); + formData.append("ownerId", ownerId); + + const response = await fetch("/api/v1/files/upload", { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + }, + body: formData, + }); + const json = (await response.json()) as ApiResponse; + if (!json.ok) { + throw new ApiError(json.error.message, json.error.code); + } + return json.data; + }, + async getFileContentBlob(token: string, fileId: string) { + const response = await fetch(`/api/v1/files/${fileId}/content`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to load file content.", "FILE_CONTENT_FAILED"); + } + + return response.blob(); + }, + getAttachments(token: string, ownerType: string, ownerId: string) { + return request( + `/api/v1/files${buildQueryString({ + ownerType, + ownerId, + })}`, + undefined, + token + ); + }, + deleteAttachment(token: string, fileId: string) { + return request( + `/api/v1/files/${fileId}`, + { + method: "DELETE", + }, + token + ); + }, + getCustomers( + token: string, + filters?: { + q?: string; + status?: CrmRecordStatus; + lifecycleStage?: CrmLifecycleStage; + state?: string; + flag?: "PREFERRED" | "STRATEGIC" | "REQUIRES_APPROVAL" | "BLOCKED"; + } + ) { + return request( + `/api/v1/crm/customers${buildQueryString({ + q: filters?.q, + status: filters?.status, + lifecycleStage: filters?.lifecycleStage, + state: filters?.state, + flag: filters?.flag, + })}`, + undefined, + token + ); + }, + getCustomer(token: string, customerId: string) { + return request(`/api/v1/crm/customers/${customerId}`, undefined, token); + }, + getCustomerHierarchyOptions(token: string, excludeCustomerId?: string) { + return request( + `/api/v1/crm/customers/hierarchy-options${buildQueryString({ + excludeCustomerId, + })}`, + undefined, + token + ); + }, + createCustomer(token: string, payload: CrmRecordInput) { + return request( + "/api/v1/crm/customers", + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + updateCustomer(token: string, customerId: string, payload: CrmRecordInput) { + return request( + `/api/v1/crm/customers/${customerId}`, + { + method: "PUT", + body: JSON.stringify(payload), + }, + token + ); + }, + createCustomerContactEntry(token: string, customerId: string, payload: CrmContactEntryInput) { + return request( + `/api/v1/crm/customers/${customerId}/contact-history`, + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + createCustomerContact(token: string, customerId: string, payload: CrmContactInput) { + return request( + `/api/v1/crm/customers/${customerId}/contacts`, + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + getVendors( + token: string, + filters?: { + q?: string; + status?: CrmRecordStatus; + lifecycleStage?: CrmLifecycleStage; + state?: string; + flag?: "PREFERRED" | "STRATEGIC" | "REQUIRES_APPROVAL" | "BLOCKED"; + } + ) { + return request( + `/api/v1/crm/vendors${buildQueryString({ + q: filters?.q, + status: filters?.status, + lifecycleStage: filters?.lifecycleStage, + state: filters?.state, + flag: filters?.flag, + })}`, + undefined, + token + ); + }, + getVendor(token: string, vendorId: string) { + return request(`/api/v1/crm/vendors/${vendorId}`, undefined, token); + }, + createVendor(token: string, payload: CrmRecordInput) { + return request( + "/api/v1/crm/vendors", + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + updateVendor(token: string, vendorId: string, payload: CrmRecordInput) { + return request( + `/api/v1/crm/vendors/${vendorId}`, + { + method: "PUT", + body: JSON.stringify(payload), + }, + token + ); + }, + createVendorContactEntry(token: string, vendorId: string, payload: CrmContactEntryInput) { + return request( + `/api/v1/crm/vendors/${vendorId}/contact-history`, + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + createVendorContact(token: string, vendorId: string, payload: CrmContactInput) { + return request( + `/api/v1/crm/vendors/${vendorId}/contacts`, + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + getInventoryItems(token: string, filters?: { q?: string; status?: InventoryItemStatus; type?: InventoryItemType }) { + return request( + `/api/v1/inventory/items${buildQueryString({ + q: filters?.q, + status: filters?.status, + type: filters?.type, + })}`, + undefined, + token + ); + }, + getInventoryItem(token: string, itemId: string) { + return request(`/api/v1/inventory/items/${itemId}`, undefined, token); + }, + getInventoryItemOptions(token: string) { + return request("/api/v1/inventory/items/options", undefined, token); + }, + getInventorySkuFamilies(token: string) { + return request("/api/v1/inventory/sku/families", undefined, token); + }, + getInventorySkuCatalog(token: string) { + return request("/api/v1/inventory/sku/catalog", undefined, token); + }, + getInventorySkuNodes(token: string, familyId: string, parentNodeId?: string | null) { + return request( + `/api/v1/inventory/sku/nodes${buildQueryString({ + familyId, + parentNodeId: parentNodeId ?? undefined, + })}`, + undefined, + token + ); + }, + getInventorySkuPreview(token: string, familyId: string, nodeId?: string | null) { + return request( + `/api/v1/inventory/sku/preview${buildQueryString({ + familyId, + nodeId: nodeId ?? undefined, + })}`, + undefined, + token + ); + }, + createInventorySkuFamily(token: string, payload: InventorySkuFamilyInput) { + return request("/api/v1/inventory/sku/families", { method: "POST", body: JSON.stringify(payload) }, token); + }, + createInventorySkuNode(token: string, payload: InventorySkuNodeInput) { + return request("/api/v1/inventory/sku/nodes", { method: "POST", body: JSON.stringify(payload) }, token); + }, + getWarehouseLocationOptions(token: string) { + return request("/api/v1/inventory/locations/options", undefined, token); + }, + createInventoryItem(token: string, payload: InventoryItemInput) { + return request( + "/api/v1/inventory/items", + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + updateInventoryItem(token: string, itemId: string, payload: InventoryItemInput) { + return request( + `/api/v1/inventory/items/${itemId}`, + { + method: "PUT", + body: JSON.stringify(payload), + }, + token + ); + }, + createInventoryTransaction(token: string, itemId: string, payload: InventoryTransactionInput) { + return request( + `/api/v1/inventory/items/${itemId}/transactions`, + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + createInventoryTransfer(token: string, itemId: string, payload: InventoryTransferInput) { + return request( + `/api/v1/inventory/items/${itemId}/transfers`, + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + createInventoryReservation(token: string, itemId: string, payload: InventoryReservationInput) { + return request( + `/api/v1/inventory/items/${itemId}/reservations`, + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + getWarehouses(token: string) { + return request("/api/v1/inventory/warehouses", undefined, token); + }, + getWarehouse(token: string, warehouseId: string) { + return request(`/api/v1/inventory/warehouses/${warehouseId}`, undefined, token); + }, + createWarehouse(token: string, payload: WarehouseInput) { + return request( + "/api/v1/inventory/warehouses", + { + method: "POST", + body: JSON.stringify(payload), + }, + token + ); + }, + updateWarehouse(token: string, warehouseId: string, payload: WarehouseInput) { + return request( + `/api/v1/inventory/warehouses/${warehouseId}`, + { + method: "PUT", + body: JSON.stringify(payload), + }, + token + ); + }, + getProjects( + token: string, + filters?: { q?: string; status?: ProjectStatus; priority?: ProjectPriority; customerId?: string; ownerId?: string } + ) { + return request( + `/api/v1/projects${buildQueryString({ + q: filters?.q, + status: filters?.status, + priority: filters?.priority, + customerId: filters?.customerId, + ownerId: filters?.ownerId, + })}`, + undefined, + token + ); + }, + getProject(token: string, projectId: string) { + return request(`/api/v1/projects/${projectId}`, undefined, token); + }, + createProject(token: string, payload: ProjectInput) { + return request("/api/v1/projects", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateProject(token: string, projectId: string, payload: ProjectInput) { + return request(`/api/v1/projects/${projectId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + getProjectCustomerOptions(token: string) { + return request("/api/v1/projects/customers/options", undefined, token); + }, + getProjectOwnerOptions(token: string) { + return request("/api/v1/projects/owners/options", undefined, token); + }, + getProjectQuoteOptions(token: string, customerId?: string) { + return request( + `/api/v1/projects/quotes/options${buildQueryString({ customerId })}`, + undefined, + token + ); + }, + getProjectOrderOptions(token: string, customerId?: string) { + return request( + `/api/v1/projects/orders/options${buildQueryString({ customerId })}`, + undefined, + token + ); + }, + getProjectShipmentOptions(token: string, customerId?: string) { + return request( + `/api/v1/projects/shipments/options${buildQueryString({ customerId })}`, + undefined, + token + ); + }, + getManufacturingItemOptions(token: string) { + return request("/api/v1/manufacturing/items/options", undefined, token); + }, + getManufacturingProjectOptions(token: string) { + return request("/api/v1/manufacturing/projects/options", undefined, token); + }, + getManufacturingStations(token: string) { + return request("/api/v1/manufacturing/stations", undefined, token); + }, + createManufacturingStation(token: string, payload: ManufacturingStationInput) { + return request("/api/v1/manufacturing/stations", { method: "POST", body: JSON.stringify(payload) }, token); + }, + getWorkOrders(token: string, filters?: { q?: string; status?: WorkOrderStatus; projectId?: string; itemId?: string }) { + return request( + `/api/v1/manufacturing/work-orders${buildQueryString({ + q: filters?.q, + status: filters?.status, + projectId: filters?.projectId, + itemId: filters?.itemId, + })}`, + undefined, + token + ); + }, + getWorkOrder(token: string, workOrderId: string) { + return request(`/api/v1/manufacturing/work-orders/${workOrderId}`, undefined, token); + }, + createWorkOrder(token: string, payload: WorkOrderInput) { + return request("/api/v1/manufacturing/work-orders", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateWorkOrder(token: string, workOrderId: string, payload: WorkOrderInput) { + return request(`/api/v1/manufacturing/work-orders/${workOrderId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + updateWorkOrderStatus(token: string, workOrderId: string, status: WorkOrderStatus) { + return request( + `/api/v1/manufacturing/work-orders/${workOrderId}/status`, + { method: "PATCH", body: JSON.stringify({ status }) }, + token + ); + }, + issueWorkOrderMaterial(token: string, workOrderId: string, payload: WorkOrderMaterialIssueInput) { + return request( + `/api/v1/manufacturing/work-orders/${workOrderId}/issues`, + { method: "POST", body: JSON.stringify(payload) }, + token + ); + }, + recordWorkOrderCompletion(token: string, workOrderId: string, payload: WorkOrderCompletionInput) { + return request( + `/api/v1/manufacturing/work-orders/${workOrderId}/completions`, + { method: "POST", body: JSON.stringify(payload) }, + token + ); + }, + getPlanningTimeline(token: string) { + return request("/api/v1/gantt/timeline", undefined, token); + }, + getSalesCustomers(token: string) { + return request("/api/v1/sales/customers/options", undefined, token); + }, + getPurchaseVendors(token: string) { + return request("/api/v1/purchasing/vendors/options", undefined, token); + }, + getQuotes(token: string, filters?: { q?: string; status?: SalesDocumentStatus }) { + return request( + `/api/v1/sales/quotes${buildQueryString({ + q: filters?.q, + status: filters?.status, + })}`, + undefined, + token + ); + }, + getQuote(token: string, quoteId: string) { + return request(`/api/v1/sales/quotes/${quoteId}`, undefined, token); + }, + createQuote(token: string, payload: SalesDocumentInput) { + return request("/api/v1/sales/quotes", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateQuote(token: string, quoteId: string, payload: SalesDocumentInput) { + return request(`/api/v1/sales/quotes/${quoteId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + updateQuoteStatus(token: string, quoteId: string, status: SalesDocumentStatus) { + return request( + `/api/v1/sales/quotes/${quoteId}/status`, + { method: "PATCH", body: JSON.stringify({ status }) }, + token + ); + }, + approveQuote(token: string, quoteId: string) { + return request(`/api/v1/sales/quotes/${quoteId}/approve`, { method: "POST" }, token); + }, + getQuoteRevisions(token: string, quoteId: string) { + return request(`/api/v1/sales/quotes/${quoteId}/revisions`, undefined, token); + }, + convertQuoteToSalesOrder(token: string, quoteId: string) { + return request(`/api/v1/sales/quotes/${quoteId}/convert`, { method: "POST" }, token); + }, + getSalesOrders(token: string, filters?: { q?: string; status?: SalesDocumentStatus }) { + return request( + `/api/v1/sales/orders${buildQueryString({ + q: filters?.q, + status: filters?.status, + })}`, + undefined, + token + ); + }, + getSalesOrder(token: string, orderId: string) { + return request(`/api/v1/sales/orders/${orderId}`, undefined, token); + }, + getSalesOrderPlanning(token: string, orderId: string) { + return request(`/api/v1/sales/orders/${orderId}/planning`, undefined, token); + }, + getDemandPlanningRollup(token: string) { + return request("/api/v1/sales/planning-rollup", undefined, token); + }, + createSalesOrder(token: string, payload: SalesDocumentInput) { + return request("/api/v1/sales/orders", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateSalesOrder(token: string, orderId: string, payload: SalesDocumentInput) { + return request(`/api/v1/sales/orders/${orderId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + updateSalesOrderStatus(token: string, orderId: string, status: SalesDocumentStatus) { + return request( + `/api/v1/sales/orders/${orderId}/status`, + { method: "PATCH", body: JSON.stringify({ status }) }, + token + ); + }, + approveSalesOrder(token: string, orderId: string) { + return request(`/api/v1/sales/orders/${orderId}/approve`, { method: "POST" }, token); + }, + getSalesOrderRevisions(token: string, orderId: string) { + return request(`/api/v1/sales/orders/${orderId}/revisions`, undefined, token); + }, + getPurchaseOrders(token: string, filters?: { q?: string; status?: PurchaseOrderStatus; vendorId?: string }) { + return request( + `/api/v1/purchasing/orders${buildQueryString({ + q: filters?.q, + status: filters?.status, + vendorId: filters?.vendorId, + })}`, + undefined, + token + ); + }, + getPurchaseOrder(token: string, orderId: string) { + return request(`/api/v1/purchasing/orders/${orderId}`, undefined, token); + }, + getPurchaseOrderRevisions(token: string, orderId: string) { + return request(`/api/v1/purchasing/orders/${orderId}/revisions`, undefined, token); + }, + createPurchaseOrder(token: string, payload: PurchaseOrderInput) { + return request("/api/v1/purchasing/orders", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updatePurchaseOrder(token: string, orderId: string, payload: PurchaseOrderInput) { + return request(`/api/v1/purchasing/orders/${orderId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + updatePurchaseOrderStatus(token: string, orderId: string, status: PurchaseOrderStatus) { + return request( + `/api/v1/purchasing/orders/${orderId}/status`, + { method: "PATCH", body: JSON.stringify({ status }) }, + token + ); + }, + createPurchaseReceipt(token: string, orderId: string, payload: PurchaseReceiptInput) { + return request( + `/api/v1/purchasing/orders/${orderId}/receipts`, + { method: "POST", body: JSON.stringify(payload) }, + token + ); + }, + getShipmentOrderOptions(token: string) { + return request("/api/v1/shipping/orders/options", undefined, token); + }, + getShipments(token: string, filters?: { q?: string; status?: ShipmentStatus; salesOrderId?: string }) { + return request( + `/api/v1/shipping/shipments${buildQueryString({ + q: filters?.q, + status: filters?.status, + salesOrderId: filters?.salesOrderId, + })}`, + undefined, + token + ); + }, + getShipment(token: string, shipmentId: string) { + return request(`/api/v1/shipping/shipments/${shipmentId}`, undefined, token); + }, + createShipment(token: string, payload: ShipmentInput) { + return request("/api/v1/shipping/shipments", { method: "POST", body: JSON.stringify(payload) }, token); + }, + updateShipment(token: string, shipmentId: string, payload: ShipmentInput) { + return request(`/api/v1/shipping/shipments/${shipmentId}`, { method: "PUT", body: JSON.stringify(payload) }, token); + }, + updateShipmentStatus(token: string, shipmentId: string, status: ShipmentStatus) { + return request( + `/api/v1/shipping/shipments/${shipmentId}/status`, + { method: "PATCH", body: JSON.stringify({ status }) }, + token + ); + }, + async getShipmentPackingSlipPdf(token: string, shipmentId: string) { + const response = await fetch(`/api/v1/documents/shipping/shipments/${shipmentId}/packing-slip.pdf`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render packing slip PDF.", "PACKING_SLIP_FAILED"); + } + + return response.blob(); + }, + async getShipmentLabelPdf(token: string, shipmentId: string) { + const response = await fetch(`/api/v1/documents/shipping/shipments/${shipmentId}/shipping-label.pdf`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render shipping label PDF.", "SHIPPING_LABEL_FAILED"); + } + + return response.blob(); + }, + async getShipmentBillOfLadingPdf(token: string, shipmentId: string) { + const response = await fetch(`/api/v1/documents/shipping/shipments/${shipmentId}/bill-of-lading.pdf`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render bill of lading PDF.", "BILL_OF_LADING_FAILED"); + } + + return response.blob(); + }, + async getQuotePdf(token: string, quoteId: string) { + const response = await fetch(`/api/v1/documents/sales/quotes/${quoteId}/document.pdf`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render quote PDF.", "QUOTE_PDF_FAILED"); + } + + return response.blob(); + }, + async getSalesOrderPdf(token: string, orderId: string) { + const response = await fetch(`/api/v1/documents/sales/orders/${orderId}/document.pdf`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render sales order PDF.", "SALES_ORDER_PDF_FAILED"); + } + + return response.blob(); + }, + async getPurchaseOrderPdf(token: string, orderId: string) { + const response = await fetch(`/api/v1/documents/purchasing/orders/${orderId}/document.pdf`, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render purchase order PDF.", "PURCHASE_ORDER_PDF_FAILED"); + } + + return response.blob(); + }, + async getCompanyProfilePreviewPdf(token: string) { + const response = await fetch("/api/v1/documents/company-profile-preview.pdf", { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + + if (!response.ok) { + throw new ApiError("Unable to render company profile preview PDF.", "PDF_PREVIEW_FAILED"); + } + + return response.blob(); + }, +}; diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..b22a78a --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,273 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import React from "react"; +import ReactDOM from "react-dom/client"; +import { createBrowserRouter, Navigate, RouterProvider } from "react-router-dom"; +import { permissions } from "@mrp/shared"; + +import { AppShell } from "./components/AppShell"; +import { ProtectedRoute } from "./components/ProtectedRoute"; +import { AuthProvider } from "./auth/AuthProvider"; +import { DashboardPage } from "./modules/dashboard/DashboardPage"; +import { LoginPage } from "./modules/login/LoginPage"; +import { ThemeProvider } from "./theme/ThemeProvider"; +import "./index.css"; + +const queryClient = new QueryClient(); + +const CompanySettingsPage = React.lazy(() => + import("./modules/settings/CompanySettingsPage").then((module) => ({ default: module.CompanySettingsPage })) +); +const AdminDiagnosticsPage = React.lazy(() => + import("./modules/settings/AdminDiagnosticsPage").then((module) => ({ default: module.AdminDiagnosticsPage })) +); +const UserManagementPage = React.lazy(() => + import("./modules/settings/UserManagementPage").then((module) => ({ default: module.UserManagementPage })) +); +const CustomersPage = React.lazy(() => + import("./modules/crm/CustomersPage").then((module) => ({ default: module.CustomersPage })) +); +const VendorsPage = React.lazy(() => + import("./modules/crm/VendorsPage").then((module) => ({ default: module.VendorsPage })) +); +const CrmDetailPage = React.lazy(() => + import("./modules/crm/CrmDetailPage").then((module) => ({ default: module.CrmDetailPage })) +); +const CrmFormPage = React.lazy(() => + import("./modules/crm/CrmFormPage").then((module) => ({ default: module.CrmFormPage })) +); +const InventoryItemsPage = React.lazy(() => + import("./modules/inventory/InventoryItemsPage").then((module) => ({ default: module.InventoryItemsPage })) +); +const InventoryDetailPage = React.lazy(() => + import("./modules/inventory/InventoryDetailPage").then((module) => ({ default: module.InventoryDetailPage })) +); +const InventoryFormPage = React.lazy(() => + import("./modules/inventory/InventoryFormPage").then((module) => ({ default: module.InventoryFormPage })) +); +const InventorySkuMasterPage = React.lazy(() => + import("./modules/inventory/InventorySkuMasterPage").then((module) => ({ default: module.InventorySkuMasterPage })) +); +const WarehousesPage = React.lazy(() => + import("./modules/inventory/WarehousesPage").then((module) => ({ default: module.WarehousesPage })) +); +const WarehouseDetailPage = React.lazy(() => + import("./modules/inventory/WarehouseDetailPage").then((module) => ({ default: module.WarehouseDetailPage })) +); +const WarehouseFormPage = React.lazy(() => + import("./modules/inventory/WarehouseFormPage").then((module) => ({ default: module.WarehouseFormPage })) +); +const ProjectsPage = React.lazy(() => + import("./modules/projects/ProjectsPage").then((module) => ({ default: module.ProjectsPage })) +); +const ProjectDetailPage = React.lazy(() => + import("./modules/projects/ProjectDetailPage").then((module) => ({ default: module.ProjectDetailPage })) +); +const ProjectFormPage = React.lazy(() => + import("./modules/projects/ProjectFormPage").then((module) => ({ default: module.ProjectFormPage })) +); +const ManufacturingPage = React.lazy(() => + import("./modules/manufacturing/ManufacturingPage").then((module) => ({ default: module.ManufacturingPage })) +); +const WorkOrderDetailPage = React.lazy(() => + import("./modules/manufacturing/WorkOrderDetailPage").then((module) => ({ default: module.WorkOrderDetailPage })) +); +const WorkOrderFormPage = React.lazy(() => + import("./modules/manufacturing/WorkOrderFormPage").then((module) => ({ default: module.WorkOrderFormPage })) +); +const PurchaseListPage = React.lazy(() => + import("./modules/purchasing/PurchaseListPage").then((module) => ({ default: module.PurchaseListPage })) +); +const PurchaseDetailPage = React.lazy(() => + import("./modules/purchasing/PurchaseDetailPage").then((module) => ({ default: module.PurchaseDetailPage })) +); +const PurchaseFormPage = React.lazy(() => + import("./modules/purchasing/PurchaseFormPage").then((module) => ({ default: module.PurchaseFormPage })) +); +const SalesListPage = React.lazy(() => + import("./modules/sales/SalesListPage").then((module) => ({ default: module.SalesListPage })) +); +const SalesDetailPage = React.lazy(() => + import("./modules/sales/SalesDetailPage").then((module) => ({ default: module.SalesDetailPage })) +); +const SalesFormPage = React.lazy(() => + import("./modules/sales/SalesFormPage").then((module) => ({ default: module.SalesFormPage })) +); +const ShipmentListPage = React.lazy(() => + import("./modules/shipping/ShipmentListPage").then((module) => ({ default: module.ShipmentListPage })) +); +const ShipmentDetailPage = React.lazy(() => + import("./modules/shipping/ShipmentDetailPage").then((module) => ({ default: module.ShipmentDetailPage })) +); +const ShipmentFormPage = React.lazy(() => + import("./modules/shipping/ShipmentFormPage").then((module) => ({ default: module.ShipmentFormPage })) +); +const GanttPage = React.lazy(() => + import("./modules/gantt/GanttPage").then((module) => ({ default: module.GanttPage })) +); + +function RouteFallback() { + return ( +
+ Loading module... +
+ ); +} + +function lazyElement(element: React.ReactNode) { + return }>{element}; +} + +const router = createBrowserRouter([ + { path: "/login", element: }, + { + element: , + children: [ + { + element: , + children: [ + { path: "/", element: }, + { + element: , + children: [{ path: "/settings/company", element: lazyElement() }], + }, + { + element: , + children: [ + { path: "/settings/admin-diagnostics", element: lazyElement() }, + { path: "/settings/users", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/crm/customers", element: lazyElement() }, + { path: "/crm/customers/:customerId", element: lazyElement() }, + { path: "/crm/vendors", element: lazyElement() }, + { path: "/crm/vendors/:vendorId", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/inventory/items", element: lazyElement() }, + { path: "/inventory/items/:itemId", element: lazyElement() }, + { path: "/inventory/sku-master", element: lazyElement() }, + { path: "/inventory/warehouses", element: lazyElement() }, + { path: "/inventory/warehouses/:warehouseId", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/projects", element: lazyElement() }, + { path: "/projects/:projectId", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/manufacturing/work-orders", element: lazyElement() }, + { path: "/manufacturing/work-orders/:workOrderId", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/purchasing/orders", element: lazyElement() }, + { path: "/purchasing/orders/:orderId", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/sales/quotes", element: lazyElement() }, + { path: "/sales/quotes/:quoteId", element: lazyElement() }, + { path: "/sales/orders", element: lazyElement() }, + { path: "/sales/orders/:orderId", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/shipping/shipments", element: lazyElement() }, + { path: "/shipping/shipments/:shipmentId", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/crm/customers/new", element: lazyElement() }, + { path: "/crm/customers/:customerId/edit", element: lazyElement() }, + { path: "/crm/vendors/new", element: lazyElement() }, + { path: "/crm/vendors/:vendorId/edit", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/projects/new", element: lazyElement() }, + { path: "/projects/:projectId/edit", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/manufacturing/work-orders/new", element: lazyElement() }, + { path: "/manufacturing/work-orders/:workOrderId/edit", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/purchasing/orders/new", element: lazyElement() }, + { path: "/purchasing/orders/:orderId/edit", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/sales/quotes/new", element: lazyElement() }, + { path: "/sales/quotes/:quoteId/edit", element: lazyElement() }, + { path: "/sales/orders/new", element: lazyElement() }, + { path: "/sales/orders/:orderId/edit", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/shipping/shipments/new", element: lazyElement() }, + { path: "/shipping/shipments/:shipmentId/edit", element: lazyElement() }, + ], + }, + { + element: , + children: [ + { path: "/inventory/items/new", element: lazyElement() }, + { path: "/inventory/items/:itemId/edit", element: lazyElement() }, + { path: "/inventory/warehouses/new", element: lazyElement() }, + { path: "/inventory/warehouses/:warehouseId/edit", element: lazyElement() }, + ], + }, + { + element: , + children: [{ path: "/planning/gantt", element: lazyElement() }], + }, + ], + }, + ], + }, + { path: "*", element: }, +]); + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + + + + + +); + diff --git a/client/src/modules/crm/CrmAttachmentsPanel.tsx b/client/src/modules/crm/CrmAttachmentsPanel.tsx new file mode 100644 index 0000000..fd838b1 --- /dev/null +++ b/client/src/modules/crm/CrmAttachmentsPanel.tsx @@ -0,0 +1,21 @@ +import { FileAttachmentsPanel } from "../../components/FileAttachmentsPanel"; + +interface CrmAttachmentsPanelProps { + ownerType: string; + ownerId: string; + onAttachmentCountChange?: (count: number) => void; +} + +export function CrmAttachmentsPanel({ ownerType, ownerId, onAttachmentCountChange }: CrmAttachmentsPanelProps) { + return ( + + ); +} diff --git a/client/src/modules/crm/CrmContactEntryForm.tsx b/client/src/modules/crm/CrmContactEntryForm.tsx new file mode 100644 index 0000000..b67cadc --- /dev/null +++ b/client/src/modules/crm/CrmContactEntryForm.tsx @@ -0,0 +1,72 @@ +import type { CrmContactEntryInput } from "@mrp/shared/dist/crm/types.js"; + +import { crmContactTypeOptions } from "./config"; + +interface CrmContactEntryFormProps { + form: CrmContactEntryInput; + isSaving: boolean; + status: string; + onChange: (key: Key, value: CrmContactEntryInput[Key]) => void; + onSubmit: (event: React.FormEvent) => void; +} + +export function CrmContactEntryForm({ form, isSaving, status, onChange, onSubmit }: CrmContactEntryFormProps) { + return ( +
+
+ + +
+ +