# Absher operations dashboard — full guide

Use this file to continue work in another editor or AI tool. Open the repo at:

`C:\Users\lenovo\Projects\absher-dashboard`

Related product: **Absher** (system + admin dashboard that already owns client accounts and products). This repo is a **separate local operations dashboard** built from:

`C:\Users\lenovo\Downloads\Functional Specifications - Dashboard (English, with diagrams).pdf`

Version of the spec: **v1.0** (19 September 2026), **14 modules**.

This project is **local only**. There is no deploy setup on purpose.

---

## 1. What this app is

Internal dashboard for a distribution company (demo brand: Absher Distribution, Algeria / DZD).

Staff (internal users) sign in. They manage customers, catalog, prices, orders, documents, truck drivers, two kinds of rounds, payments, stats, late-payment tracking, and notifications. The UI is English or Arabic (RTL), light or dark.

**Truck drivers are not users.** They have no login and no app. Staff create a **driver card** and assign it to rounds.

The Codex project-management app (`C:\Users\lenovo\Projects\codex-project-management`) is a different product. Do not merge the two.

---

## 2. Start locally

Requirements: Node.js 20.9+, npm.

```powershell
cd C:\Users\lenovo\Projects\absher-dashboard
npm install
npx prisma generate
npx prisma db push
npm run db:seed
npm run dev
```

Open http://localhost:3000

Reset demo data:

```powershell
npm run db:reset
```

Env file (already present, not for git):

```
DATABASE_URL="file:./dev.db"
AUTH_SECRET="absher-local-dev-secret"
```

SQLite file: `prisma/dev.db`

### Demo logins

Password for every seeded user: `absher123`

| Role | Email |
| --- | --- |
| Super Admin | admin@absher.local |
| Sales Manager | sales@absher.local |
| Warehouse Agent | warehouse@absher.local |
| Accountant | accounts@absher.local |
| Dispatcher | dispatch@absher.local |

Seeded field data: customers (including one pending), 6 products with pictures (`public/products/*.svg`), a price and a min/max quantity per account type, orders, one **delivery** round (Algiers / Blida), one **van-sale** round (Oran west), drivers Bilal Ait and Sofiane Dahmani with GPS pins, a payment, a remittance, and a delivery note old enough to show as a late payment.

---

## 3. Stack (do not “upgrade Next” casually)

| Piece | Version / choice |
| --- | --- |
| Next.js | **16.3.5** App Router + Turbopack |
| React | 19.2.8 |
| Prisma | 6.19.3 + SQLite |
| Auth | JWT cookie `absher_session` via `jose` + `bcryptjs` |
| UI | Tailwind CSS 4, `lucide-react` icons |
| PDF | Print pages + browser “Print / save PDF” |
| Excel | CSV with BOM (`src/lib/format.ts` `toCsv`) so Excel opens it |

**This is not the Next.js from old training data.** Before changing routing, cookies, or server actions, read:

`node_modules/next/dist/docs/`

Important Next 16 facts already used here:

- Auth gate is `src/proxy.ts` (the `middleware.ts` convention is deprecated; export `proxy`).
- `searchParams` and `params` in pages are **Promises** — always `await` them.
- Server Actions live in `src/actions/*.ts` with `"use server"`.
- Use `revalidatePath` after mutations.

---

## 4. Folder map

```
absher-dashboard/
  prisma/schema.prisma     data model
  prisma/seed.ts           demo data (wipes tables then inserts)
  .env                     local SQLite + auth secret
  src/proxy.ts             redirects to /login if no cookie
  src/app/layout.tsx       root layout + Geist font
  src/app/globals.css      design tokens, .card .btn .input
  src/app/login/page.tsx   sign-in
  src/app/(dash)/          all signed-in pages (sidebar shell)
  src/app/print/           printable proforma, delivery note, round sheet
  src/app/api/export/      CSV downloads
  src/actions/             mutations
  src/lib/                 prisma, auth, pricing, documents, analysis, activity
  src/components/          shell, nav, ui, maps, driver activity
```

### Routes

