MiknasTrading/CLAUDE.md
Ghassan Yusuf d8cab94bcb feat: supplier modal wizard, pipeline delete, sidebar cleanup
- Replace two-tab supplier selector with two-step wizard (method select → suppliers → summary)
- Add per-item channel picker (Email / WhatsApp / Both) in By Item mode
- Add confirmation summary step before submitting By Item supplier assignments
- Add type-to-confirm delete on pipeline list rows
- Redirect purchase.requests.index to pipeline (same data, single entry point)
- Remove Purchase Requests from sidebar nav
- Add edit-request-modal, supplier-invite-list components
- Add address coordinates migration for settings_locations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:08:58 +03:00

399 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# OperationModule — SteelERP
Manufacturing & Trading ERP. Laravel 12 / PHP 8.2. SQLite. Four modules: Purchase, Inventory, Production, Sales.
---
## Quick Start
```bash
composer install && npm install
cp .env.example .env && php artisan key:generate
php artisan migrate --seed
npm run dev # Vite dev server (keep running)
php artisan serve # http://localhost:8000
```
---
## Tech Stack
| | |
|---|---|
| Framework | Laravel 12, PHP 8.2 |
| Frontend | Tailwind CSS v3 (JIT), Alpine.js v3, Vite 7 |
| Database | SQLite — `database/database.sqlite` |
| Auth | Laravel Breeze (email+password, verified) |
| RBAC | spatie/laravel-permission v6 — middleware: `role`, `permission`, `role_or_permission` |
| PDF | barryvdh/laravel-dompdf v3 |
| Excel | phpoffice/phpspreadsheet v5 |
| Dev tools | Pint, Pail, Sail, PHPUnit 11 |
**Roles (seeded):** Admin, Accounts, Store Manager, Production Manager, Sales Manager
---
## Controllers — `app/Http/Controllers/`
```
Controller.php
DashboardController.php
ProfileController.php
Auth/
AuthenticatedSessionController.php
ConfirmablePasswordController.php
EmailVerificationNotificationController.php
EmailVerificationPromptController.php
NewPasswordController.php
PasswordController.php
PasswordResetLinkController.php
RegisteredUserController.php
VerifyEmailController.php
Purchase/
SupplierController.php import, downloadTemplate, exportPdf + CRUD
PurchaseRequestController.php + approve, reject, print
PurchaseOrderController.php
GoodsReceiptNoteController.php + confirm
SupplierInvoiceController.php
SupplierPaymentController.php
Inventory/
ItemController.php import, downloadTemplate, exportPdf + CRUD
WarehouseController.php
StockMovementController.php
StockReportController.php summary, movement, lowStock, valuation
Production/
ProductionOrderController.php + start, complete
BillOfMaterialController.php
MaterialIssueController.php
ProductionOutputController.php
Sales/
CustomerController.php
SalesOrderController.php + confirm
DeliveryNoteController.php + dispatch
SalesInvoiceController.php
PaymentReceiptController.php
```
---
## Models — `app/Models/`
```
User.php
Supplier.php SupplierInvoice.php SupplierPayment.php
PurchaseRequest.php PurchaseRequestItem.php
PurchaseOrder.php PurchaseOrderItem.php
GoodsReceiptNote.php GrnItem.php
Item.php Warehouse.php StockLevel.php StockMovement.php
ProductionOrder.php BillOfMaterial.php MaterialIssue.php
ProductionOutput.php ProductionCost.php
Customer.php SalesOrder.php SalesOrderItem.php
DeliveryNote.php DeliveryNoteItem.php
SalesInvoice.php PaymentReceipt.php
```
---
## Services — `app/Services/`
| File | Purpose |
|------|---------|
| `SupplierImportService.php` | Excel import — detects MRF vs template format, skips duplicates |
| `ItemImportService.php` | Excel import — detects Forkoll vs template format, skips duplicates |
---
## Artisan Commands — `app/Console/Commands/`
```bash
php artisan suppliers:import "path/to/file.xlsx" [--dry-run]
php artisan suppliers:template [--output=path] # → storage/app/suppliers_template.xlsx
php artisan items:template [--output=path] # → storage/app/items_template.xlsx
```
Files: `ImportSuppliers.php`, `GenerateSupplierTemplate.php`, `GenerateItemTemplate.php`
---
## Routes — `routes/web.php`
All protected by `['auth', 'verified']`. Prefix groups:
### Purchase — `prefix('purchase')->name('purchase.')`
```
GET/POST suppliers purchase.suppliers.*
POST suppliers/import purchase.suppliers.import ← must be BEFORE resource
GET suppliers/template purchase.suppliers.template ← must be BEFORE resource
GET suppliers/export-pdf purchase.suppliers.export-pdf ← must be BEFORE resource
GET/POST requests purchase.requests.*
PATCH requests/{id}/approve purchase.requests.approve
PATCH requests/{id}/reject purchase.requests.reject
GET requests/{id}/print purchase.requests.print
GET/POST orders purchase.orders.*
GET/POST grns purchase.grns.*
PATCH grns/{id}/confirm purchase.grns.confirm
GET/POST invoices purchase.invoices.*
GET/POST payments purchase.payments.*
```
### Inventory — `prefix('inventory')->name('inventory.')`
```
GET/POST items inventory.items.*
POST items/import inventory.items.import ← must be BEFORE resource
GET items/template inventory.items.template ← must be BEFORE resource
GET items/export-pdf inventory.items.export-pdf ← must be BEFORE resource
GET/POST warehouses inventory.warehouses.*
GET/POST movements inventory.movements.*
GET reports/summary inventory.reports.summary
GET reports/movement inventory.reports.movement
GET reports/low-stock inventory.reports.low-stock
GET reports/valuation inventory.reports.valuation
```
### Production — `prefix('production')->name('production.')`
```
GET/POST orders production.orders.*
PATCH orders/{id}/start production.orders.start
PATCH orders/{id}/complete production.orders.complete
GET/POST bom production.bom.*
GET/POST material-issues production.material-issues.*
GET/POST outputs production.outputs.*
```
### Sales — `prefix('sales')->name('sales.')`
```
GET/POST customers sales.customers.*
GET/POST orders sales.orders.*
PATCH orders/{id}/confirm sales.orders.confirm
GET/POST delivery-notes sales.delivery-notes.*
PATCH delivery-notes/{id}/dispatch sales.delivery-notes.dispatch
GET/POST invoices sales.invoices.*
GET/POST payments sales.payments.*
```
---
## Views — `resources/views/`
```
dashboard.blade.php
welcome.blade.php
layouts/
app.blade.php main layout (sidebar + topbar)
navigation.blade.php
guest.blade.php
components/ (Breeze defaults: modal, dropdown, buttons, inputs, etc.)
auth/ (login, register, forgot-password, reset-password, verify-email, confirm-password)
profile/edit.blade.php + partials/
purchase/
suppliers/ index, create, edit, pdf
requests/ index, create, edit, show
orders/ index, create, edit, show
grns/ index, create, show
invoices/ index, create, edit
payments/ index, create
inventory/
items/ index, create, edit, pdf
warehouses/ index, create, edit
movements/ index, create
reports/ summary, movement, low-stock, valuation
production/
orders/ index, create, edit, show
bom/ index, create, edit
material-issues/ index
outputs/ index
sales/
customers/ index, create, edit
orders/ index, create, edit, show
delivery-notes/ index, create, edit
invoices/ index, create, edit
payments/ index, create
```
---
## Database — `database/`
**Driver:** SQLite — `database/database.sqlite`
### Migrations (29 total)
```
users, cache, jobs (Laravel defaults)
permission_tables (Spatie)
suppliers, items, warehouses, stock_levels, stock_movements
purchase_orders, purchase_order_items, goods_receipt_notes, purchase_requests
grn_items, supplier_invoices, supplier_payments
production_orders, bill_of_materials, material_issues
production_outputs, production_costs
customers, sales_orders, sales_order_items, delivery_notes
delivery_note_items, sales_invoices, payment_receipts
purchase_request_items
```
### Seeders
`database/seeders/DatabaseSeeder.php` — creates roles (Admin, Accounts, Store Manager, Production Manager, Sales Manager)
---
## MCP Server — `.claude/mcp-server.py`
Read-only SQLite access. 16 tools:
```
list_tables describe_table(table)
run_query(sql) — SELECT/WITH only, max 200 rows
get_suppliers(active_only) get_purchase_orders(status)
get_purchase_requests(status) get_supplier_invoices(status)
get_items(category,active) get_stock_levels(warehouse_id,low_stock_only)
get_warehouses()
get_production_orders(status) get_bill_of_materials(product_id)
get_customers(active_only) get_sales_orders(status)
get_sales_invoices(status) get_dashboard_summary()
```
**Activation:** run `claude mcp add operation-module-db python -- .claude/mcp-server.py` once, then restart Claude Code. Use these tools before reading PHP files or running artisan for data questions.
---
## Critical Gotchas
### 1. Tailwind JIT — use inline styles for modals/dynamic classes
Vite JIT only compiles classes found in scanned source files at build time. Classes like `max-w-lg`, `max-w-md`, arbitrary colors used only in a modal or added dynamically **will not appear in the compiled CSS**. Always use `style="..."` inline for modal widths, custom colors, and one-off layout values.
### 2. PhpSpreadsheet v5 — `getCellByColumnAndRow()` removed
Use `Coordinate::stringFromColumnIndex($col) . $row` then `$sheet->getCell($coord)` instead:
```php
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
$val = $sheet->getCell(Coordinate::stringFromColumnIndex($col) . $row)->getValue();
```
### 3. Custom routes BEFORE Route::resource()
`Route::resource('suppliers', ...)` registers `{supplier}` wildcard that captures `import`, `template`, `export-pdf`. Custom routes MUST be declared first:
```php
Route::post('suppliers/import', ...); // first
Route::get('suppliers/template', ...); // first
Route::get('suppliers/export-pdf', ...); // first
Route::resource('suppliers', ...); // last
```
### 4. MRFMI supplier import — contact fields are always empty
The MRFMI price-comparison Excel file only contains supplier names in row 4 as column headers. There is no email, phone, or contact data anywhere in the file. This is expected — not a bug.
### 5. Forkoll import — "DESCRIPTION" header row
The Forkoll inventory file has a header row (col A = SI.NO, col B = DESCRIPTION). `ItemImportService` skips col B values in `$headerWords = ['description', 'item name', 'item_name', 'material']` to avoid importing headers as items.
### 6. Search bars — always client-side, never page-refreshing
All search/filter inputs MUST filter records instantly using JavaScript. No `?search=` URL params, no form submissions, no `debounce` + fetch, no page reloads of any kind.
**The pattern to follow (used on the Suppliers index):**
```javascript
var searchInput = document.getElementById('my-search');
var rows = document.querySelectorAll('.data-row');
var countEl = document.getElementById('search-count');
var total = rows.length;
searchInput.addEventListener('input', function() {
var q = this.value.trim().toLowerCase();
var visible = 0;
rows.forEach(function(row) {
var match = !q || row.textContent.toLowerCase().indexOf(q) !== -1;
row.classList.toggle('hidden-by-search', !match);
if (match) visible++;
});
countEl.textContent = q ? (visible + ' of ' + total) : total;
});
```
- All records are loaded on page load — the server query returns everything, JS does the filtering
- Add `.hidden-by-search { display:none; }` to the page `<style>` block
- Show a live count ("12 of 47 suppliers") next to the search input
- Show a "No results" message when `visible === 0 && q !== ''`
- Never add `?search=` query params or submit a form for search
### 7. Never use `alert()`, `confirm()`, or `prompt()` — use toasts and modals
These native browser dialogs are ugly, block the thread, and cannot be styled. They are **banned** across the entire project.
- Use `showToast('msg', 'type')` for notifications instead of `alert()`
- Use a custom modal (like the delete confirmation modal) instead of `confirm()`
- Use a styled modal with an `<input>` instead of `prompt()`
### 8. Route parameter names must never be `{request}`
Laravel injects `Illuminate\Http\Request` by type-hint into `$request`. If a route parameter is also named `{request}`, implicit model binding silently fails — the model parameter receives null/empty, causing NOT NULL violations or UrlGenerationException when generating URLs from that model.
**Rule:** Always name route parameters after the model, matching the controller variable name exactly:
```php
// WRONG — {request} clashes with Request $request injection
Route::post('requests/{request}/sign', [PurchaseSignatureController::class, 'store']);
// store(Request $request, PurchaseRequest $purchaseRequest) — $purchaseRequest gets null
// CORRECT
Route::post('requests/{purchaseRequest}/sign', [PurchaseSignatureController::class, 'store']);
// store(Request $request, PurchaseRequest $purchaseRequest) — binding works
```
**Route::resource also generates `{request}` for a resource named `requests`.** Always override it:
```php
// WRONG
Route::resource('requests', PurchaseRequestController::class);
// Generates {request} — clashes with $request injection in every method
// CORRECT
Route::resource('requests', PurchaseRequestController::class)->parameters(['requests' => 'purchaseRequest']);
// Generates {purchaseRequest} — matches $purchaseRequest in controller methods
```
Use `{purchaseRequest}`, `{purchaseOrder}`, `{supplier}`, etc. — never `{request}`.
### 9. Status notifications — always use toasts, never inline banners
All success/error/info/warning messages MUST be displayed as toasts, not as `<div>` banners inside the page content. The global toast system is wired into `layouts/app.blade.php` and fires automatically from Laravel session flash keys (`success`, `error`, `info`, `warning`). In controllers, use:
```php
return redirect()->route('...')->with('success', 'Done.');
return redirect()->route('...')->with('error', 'Something failed.');
```
To trigger a toast from JavaScript (e.g. after an in-page action), call:
```javascript
showToast('Message text', 'success'); // types: success | error | info | warn
```
**Never** add inline `@if(session('success'))` banner divs to individual views — the layout handles all of them. The toast appears bottom-right, auto-dismisses after 4 s, has a shrinking progress bar, and can be clicked or ×-closed early.
### 10. Purchase Request creation — always use `<x-purchase.request-modal />`
The create form lives in `resources/views/components/purchase/request-modal.blade.php` as a reusable Blade component. Wherever a "New Purchase Request" trigger is needed, drop in the component tag — it renders the button and the full modal itself:
```blade
<x-purchase.request-modal />
```
- **Never** link to `route('purchase.requests.create')` for creating new requests — the component replaces that flow entirely.
- The component is self-contained: it owns the trigger button, the Alpine.js open/close state, the full MPR form (POSTing to `purchase.requests.store`), dynamic item rows, and validation-error auto-reopen logic.
- The `/purchase/requests/create` page and route remain as a fallback but should not be referenced in new UI.
### 11. Data entry pages — AJAX only, no page refreshes
All settings and management pages where users create, edit, or delete records MUST use `fetch()` AJAX. No `<form>` submissions, no page reloads, no redirects after data entry.
**Controller:** return `response()->json(...)` for all create/update/delete endpoints. Laravel auto-returns 422 JSON on validation failure when `Accept: application/json` is set.
**Frontend fetch helper pattern:**
```javascript
var CSRF = document.querySelector('meta[name="csrf-token"]').content;
function api(url, method, data) {
var opts = { method: method, headers: { 'X-CSRF-TOKEN': CSRF, 'Accept': 'application/json', 'Content-Type': 'application/json' } };
if (data) opts.body = JSON.stringify(data);
return fetch(url, opts).then(function(r) {
return r.json().then(function(body) {
if (!r.ok) return Promise.reject(body);
return body;
});
});
}
```
**After each operation:**
- On success: update the DOM in-place (append row, update text, remove element), then call `showToast('Done.', 'success')`
- On error: call `showToast(err.message || 'Error', 'error')`
- For deletes: use `confirmAction(title, body, onConfirm)` (not `confirm()`) before calling the API
**Never** use `<form method="POST">` for inline data entry on settings/management pages. `<form>` submissions that navigate away from the page are banned for these flows.