| URL | Module |
| --- | --- |
| `/` | Overview |
| `/login` | Login |
| `/users` | Internal users, roles, permission matrix |
| `/activity` | Staff activity log + Excel |
| `/customers` `/customers/[id]` | Customer accounts |
| `/guide` | In-app user guide, English + Arabic, searchable. Content in `src/lib/guide.ts`; open to every role (`OPEN_PAGES` in `src/lib/access.ts`) |
| `/products` | Categories, products, stock moves |
| `/products/[id]` | Product page: photo, edit, stock history, orders |
| `/stock` | Stock ledger: every movement with type, document, user, balance; manual movements; Excel |
| `/purchases` `/purchases/[id]` | Purchases (achats): create, receive into stock, cancel, pay |
| `/suppliers` `/suppliers/[id]` | Suppliers (fournisseurs): details, purchases, payments, amount owed |
| `/pricing` | Price + min/max quantity per account type, minima, change log |
| `/orders` `/orders/[id]` | Orders |
| `/invoices` `/invoices/[id]` | Proformas |
| `/delivery-notes` `/delivery-notes/[id]` | Delivery notes |
| `/settings` | Company / document settings |
| `/store` | Point of sale: counter sales from the shop shelf at the retail price |
| `/store/sales` | Bons de vente (receipts), cancel puts goods back on the shelf |
| `/store/expenses` | Shop expenses by category |
| `/store/stock` | Shop shelf: move stock in from the warehouse, or return it |
| `/print/store-sale/[id]` | Printable sales receipt |
| `/rounds` `/rounds/[id]` | Delivery + van-sale rounds |
| `/fleet` `/fleet/[id]` | Driver cards + trucks + driver activity |
| `/fleet/trucks/[id]` | Truck edit + truck activity |
| `/payments/[id]` | Edit a payment + its change history |
| `/payments` | Payments, remittances, balances |
| `/statistics` | Period stats + Excel |
| `/analysis` | Charts, rankings, segments, late payers |
| `/notifications` | Management + customer messages |
| `/print/proforma/[id]` | Printable proforma |
| `/print/delivery/[id]` | Printable delivery note |
| `/print/round/[id]` | Round sheet |
| `/print/round/[id]/notes` | Every delivery note of one round (batch print) |
| `/api/uploads/...` | Uploaded product photos (files in `uploads/`, git-ignored) |
| `/api/export/activity` | CSV |
| `/api/export/payments` | CSV |
| `/api/export/stats` | CSV |
| `/api/export/analysis` | CSV |

### Actions

| File | Responsibility |
| --- | --- |
| `src/actions/auth.ts` | Login / logout |
| `src/actions/guard.ts` | `actor(department, operation)` |
| `src/actions/users.ts` | Users + roles |
| `src/actions/customers.ts` | Customers + credit limit |
| `src/actions/catalog.ts` | Categories, products, stock, prices |
| `src/actions/orders.ts` | Desk orders + status / delivery qty |
| `src/actions/invoices.ts` | Proformas + delivery notes |
| `src/actions/rounds.ts` | Both round types, truck load/return, field orders |
| `src/actions/fleet.ts` | Driver cards + vehicles |
| `src/actions/payments.ts` | Payments + remittances |
| `src/actions/settings.ts` | Company settings + mark notification read |
| `src/actions/locale.ts` | Language (en/ar) and theme (light/dark) cookies |

---

## 5. Domain rules (read before changing field logic)

### Internal users vs drivers

- **User** (`User` + `Role`): can log in. Permission JSON on the role: departments × `view|create|edit|delete`. Super Admin bypasses the matrix.  
Special permissions live under `special` in the same JSON (`SPECIAL_PERMISSIONS` in `src/lib/auth.ts`). `editValidatedInvoices` lets a role edit a validated proforma; only Super Admin has it by default and can grant it per role on `/users`.
- **Driver** (`DeliveryAgent`): card only — name, phone, region, address, lat/lng, assigned truck. Created by staff. Assigned to rounds. **Never add a password or session for drivers** unless the spec changes.

### Customers

Statuses: `PENDING` `ACTIVE` `REJECTED` `INACTIVE`  
Account types: `RETAIL` `SEMI_WHOLESALE` `WHOLESALE`  
Credit limit: block new orders when `outstanding + order total > creditLimit`. Notify management.

### Pricing

Each product has one price per account type (`retailPrice`, `semiWholesalePrice`, `wholesalePrice`) and a `PriceList` row per account type with `minQty` / `maxQty` / `active`.  
`src/lib/pricing.ts` `unitPriceFor()` returns the account type's price and throws if the quantity is outside that window. Order forms (`LineItems`) show the price and window live. `PriceTier` is no longer used.  
`AccountMinimum` is the minimum order amount per account type.

### Orders (desk)

Lifecycle used in the UI: `NEW` → `ACCEPTED` / `REJECTED` → `SCHEDULED` → `PARTIAL` / `DELIVERED` → `RECONCILED`.

Quantities can be edited **before** scheduling. Partial delivery updates lines and draft delivery notes.

### Two round types (`DeliveryRound.kind`)

**`DELIVERY` (type 1)** — existing booked work.

1. Orders wait in `NEW` / `ACCEPTED` without a stop.
2. Create a delivery round (date, driver, truck, region).
3. Assign orders as sequenced stops.
4. Update stop status live.
5. Issue delivery notes (one order or whole round).
6. Close / cancel.

**`PROSPECTING` (type 2, “van sale”)** — no orders at start.

1. Create van-sale round.
2. `loadTruck` — take qty from warehouse onto the truck (`RoundLoad`, `StockMove`).
3. Driver proposes products on the road.
4. `recordAtStop` — at a stop: quantities actually sold from the order, extra products taken off the truck (priced by the customer account type, checked against the remaining load), and the money collected. Any part may be left empty.
5. `createFieldOrder` — record a result:
   - `FULL_PAID` — sold, paid in full (`status` RECONCILED, payment created)
   - `PART_PAID` — sold, partial cash (`paidAmount`)
   - `ORDER` — unpaid order (future delivery unless “from truck” is checked)
   - `fromTruck` — goods left the truck (must fit remaining load)
5. `returnTruckStock` — leftover qty back to warehouse, round `CLOSED`.

Do not assign waiting desk orders onto a prospecting round.

### Documents

`CompanySettings` id is always `"company"`.  
Prefixes + sequential counters: `proformaPrefix` / `proformaNext`, `deliveryPrefix` / `deliveryNext`.  
Totals: discount, VAT %, tax stamp, amount in words (`amountInWords`).  
“Send to app” sets `sentToApp` and creates a customer notification (no real mobile app in this repo).

### Payments

Record against customer, optional driver, optional delivery note / order.  
Remittance = cash the driver hands to the company.  
Customer balance = validated delivery notes − payments.

### Notifications

`audience`: `MANAGEMENT` or `CUSTOMER`.  
Triggers already wired: new order, account accept/reject, order status, scheduled delivery, documents, payment, credit limit.

### Driver activity on the card

`src/lib/driver-activity.ts` **derives** a timeline from rounds, stops, field orders, loads, payments, remittances, and staff `ActivityLog` rows. Drivers do not write logs themselves.

`/fleet` must **not** call the heavy include (`loads` / `fieldOrders` on `DeliveryRound`) for every card — that caused a 500 when the Prisma client was stale. The list page only uses `rounds` + `payments` already on the driver.

---

### Stock ledger

Products carry an optional unique `barcode` and a packaging pair (`packaging` SINGLE/CARTON + `unitsPerCarton`). Counter scanners are keyboard-wedge devices, so there is no driver and no device pairing. `src/lib/scanner.ts` (`watchScanner`) holds the whole mechanism and both the product forms (`barcode-field.tsx`) and the point of sale (`pos-terminal.tsx`) use it: it reads a burst whose characters average under 40ms apart and ends in Enter, takes those characters back out of whichever field had the cursor, routes the code, and swallows the Enter so it can never submit the form behind the screen. Ordinary typing (~80ms+ per key) is left alone. Attach the listener once per screen — re-attaching it on every render loses the keystrokes of a scan in progress. A new product is created empty: there is no opening-quantity field. Goods enter the warehouse through a purchase (`/purchases`, received), which writes a PURCHASE move; manual movements stay for corrections (loss, inventory count, customer return, adjustment). OPENING only exists for the seeded history.

All warehouse quantity changes go through `moveStock()` in `src/lib/stock.ts`, which updates `Product.quantity` and writes a `StockMove` with `type`, `reference`, `sourceId` and `balanceAfter`. Types: OPENING, PURCHASE, PURCHASE_CANCEL, SALE, SALE_CANCEL, TRUCK_LOAD, TRUCK_RETURN, CUSTOMER_RETURN, LOSS, INVENTORY, ADJUSTMENT.

- The internal store (`store` department) holds its own shelf stock in `StoreStock`. Moving goods there is a STORE_TRANSFER exit from the warehouse (STORE_RETURN to send them back), so both show in the stock ledger; counter sales (`StoreSale`) only draw down the shelf. Expenses live in `Expense`.
- Purchases add stock when RECEIVED (and set `Product.costPrice` to the unit cost); cancelling a received purchase takes it back out.
- Sales remove stock when a delivery note is VALIDATED; cancelling a validated note puts it back. Van-sale orders (`fromTruck`) do not, because their goods left with the TRUCK_LOAD.
- Never write `product.quantity` directly.

### Reasons for changes (motif)

Every edit or delete of an existing record needs a reason. Forms that change records use `ChangeForm` (`src/components/change-form.tsx`), which asks for the reason in a dialog and posts it as `motif`. Actions call `actor(department, operation, formData)`; for `edit` / `delete` it refuses a missing motif, and `logActivity` stores it in `ActivityLog.motif`.

### Late payments

`src/lib/receivables.ts` applies payments to validated delivery notes oldest-first. The delay counts from the oldest unpaid note; it is late after `PAYMENT_TERM_DAYS` (15).

### Printed documents

Proformas, delivery notes and round sheets always print in English (`makeI18n("en")`, `dir="ltr"` in `src/app/print/layout.tsx`), whatever the UI language.

## 6. Auth and permissions

Cookie: `absher_session` (httpOnly, 14 days).  
`src/lib/auth.ts` — `login`, `requireUser`, `can(user, department, operation)`.  
`src/actions/guard.ts` — `actor()` throws if the signed-in user lacks the operation.

Departments (`src/lib/format.ts` `DEPARTMENTS`):

`users` `activity` `customers` `products` `pricing` `orders` `invoices` `settings` `rounds` `fleet` `payments` `statistics` `analysis` `notifications`

Pages and their departments live in `src/lib/access.ts` (`PAGES`). The sidebar filters that list with `can(..., "view")`, and `src/proxy.ts` refuses the same pages (and their print / export routes) when the URL is typed by hand, redirecting to the first page the role may view.

---

## 7. Language and theme

- Strings: `t("English text")` from `getI18n()` (`src/lib/i18n/server.ts`). Arabic lives in `src/lib/i18n/dictionaries.ts`; English text is the key.
- Stored English text (notifications, log items, stock reasons) is translated on display with `tr()` using the `{placeholder}` keys.
- Cookies: `absher_lang` (en/ar, sets `dir`), `absher_theme` (light/dark, sets `html.dark`).

## 8. UI conventions

- Tokens in `src/app/globals.css`: blue brand gradient (`--brand-from/mid/to`), light and dark palettes (`html.dark`). Status colors (good/warn/bad) are separate.
- Shared: `PageHeader` (optional lucide icon), `Card`, `Stat`, `Badge`, `Field`, `FormDialog`.
- Forms live in popups, not in frames on the page: a create or edit form sits inside `FormDialog` (`src/components/form-dialog.tsx`) and its trigger button goes in the `PageHeader` `actions` (or in the row it belongs to, like editing one user or one role). Pages themselves show data. The popup closes itself once the form has submitted; when the form is a `ChangeForm`, the reason prompt opens on top and the popup waits for it.
- Buttons: `btn btn-primary` | `btn-ghost` | `btn-danger`.
- Tables: `table.data`.
- Sidebar groups: Home, Sales, Field, Finance, Insights, Admin (`src/components/nav.tsx`).
- Maps: OpenStreetMap embed for display (`src/components/map.tsx`); `src/components/location-picker.tsx` (Leaflet) to choose a customer position by clicking, dragging, address search (Nominatim) or the device location.
- Layout: the sidebar shows from `lg` up; below that a top bar opens a drawer (`src/components/mobile-nav.tsx`). Wide tables scroll sideways on phones.
- Buttons and forms are hidden from roles that may not use them via `pageAccess(department)` (`src/lib/page-access.ts`); the actions still check permissions themselves.
- Sidebar rows are 15px text with 19px icons (`src/components/nav.tsx`); the list scrolls with a visible scrollbar when the screen is too short for all the entries. The same component fills the phone drawer.
- Lucide icons can hydrate-mismatch; `SideNav` mounts icons after `useEffect`.

---

## 9. Prisma / Windows pitfall

`npx prisma generate` fails with **EPERM rename query_engine-windows.dll.node** if `next dev` is running — the process locks the DLL.

1. Stop all Node processes for this repo.
2. `npx prisma generate`
3. `npm run dev`

After changing `schema.prisma`: `npx prisma db push` then generate.  
If generate is locked, `/fleet` or round pages may 500 with `Unknown field loads`. Restart Next after a successful generate.

---

## 10. What is implemented vs still rough

Implemented against the 14-module spec (working local demo):

1. Users, roles, permission matrix  
2. Activity log + Excel  
3. Customer accounts + billing + credit  
4. Categories, products, stock log  
5. Pricing per account type with min/max quantities, minima, change log  
6. Orders (desk + field)  
7. Proformas + delivery notes + print + “send to app”  
8. Document settings  
9. Two round types + stop order + truck load/return  
10. Driver cards (no login) + vehicles + location + per-round activity  
11. Payments, remittances, balances, Excel  
12. Statistics + Excel  
13. Analysis charts, segments, late payers (offers were removed)  
14. Notifications  

Not a real mobile app, not wired to the existing Absher admin API, no production deploy, no real PDF engine (browser print), Excel is CSV.

Reasonable next work if asked:

- Connect to the live Absher API instead of SQLite seed  
- Real PDF (company logo/stamp images)  
- True `.xlsx`  
- Driver activity as stored events (not only derived)  
- Offline van-sale flow / photos of remittance slips  
- Tests  

---

## 11. How another AI should work in this repo

1. Read this file and `prisma/schema.prisma` first.  
2. Read `node_modules/next/dist/docs/` before inventing Next APIs.  
3. Keep changes local. Do not add deploy configs unless asked.  
4. Do not give drivers login accounts.  
5. Preserve both round types.  
6. After schema edits: db push, generate (Next stopped), seed if needed.  
7. Verify UI in the browser when you change screens.  
8. User of this repo is **codex**, software engineer — work in code, iterate.

Prompt you can paste into the next tool:

> Continue the Absher operations dashboard at C:\Users\lenovo\Projects\absher-dashboard. Read GUIDE.md and AI-CONTEXT.md first. Local Next.js 16 + Prisma SQLite. Do not deploy. Drivers have no accounts. There are two round types: DELIVERY and PROSPECTING.

---

## 12. Spec module checklist (for design review)

| # | Spec name | Screen |
| --- | --- | --- |
| 01 | Internal users, roles & permissions | `/users` |
| 02 | Activity log | `/activity` |
| 03 | Customer accounts | `/customers` |
| 04 | Categories & products | `/products` |
| 05 | Pricing | `/pricing` |
| 06 | Orders | `/orders` |
| 07 | Invoicing — proforma & delivery notes | `/invoices`, `/delivery-notes` |
| 08 | Document settings | `/settings` |
| 09 | Delivery rounds | `/rounds` (`DELIVERY`) |
| 09b | Van-sale / prospecting rounds | `/rounds` (`PROSPECTING`) — extension beyond the PDF |
| 10 | Delivery agents & vehicles | `/fleet` (named Drivers) |
| 11 | Payments & collections | `/payments` |
| 12 | Statistics | `/statistics` |
| 13 | Data analysis tool | `/analysis` |
| 14 | Notifications | `/notifications` |
