Asset category masters removed and items category mapped to assets
This commit is contained in:
parent
917917ad29
commit
4bab5f8859
@ -78,7 +78,6 @@ erp-backend/
|
|||||||
│ │ │ ├── warehouses/
|
│ │ │ ├── warehouses/
|
||||||
│ │ │ ├── payment-terms/
|
│ │ │ ├── payment-terms/
|
||||||
│ │ │ ├── delivery-terms/
|
│ │ │ ├── delivery-terms/
|
||||||
│ │ │ ├── asset-categories/
|
|
||||||
│ │ │ ├── departments/
|
│ │ │ ├── departments/
|
||||||
│ │ │ ├── designations/
|
│ │ │ ├── designations/
|
||||||
│ │ │ ├── plants/
|
│ │ │ ├── plants/
|
||||||
@ -1180,7 +1179,7 @@ module.exports = router;
|
|||||||
|
|
||||||
### 9.4 Masters module pattern
|
### 9.4 Masters module pattern
|
||||||
|
|
||||||
Every master (UOM, Item Categories, Brands, GST Rates, Warehouses, Payment Terms, Delivery Terms, Asset Categories, Departments, Designations, Plants, etc.) follows the **same four-file pattern as Vendors**, scaled down — simple `name`/`code` fields, no document-number generation, RBAC permission code `MASTERS`. Build one master fully (UOM is simplest) as the template, then replicate for the rest.
|
Every master (UOM, Item Categories, Brands, GST Rates, Warehouses, Payment Terms, Delivery Terms, Departments, Designations, Plants, etc.) follows the **same four-file pattern as Vendors**, scaled down — simple `name`/`code` fields, no document-number generation, RBAC permission code `MASTERS`. Build one master fully (UOM is simplest) as the template, then replicate for the rest. Assets use the same item category / subcategory masters (no separate asset category APIs).
|
||||||
|
|
||||||
`src/modules/masters/index.js` aggregates all sub-routers:
|
`src/modules/masters/index.js` aggregates all sub-routers:
|
||||||
|
|
||||||
@ -1197,7 +1196,6 @@ router.use('/gst-rates', require('./gst-rates/gst-rates.routes'));
|
|||||||
router.use('/warehouses', require('./warehouses/warehouses.routes'));
|
router.use('/warehouses', require('./warehouses/warehouses.routes'));
|
||||||
router.use('/payment-terms', require('./payment-terms/payment-terms.routes'));
|
router.use('/payment-terms', require('./payment-terms/payment-terms.routes'));
|
||||||
router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes'));
|
router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes'));
|
||||||
router.use('/asset-categories', require('./asset-categories/asset-categories.routes'));
|
|
||||||
router.use('/departments', require('./departments/departments.routes'));
|
router.use('/departments', require('./departments/departments.routes'));
|
||||||
router.use('/designations', require('./designations/designations.routes'));
|
router.use('/designations', require('./designations/designations.routes'));
|
||||||
router.use('/plants', require('./plants/plants.routes'));
|
router.use('/plants', require('./plants/plants.routes'));
|
||||||
@ -1276,7 +1274,6 @@ BigInt.prototype.toJSON = function () {
|
|||||||
/api/v1/masters/warehouses
|
/api/v1/masters/warehouses
|
||||||
/api/v1/masters/payment-terms
|
/api/v1/masters/payment-terms
|
||||||
/api/v1/masters/delivery-terms
|
/api/v1/masters/delivery-terms
|
||||||
/api/v1/masters/asset-categories
|
|
||||||
/api/v1/masters/departments
|
/api/v1/masters/departments
|
||||||
/api/v1/masters/designations
|
/api/v1/masters/designations
|
||||||
/api/v1/masters/plants
|
/api/v1/masters/plants
|
||||||
|
|||||||
@ -162,15 +162,15 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
|||||||
| Status | Sub-master | Base path |
|
| Status | Sub-master | Base path |
|
||||||
|--------|------------|-----------|
|
|--------|------------|-----------|
|
||||||
| [x] | UOM | `/masters/uom` |
|
| [x] | UOM | `/masters/uom` |
|
||||||
| [x] | Item Categories | `/masters/item-categories` |
|
| [x] | Item Categories | `/masters/item-categories` | Optional asset defaults: `code_prefix`, `default_useful_life_years`, `default_depreciation_method` |
|
||||||
| [x] | Item Subcategories | `/masters/item-subcategories` |
|
| [x] | Item Subcategories | `/masters/item-subcategories` | Filter: `item_category_id` — shared by items + assets |
|
||||||
| [x] | Items | `/masters/items` |
|
| [x] | Items | `/masters/items` |
|
||||||
| [x] | Brands | `/masters/brands` |
|
| [x] | Brands | `/masters/brands` |
|
||||||
| [x] | GST Rates | `/masters/gst-rates` |
|
| [x] | GST Rates | `/masters/gst-rates` |
|
||||||
| [x] | Payment Terms | `/masters/payment-terms` |
|
| [x] | Payment Terms | `/masters/payment-terms` |
|
||||||
| [x] | Delivery Terms | `/masters/delivery-terms` |
|
| [x] | Delivery Terms | `/masters/delivery-terms` |
|
||||||
| [x] | Asset Categories | `/masters/asset-categories` |
|
| [~] | Asset Categories | removed — use Item Categories |
|
||||||
| [x] | Asset Subcategories | `/masters/asset-subcategories` | Filter: `asset_category_id` |
|
| [~] | Asset Subcategories | removed — use Item Subcategories |
|
||||||
| [x] | Departments | `/masters/departments` |
|
| [x] | Departments | `/masters/departments` |
|
||||||
| [x] | Designations | `/masters/designations` |
|
| [x] | Designations | `/masters/designations` |
|
||||||
| [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`) |
|
| [x] | Locations | `/masters/locations` | Unified plants + warehouses (`type`: `plant` \| `warehouse`) |
|
||||||
@ -178,7 +178,7 @@ Each sub-master supports: `GET /` (list), `GET /:id`, `POST /`, `PUT /:id`, `DEL
|
|||||||
| [x] | Warehouses | `/masters/warehouses` | Alias — warehouse locations only |
|
| [x] | Warehouses | `/masters/warehouses` | Alias — warehouse locations only |
|
||||||
| [x] | Document Series | `/masters/document-series` |
|
| [x] | Document Series | `/masters/document-series` |
|
||||||
|
|
||||||
**Masters total:** 14 modules × 5 endpoints = **70 APIs** [x]
|
**Masters total:** 12 modules × 5 endpoints (+ plants/warehouses aliases) — asset category masters retired; assets use item categories
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -210,27 +210,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.diagram-viewport {
|
.diagram-viewport {
|
||||||
overflow: auto;
|
position: relative;
|
||||||
max-height: 70vh;
|
overflow: hidden;
|
||||||
padding: 1rem;
|
height: min(70vh, 560px);
|
||||||
cursor: grab;
|
min-height: 280px;
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diagram-viewport.can-pan { cursor: grab; }
|
||||||
.diagram-viewport.is-panning { cursor: grabbing; }
|
.diagram-viewport.is-panning { cursor: grabbing; }
|
||||||
|
|
||||||
|
.diagram-stage {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.diagram-canvas {
|
.diagram-canvas {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 100%;
|
|
||||||
transform-origin: 0 0;
|
transform-origin: 0 0;
|
||||||
transition: transform 0.12s ease-out;
|
will-change: transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diagram-canvas .mermaid,
|
.diagram-canvas .mermaid,
|
||||||
.diagram-canvas svg {
|
.diagram-canvas svg {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 0 auto;
|
margin: 0;
|
||||||
max-width: none !important;
|
max-width: none !important;
|
||||||
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.diagram-fullscreen {
|
.diagram-fullscreen {
|
||||||
@ -302,25 +311,36 @@
|
|||||||
|
|
||||||
.diagram-fullscreen-viewport {
|
.diagram-fullscreen-viewport {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
position: relative;
|
||||||
padding: 2rem;
|
overflow: hidden;
|
||||||
cursor: grab;
|
background: #0f172a;
|
||||||
display: flex;
|
touch-action: none;
|
||||||
align-items: flex-start;
|
user-select: none;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diagram-fullscreen-viewport.can-pan { cursor: grab; }
|
||||||
.diagram-fullscreen-viewport.is-panning { cursor: grabbing; }
|
.diagram-fullscreen-viewport.is-panning { cursor: grabbing; }
|
||||||
|
|
||||||
|
.diagram-fullscreen-stage {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.diagram-fullscreen-canvas {
|
.diagram-fullscreen-canvas {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
transform-origin: center center;
|
transform-origin: 0 0;
|
||||||
|
will-change: transform;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.35);
|
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diagram-fullscreen-canvas svg {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.diagram-fullscreen-hint {
|
.diagram-fullscreen-hint {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 1rem;
|
bottom: 1rem;
|
||||||
@ -357,7 +377,9 @@
|
|||||||
main { margin-left: 0; max-width: 100%; }
|
main { margin-left: 0; max-width: 100%; }
|
||||||
.diagram { break-inside: avoid; }
|
.diagram { break-inside: avoid; }
|
||||||
.diagram-toolbar { display: none; }
|
.diagram-toolbar { display: none; }
|
||||||
.diagram-viewport { max-height: none; overflow: visible; }
|
.diagram-viewport { height: auto; min-height: 0; overflow: visible; }
|
||||||
|
.diagram-stage { position: static; overflow: visible; }
|
||||||
|
.diagram-canvas { transform: none !important; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@ -462,7 +484,7 @@ flowchart LR
|
|||||||
M2[item-categories / subcategories / items]
|
M2[item-categories / subcategories / items]
|
||||||
M3[brands / gst-rates]
|
M3[brands / gst-rates]
|
||||||
M4[payment-terms / delivery-terms]
|
M4[payment-terms / delivery-terms]
|
||||||
M5[asset-categories / subcategories]
|
M5[item-categories / subcategories]
|
||||||
M6[departments / designations]
|
M6[departments / designations]
|
||||||
M7[locations / plants / warehouses]
|
M7[locations / plants / warehouses]
|
||||||
M8[document-series]
|
M8[document-series]
|
||||||
@ -565,7 +587,10 @@ flowchart TD
|
|||||||
PT --> PO
|
PT --> PO
|
||||||
DT[delivery_terms] --> PO
|
DT[delivery_terms] --> PO
|
||||||
|
|
||||||
ACAT[asset_categories] --> ASUB[asset_subcategories]
|
ICAT[item_categories] --> ISUB[item_subcategories]
|
||||||
|
ISUB --> ITEMS[items]
|
||||||
|
ICAT --> ASSETS[assets]
|
||||||
|
ISUB --> ASSETS
|
||||||
ACAT --> ASSET
|
ACAT --> ASSET
|
||||||
ACAT --> GRN_AUTO[GRN auto-asset creation]
|
ACAT --> GRN_AUTO[GRN auto-asset creation]
|
||||||
ASUB --> ASSET
|
ASUB --> ASSET
|
||||||
@ -646,12 +671,12 @@ stateDiagram-v2
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr><td><code>asset_code</code></td><td><code>document_series</code> via <code>ASSET_{category.code}</code></td></tr>
|
<tr><td><code>asset_code</code></td><td><code>document_series</code> via <code>ASSET_{category.code}</code></td></tr>
|
||||||
<tr><td><code>asset_name</code></td><td>Item name (+ <code>#N</code> if qty > 1)</td></tr>
|
<tr><td><code>asset_name</code></td><td>Item name (+ <code>#N</code> if qty > 1)</td></tr>
|
||||||
<tr><td><code>asset_category_id</code> / <code>asset_subcategory_id</code></td><td>From GRN line payload</td></tr>
|
<tr><td><code>item_category_id</code> / <code>item_subcategory_id</code></td><td>From item master (optional GRN line override)</td></tr>
|
||||||
<tr><td><code>plant_id</code></td><td>From PO</td></tr>
|
<tr><td><code>plant_id</code></td><td>From PO</td></tr>
|
||||||
<tr><td><code>warehouse_id</code>, <code>vendor_id</code>, <code>po_id</code>, <code>grn_id</code>, <code>grn_item_id</code></td><td>From GRN</td></tr>
|
<tr><td><code>warehouse_id</code>, <code>vendor_id</code>, <code>po_id</code>, <code>grn_id</code>, <code>grn_item_id</code></td><td>From GRN</td></tr>
|
||||||
<tr><td><code>purchase_date</code></td><td><code>grn.grn_date</code></td></tr>
|
<tr><td><code>purchase_date</code></td><td><code>grn.grn_date</code></td></tr>
|
||||||
<tr><td><code>purchase_cost</code></td><td><code>grn_items.rate</code></td></tr>
|
<tr><td><code>purchase_cost</code></td><td><code>grn_items.rate</code></td></tr>
|
||||||
<tr><td><code>useful_life_years</code>, <code>depreciation_method</code></td><td>From <code>asset_categories</code> defaults</td></tr>
|
<tr><td><code>useful_life_years</code>, <code>depreciation_method</code></td><td>From <code>item_categories</code> defaults</td></tr>
|
||||||
<tr><td><code>condition</code></td><td><code>NEW</code></td></tr>
|
<tr><td><code>condition</code></td><td><code>NEW</code></td></tr>
|
||||||
<tr><td><code>status</code></td><td><code>IN_USE</code></td></tr>
|
<tr><td><code>status</code></td><td><code>IN_USE</code></td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@ -667,7 +692,7 @@ flowchart TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph Validate["normalizeAssetPayload validations"]
|
subgraph Validate["normalizeAssetPayload validations"]
|
||||||
V1[asset_category + subcategory match]
|
V1[item_category + subcategory match]
|
||||||
V2[plant / dept / warehouse / user refs]
|
V2[plant / dept / warehouse / user refs]
|
||||||
V3[vendor / PO / GRN / grn_item refs]
|
V3[vendor / PO / GRN / grn_item refs]
|
||||||
V4[disposal_date required if DISPOSED/SCRAPPED]
|
V4[disposal_date required if DISPOSED/SCRAPPED]
|
||||||
@ -801,7 +826,7 @@ flowchart TD
|
|||||||
<pre class="mermaid">
|
<pre class="mermaid">
|
||||||
flowchart TD
|
flowchart TD
|
||||||
subgraph Inputs
|
subgraph Inputs
|
||||||
CAT[asset_categories defaults]
|
CAT[item_categories defaults]
|
||||||
FORM[User form fields]
|
FORM[User form fields]
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -951,9 +976,11 @@ erDiagram
|
|||||||
items ||--o{ purchase_order_items : ordered
|
items ||--o{ purchase_order_items : ordered
|
||||||
items ||--o{ grn_items : received
|
items ||--o{ grn_items : received
|
||||||
|
|
||||||
asset_categories ||--o{ asset_subcategories : has
|
item_categories ||--o{ item_subcategories : has
|
||||||
asset_categories ||--o{ assets : classifies
|
item_categories ||--o{ items : classifies
|
||||||
asset_subcategories ||--o{ assets : sub_classifies
|
item_categories ||--o{ assets : classifies
|
||||||
|
item_subcategories ||--o{ items : sub_classifies
|
||||||
|
item_subcategories ||--o{ assets : sub_classifies
|
||||||
|
|
||||||
assets ||--o{ asset_amc_contracts : has
|
assets ||--o{ asset_amc_contracts : has
|
||||||
assets ||--o{ asset_insurance_policies : insured_by
|
assets ||--o{ asset_insurance_policies : insured_by
|
||||||
@ -1039,15 +1066,17 @@ erDiagram
|
|||||||
<button type="button" class="diagram-fullscreen-close" id="diagram-fullscreen-close" title="Exit fullscreen (Esc)">×</button>
|
<button type="button" class="diagram-fullscreen-close" id="diagram-fullscreen-close" title="Exit fullscreen (Esc)">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="diagram-fullscreen-viewport" id="diagram-fullscreen-viewport">
|
<div class="diagram-fullscreen-viewport" id="diagram-fullscreen-viewport">
|
||||||
<div class="diagram-fullscreen-canvas" id="diagram-fullscreen-canvas"></div>
|
<div class="diagram-fullscreen-stage" id="diagram-fullscreen-stage">
|
||||||
|
<div class="diagram-fullscreen-canvas" id="diagram-fullscreen-canvas"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="diagram-fullscreen-hint">Fullscreen: scroll to zoom · drag to pan · + / − / 0 keys · Esc to exit</div>
|
<div class="diagram-fullscreen-hint">Drag to move · Scroll or Ctrl+scroll to zoom · + / − / 0 keys · Esc to exit</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const ZOOM_MIN = 0.25;
|
const ZOOM_MIN = 0.2;
|
||||||
const ZOOM_MAX = 4;
|
const ZOOM_MAX = 5;
|
||||||
const ZOOM_STEP = 0.2;
|
const ZOOM_STEP = 0.15;
|
||||||
const ZOOM_DEFAULT = 1;
|
const ZOOM_DEFAULT = 1;
|
||||||
|
|
||||||
function clampZoom(value) {
|
function clampZoom(value) {
|
||||||
@ -1061,7 +1090,8 @@ erDiagram
|
|||||||
<button type="button" data-action="zoom-out" title="Zoom out (-)">−</button>
|
<button type="button" data-action="zoom-out" title="Zoom out (-)">−</button>
|
||||||
<span class="diagram-zoom-label" data-zoom-label>100%</span>
|
<span class="diagram-zoom-label" data-zoom-label>100%</span>
|
||||||
<button type="button" data-action="zoom-in" title="Zoom in (+)">+</button>
|
<button type="button" data-action="zoom-in" title="Zoom in (+)">+</button>
|
||||||
<button type="button" data-action="zoom-reset" title="Reset zoom (0)">Reset</button>
|
<button type="button" data-action="zoom-reset" title="Reset view (0)">Reset</button>
|
||||||
|
<button type="button" data-action="zoom-fit" title="Fit to view">Fit</button>
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button type="button" data-action="fullscreen" title="Fullscreen (F)">⛶ Fullscreen</button>
|
<button type="button" data-action="fullscreen" title="Fullscreen (F)">⛶ Fullscreen</button>
|
||||||
`;
|
`;
|
||||||
@ -1072,100 +1102,204 @@ erDiagram
|
|||||||
return toolbar;
|
return toolbar;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyZoom(state) {
|
class DiagramViewer {
|
||||||
state.canvas.style.transform = `scale(${state.zoom})`;
|
constructor({ viewport, stage, canvas, label, wheelNeedsModifier = true }) {
|
||||||
state.label.textContent = `${Math.round(state.zoom * 100)}%`;
|
this.viewport = viewport;
|
||||||
|
this.stage = stage;
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.label = label;
|
||||||
|
this.wheelNeedsModifier = wheelNeedsModifier;
|
||||||
|
this.zoom = ZOOM_DEFAULT;
|
||||||
|
this.panX = 0;
|
||||||
|
this.panY = 0;
|
||||||
|
this.isPanning = false;
|
||||||
|
this.pointerId = null;
|
||||||
|
this.lastPointer = { x: 0, y: 0 };
|
||||||
|
this.bindEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.canvas.style.transform = `translate(${this.panX}px, ${this.panY}px) scale(${this.zoom})`;
|
||||||
|
if (this.label) {
|
||||||
|
this.label.textContent = `${Math.round(this.zoom * 100)}%`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getViewportPoint(clientX, clientY) {
|
||||||
|
const rect = this.stage.getBoundingClientRect();
|
||||||
|
return { x: clientX - rect.left, y: clientY - rect.top };
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomAt(clientX, clientY, delta) {
|
||||||
|
const point = this.getViewportPoint(clientX, clientY);
|
||||||
|
const prevZoom = this.zoom;
|
||||||
|
const nextZoom = clampZoom(prevZoom + delta);
|
||||||
|
if (nextZoom === prevZoom) return;
|
||||||
|
|
||||||
|
this.panX = point.x - ((point.x - this.panX) * nextZoom) / prevZoom;
|
||||||
|
this.panY = point.y - ((point.y - this.panY) * nextZoom) / prevZoom;
|
||||||
|
this.zoom = nextZoom;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
zoomBy(delta, centerToStage = true) {
|
||||||
|
const rect = this.stage.getBoundingClientRect();
|
||||||
|
const x = centerToStage ? rect.left + rect.width / 2 : rect.left;
|
||||||
|
const y = centerToStage ? rect.top + rect.height / 2 : rect.top;
|
||||||
|
this.zoomAt(x, y, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.zoom = ZOOM_DEFAULT;
|
||||||
|
this.panX = 0;
|
||||||
|
this.panY = 0;
|
||||||
|
this.render();
|
||||||
|
this.centerContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
fitToView() {
|
||||||
|
const svg = this.canvas.querySelector('svg');
|
||||||
|
if (!svg) {
|
||||||
|
this.reset();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stageRect = this.stage.getBoundingClientRect();
|
||||||
|
const bbox = svg.getBBox();
|
||||||
|
const pad = 24;
|
||||||
|
const scaleX = (stageRect.width - pad * 2) / bbox.width;
|
||||||
|
const scaleY = (stageRect.height - pad * 2) / bbox.height;
|
||||||
|
const fitZoom = clampZoom(Math.min(scaleX, scaleY, 1));
|
||||||
|
|
||||||
|
this.zoom = fitZoom;
|
||||||
|
this.panX = (stageRect.width - bbox.width * fitZoom) / 2 - bbox.x * fitZoom;
|
||||||
|
this.panY = (stageRect.height - bbox.height * fitZoom) / 2 - bbox.y * fitZoom;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
centerContent() {
|
||||||
|
const svg = this.canvas.querySelector('svg');
|
||||||
|
if (!svg) return;
|
||||||
|
|
||||||
|
const stageRect = this.stage.getBoundingClientRect();
|
||||||
|
const bbox = svg.getBBox();
|
||||||
|
this.panX = (stageRect.width - bbox.width * this.zoom) / 2 - bbox.x * this.zoom;
|
||||||
|
this.panY = (stageRect.height - bbox.height * this.zoom) / 2 - bbox.y * this.zoom;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleAction(action) {
|
||||||
|
switch (action) {
|
||||||
|
case 'zoom-in':
|
||||||
|
this.zoomBy(ZOOM_STEP);
|
||||||
|
break;
|
||||||
|
case 'zoom-out':
|
||||||
|
this.zoomBy(-ZOOM_STEP);
|
||||||
|
break;
|
||||||
|
case 'zoom-reset':
|
||||||
|
this.reset();
|
||||||
|
break;
|
||||||
|
case 'zoom-fit':
|
||||||
|
this.fitToView();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bindEvents() {
|
||||||
|
this.viewport.classList.add('can-pan');
|
||||||
|
|
||||||
|
this.viewport.addEventListener('pointerdown', (e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
if (e.target.closest('button')) return;
|
||||||
|
this.isPanning = true;
|
||||||
|
this.pointerId = e.pointerId;
|
||||||
|
this.lastPointer = { x: e.clientX, y: e.clientY };
|
||||||
|
this.viewport.classList.add('is-panning');
|
||||||
|
this.viewport.setPointerCapture(e.pointerId);
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.viewport.addEventListener('pointermove', (e) => {
|
||||||
|
if (!this.isPanning || e.pointerId !== this.pointerId) return;
|
||||||
|
this.panX += e.clientX - this.lastPointer.x;
|
||||||
|
this.panY += e.clientY - this.lastPointer.y;
|
||||||
|
this.lastPointer = { x: e.clientX, y: e.clientY };
|
||||||
|
this.render();
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
const endPan = (e) => {
|
||||||
|
if (!this.isPanning || (e.pointerId !== undefined && e.pointerId !== this.pointerId)) return;
|
||||||
|
this.isPanning = false;
|
||||||
|
this.pointerId = null;
|
||||||
|
this.viewport.classList.remove('is-panning');
|
||||||
|
if (this.viewport.hasPointerCapture(e.pointerId)) {
|
||||||
|
this.viewport.releasePointerCapture(e.pointerId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.viewport.addEventListener('pointerup', endPan);
|
||||||
|
this.viewport.addEventListener('pointercancel', endPan);
|
||||||
|
this.viewport.addEventListener('lostpointercapture', () => {
|
||||||
|
this.isPanning = false;
|
||||||
|
this.pointerId = null;
|
||||||
|
this.viewport.classList.remove('is-panning');
|
||||||
|
});
|
||||||
|
|
||||||
|
this.viewport.addEventListener(
|
||||||
|
'wheel',
|
||||||
|
(e) => {
|
||||||
|
if (this.wheelNeedsModifier && !e.ctrlKey && !e.metaKey) return;
|
||||||
|
e.preventDefault();
|
||||||
|
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
|
||||||
|
this.zoomAt(e.clientX, e.clientY, delta);
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupPan(viewport, getZoom) {
|
const diagramViewers = [];
|
||||||
let panning = false;
|
let fullscreenViewer = null;
|
||||||
let startX = 0;
|
let fullscreenToolbarReady = false;
|
||||||
let startY = 0;
|
|
||||||
let scrollLeft = 0;
|
|
||||||
let scrollTop = 0;
|
|
||||||
|
|
||||||
viewport.addEventListener('mousedown', (e) => {
|
function openFullscreen(sourceViewer) {
|
||||||
if (e.button !== 0 || getZoom() <= 1) return;
|
|
||||||
if (e.target.closest('button')) return;
|
|
||||||
panning = true;
|
|
||||||
viewport.classList.add('is-panning');
|
|
||||||
startX = e.clientX;
|
|
||||||
startY = e.clientY;
|
|
||||||
scrollLeft = viewport.scrollLeft;
|
|
||||||
scrollTop = viewport.scrollTop;
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', (e) => {
|
|
||||||
if (!panning) return;
|
|
||||||
viewport.scrollLeft = scrollLeft - (e.clientX - startX);
|
|
||||||
viewport.scrollTop = scrollTop - (e.clientY - startY);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('mouseup', () => {
|
|
||||||
if (!panning) return;
|
|
||||||
panning = false;
|
|
||||||
viewport.classList.remove('is-panning');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupWheelZoom(viewport, state, applyFn, requireModifier) {
|
|
||||||
viewport.addEventListener('wheel', (e) => {
|
|
||||||
if (requireModifier && !e.ctrlKey && !e.metaKey) return;
|
|
||||||
e.preventDefault();
|
|
||||||
const delta = e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP;
|
|
||||||
state.zoom = clampZoom(state.zoom + delta);
|
|
||||||
applyFn(state);
|
|
||||||
}, { passive: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
const diagramStates = [];
|
|
||||||
let activeFullscreenState = null;
|
|
||||||
|
|
||||||
function openFullscreen(diagramState) {
|
|
||||||
const overlay = document.getElementById('diagram-fullscreen');
|
const overlay = document.getElementById('diagram-fullscreen');
|
||||||
const canvas = document.getElementById('diagram-fullscreen-canvas');
|
|
||||||
const viewport = document.getElementById('diagram-fullscreen-viewport');
|
const viewport = document.getElementById('diagram-fullscreen-viewport');
|
||||||
|
const stage = document.getElementById('diagram-fullscreen-stage');
|
||||||
|
const canvas = document.getElementById('diagram-fullscreen-canvas');
|
||||||
const title = document.getElementById('diagram-fullscreen-title');
|
const title = document.getElementById('diagram-fullscreen-title');
|
||||||
const toolbarHost = document.getElementById('diagram-fullscreen-toolbar');
|
const toolbarHost = document.getElementById('diagram-fullscreen-toolbar');
|
||||||
|
|
||||||
title.textContent = diagramState.title;
|
title.textContent = sourceViewer.title || 'Diagram';
|
||||||
canvas.innerHTML = '';
|
canvas.innerHTML = sourceViewer.canvas.innerHTML;
|
||||||
const clone = diagramState.canvas.cloneNode(true);
|
|
||||||
clone.style.transform = '';
|
|
||||||
canvas.appendChild(clone);
|
|
||||||
|
|
||||||
const fsState = {
|
if (!fullscreenToolbarReady) {
|
||||||
zoom: diagramState.zoom,
|
const toolbar = createToolbar((action) => {
|
||||||
canvas: clone,
|
if (action === 'fullscreen') return;
|
||||||
label: toolbarHost.querySelector('[data-zoom-label]'),
|
if (fullscreenViewer) fullscreenViewer.handleAction(action);
|
||||||
viewport,
|
});
|
||||||
source: diagramState,
|
toolbarHost.appendChild(toolbar);
|
||||||
};
|
fullscreenToolbarReady = true;
|
||||||
|
|
||||||
if (!toolbarHost.dataset.ready) {
|
|
||||||
const fsToolbar = createToolbar((action) => handleDiagramAction(fsState, action, true));
|
|
||||||
toolbarHost.innerHTML = '';
|
|
||||||
toolbarHost.appendChild(fsToolbar);
|
|
||||||
toolbarHost.dataset.ready = '1';
|
|
||||||
setupPan(viewport, () => fsState.zoom);
|
|
||||||
setupWheelZoom(viewport, fsState, (s) => {
|
|
||||||
applyZoom(s);
|
|
||||||
if (s.source) {
|
|
||||||
s.source.zoom = s.zoom;
|
|
||||||
applyZoom(s.source);
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fsState.label = toolbarHost.querySelector('[data-zoom-label]');
|
if (!fullscreenViewer) {
|
||||||
applyZoom(fsState);
|
fullscreenViewer = new DiagramViewer({
|
||||||
activeFullscreenState = fsState;
|
viewport,
|
||||||
|
stage,
|
||||||
|
canvas,
|
||||||
|
label: toolbarHost.querySelector('[data-zoom-label]'),
|
||||||
|
wheelNeedsModifier: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fullscreenViewer.fitToView();
|
||||||
|
|
||||||
overlay.classList.add('is-open');
|
overlay.classList.add('is-open');
|
||||||
overlay.setAttribute('aria-hidden', 'false');
|
overlay.setAttribute('aria-hidden', 'false');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
viewport.scrollTop = 0;
|
|
||||||
viewport.scrollLeft = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeFullscreen() {
|
function closeFullscreen() {
|
||||||
@ -1173,60 +1307,21 @@ erDiagram
|
|||||||
overlay.classList.remove('is-open');
|
overlay.classList.remove('is-open');
|
||||||
overlay.setAttribute('aria-hidden', 'true');
|
overlay.setAttribute('aria-hidden', 'true');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
activeFullscreenState = null;
|
fullscreenViewer = null;
|
||||||
}
|
|
||||||
|
|
||||||
function handleDiagramAction(state, action, fromFullscreen) {
|
|
||||||
switch (action) {
|
|
||||||
case 'zoom-in':
|
|
||||||
state.zoom = clampZoom(state.zoom + ZOOM_STEP);
|
|
||||||
applyZoom(state);
|
|
||||||
if (fromFullscreen && state.source) {
|
|
||||||
state.source.zoom = state.zoom;
|
|
||||||
applyZoom(state.source);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'zoom-out':
|
|
||||||
state.zoom = clampZoom(state.zoom - ZOOM_STEP);
|
|
||||||
applyZoom(state);
|
|
||||||
if (fromFullscreen && state.source) {
|
|
||||||
state.source.zoom = state.zoom;
|
|
||||||
applyZoom(state.source);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'zoom-reset':
|
|
||||||
state.zoom = ZOOM_DEFAULT;
|
|
||||||
applyZoom(state);
|
|
||||||
if (fromFullscreen && state.source) {
|
|
||||||
state.source.zoom = ZOOM_DEFAULT;
|
|
||||||
applyZoom(state.source);
|
|
||||||
state.viewport.scrollTop = 0;
|
|
||||||
state.viewport.scrollLeft = 0;
|
|
||||||
} else if (state.viewport) {
|
|
||||||
state.viewport.scrollTop = 0;
|
|
||||||
state.viewport.scrollLeft = 0;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'fullscreen':
|
|
||||||
if (!fromFullscreen) openFullscreen(state);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapDiagrams() {
|
function wrapDiagrams() {
|
||||||
document.querySelectorAll('.diagram').forEach((diagram, index) => {
|
document.querySelectorAll('.diagram').forEach((diagram, index) => {
|
||||||
const section = diagram.closest('main')?.querySelector(`h2, h3`);
|
|
||||||
let title = `Diagram ${index + 1}`;
|
let title = `Diagram ${index + 1}`;
|
||||||
const prevHeading = diagram.previousElementSibling;
|
const prevHeading = diagram.previousElementSibling;
|
||||||
if (prevHeading && (prevHeading.tagName === 'H2' || prevHeading.tagName === 'H3')) {
|
if (prevHeading && (prevHeading.tagName === 'H2' || prevHeading.tagName === 'H3')) {
|
||||||
title = prevHeading.textContent.trim();
|
title = prevHeading.textContent.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolbar = createToolbar((action) => handleDiagramAction(state, action, false));
|
|
||||||
const viewport = document.createElement('div');
|
const viewport = document.createElement('div');
|
||||||
viewport.className = 'diagram-viewport';
|
viewport.className = 'diagram-viewport';
|
||||||
|
const stage = document.createElement('div');
|
||||||
|
stage.className = 'diagram-stage';
|
||||||
const canvas = document.createElement('div');
|
const canvas = document.createElement('div');
|
||||||
canvas.className = 'diagram-canvas';
|
canvas.className = 'diagram-canvas';
|
||||||
|
|
||||||
@ -1234,22 +1329,32 @@ erDiagram
|
|||||||
canvas.appendChild(diagram.firstChild);
|
canvas.appendChild(diagram.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
viewport.appendChild(canvas);
|
stage.appendChild(canvas);
|
||||||
|
viewport.appendChild(stage);
|
||||||
|
|
||||||
|
const viewer = new DiagramViewer({
|
||||||
|
viewport,
|
||||||
|
stage,
|
||||||
|
canvas,
|
||||||
|
label: null,
|
||||||
|
wheelNeedsModifier: true,
|
||||||
|
});
|
||||||
|
viewer.title = title;
|
||||||
|
viewer.element = diagram;
|
||||||
|
|
||||||
|
const toolbar = createToolbar((action) => {
|
||||||
|
if (action === 'fullscreen') {
|
||||||
|
openFullscreen(viewer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
viewer.handleAction(action);
|
||||||
|
});
|
||||||
|
viewer.label = toolbar.querySelector('[data-zoom-label]');
|
||||||
|
viewer.render();
|
||||||
|
|
||||||
diagram.appendChild(toolbar);
|
diagram.appendChild(toolbar);
|
||||||
diagram.appendChild(viewport);
|
diagram.appendChild(viewport);
|
||||||
|
diagramViewers.push(viewer);
|
||||||
const state = {
|
|
||||||
zoom: ZOOM_DEFAULT,
|
|
||||||
canvas,
|
|
||||||
label: toolbar.querySelector('[data-zoom-label]'),
|
|
||||||
viewport,
|
|
||||||
title,
|
|
||||||
element: diagram,
|
|
||||||
};
|
|
||||||
|
|
||||||
setupPan(viewport, () => state.zoom);
|
|
||||||
setupWheelZoom(viewport, state, applyZoom, true);
|
|
||||||
diagramStates.push(state);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1257,35 +1362,32 @@ erDiagram
|
|||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
const fsOpen = document.getElementById('diagram-fullscreen').classList.contains('is-open');
|
const fsOpen = document.getElementById('diagram-fullscreen').classList.contains('is-open');
|
||||||
const target = fsOpen && activeFullscreenState ? activeFullscreenState : null;
|
const viewer = fsOpen
|
||||||
|
? fullscreenViewer
|
||||||
|
: diagramViewers.find((v) => {
|
||||||
|
const rect = v.element.getBoundingClientRect();
|
||||||
|
return rect.top < window.innerHeight && rect.bottom > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!viewer) return;
|
||||||
|
|
||||||
if (e.key === 'Escape' && fsOpen) {
|
if (e.key === 'Escape' && fsOpen) {
|
||||||
closeFullscreen();
|
closeFullscreen();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!target && !e.target.closest('.diagram')) return;
|
|
||||||
|
|
||||||
const state = target || diagramStates.find((s) => s.element.contains(document.activeElement))
|
|
||||||
|| diagramStates.find((s) => {
|
|
||||||
const rect = s.element.getBoundingClientRect();
|
|
||||||
return rect.top < window.innerHeight && rect.bottom > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!state) return;
|
|
||||||
|
|
||||||
if (e.key === '+' || e.key === '=') {
|
if (e.key === '+' || e.key === '=') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleDiagramAction(state, 'zoom-in', !!target);
|
viewer.handleAction('zoom-in');
|
||||||
} else if (e.key === '-' || e.key === '_') {
|
} else if (e.key === '-' || e.key === '_') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleDiagramAction(state, 'zoom-out', !!target);
|
viewer.handleAction('zoom-out');
|
||||||
} else if (e.key === '0') {
|
} else if (e.key === '0') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleDiagramAction(state, 'zoom-reset', !!target);
|
viewer.handleAction('zoom-reset');
|
||||||
} else if ((e.key === 'f' || e.key === 'F') && !target) {
|
} else if ((e.key === 'f' || e.key === 'F') && !fsOpen) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openFullscreen(state);
|
openFullscreen(viewer);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1300,7 +1402,7 @@ erDiagram
|
|||||||
|
|
||||||
wrapDiagrams();
|
wrapDiagrams();
|
||||||
mermaid.run({ querySelector: '.mermaid' }).then(() => {
|
mermaid.run({ querySelector: '.mermaid' }).then(() => {
|
||||||
diagramStates.forEach(applyZoom);
|
diagramViewers.forEach((viewer) => viewer.fitToView());
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@ -69,15 +69,14 @@ flowchart LR
|
|||||||
ROLES["/roles CRUD + permissions matrix"]
|
ROLES["/roles CRUD + permissions matrix"]
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Masters["/masters/* (14 sub-masters)"]
|
subgraph Masters["/masters/* (item categories shared with assets)"]
|
||||||
M1[uom]
|
M1[uom]
|
||||||
M2[item-categories / subcategories / items]
|
M2[item-categories / subcategories / items]
|
||||||
M3[brands / gst-rates]
|
M3[brands / gst-rates]
|
||||||
M4[payment-terms / delivery-terms]
|
M4[payment-terms / delivery-terms]
|
||||||
M5[asset-categories / subcategories]
|
M5[departments / designations]
|
||||||
M6[departments / designations]
|
M6[locations / plants / warehouses]
|
||||||
M7[locations / plants / warehouses]
|
M7[document-series]
|
||||||
M8[document-series]
|
|
||||||
end
|
end
|
||||||
|
|
||||||
subgraph Procurement
|
subgraph Procurement
|
||||||
@ -180,11 +179,12 @@ flowchart TD
|
|||||||
PT --> PO
|
PT --> PO
|
||||||
DT[delivery_terms] --> PO
|
DT[delivery_terms] --> PO
|
||||||
|
|
||||||
ACAT[asset_categories] --> ASUB[asset_subcategories]
|
ICAT[item_categories] --> ISUB[item_subcategories]
|
||||||
ACAT --> ASSET
|
ISUB --> ITEMS[items]
|
||||||
ACAT --> GRN_AUTO[GRN auto-asset creation]
|
ICAT --> ASSET
|
||||||
ASUB --> ASSET
|
ICAT --> GRN_AUTO[GRN auto-asset creation]
|
||||||
ASUB --> GRN_AUTO
|
ISUB --> ASSET
|
||||||
|
ISUB --> GRN_AUTO
|
||||||
|
|
||||||
ITEMS --> PO_ITEMS
|
ITEMS --> PO_ITEMS
|
||||||
ITEMS --> GRN_ITEMS
|
ITEMS --> GRN_ITEMS
|
||||||
@ -260,12 +260,12 @@ stateDiagram-v2
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `asset_code` | `document_series` via `ASSET_{category.code}` |
|
| `asset_code` | `document_series` via `ASSET_{category.code}` |
|
||||||
| `asset_name` | Item name (+ `#N` if qty > 1) |
|
| `asset_name` | Item name (+ `#N` if qty > 1) |
|
||||||
| `asset_category_id` / `asset_subcategory_id` | From GRN line payload |
|
| `item_category_id` / `item_subcategory_id` | From item master (optional GRN line override) |
|
||||||
| `plant_id` | From PO |
|
| `plant_id` | From PO |
|
||||||
| `warehouse_id`, `vendor_id`, `po_id`, `grn_id`, `grn_item_id` | From GRN |
|
| `warehouse_id`, `vendor_id`, `po_id`, `grn_id`, `grn_item_id` | From GRN |
|
||||||
| `purchase_date` | `grn.grn_date` |
|
| `purchase_date` | `grn.grn_date` |
|
||||||
| `purchase_cost` | `grn_items.rate` |
|
| `purchase_cost` | `grn_items.rate` |
|
||||||
| `useful_life_years`, `depreciation_method` | From `asset_categories` defaults |
|
| `useful_life_years`, `depreciation_method` | From `item_categories` defaults |
|
||||||
| `condition` | `NEW` |
|
| `condition` | `NEW` |
|
||||||
| `status` | `IN_USE` |
|
| `status` | `IN_USE` |
|
||||||
|
|
||||||
@ -281,7 +281,7 @@ flowchart TB
|
|||||||
end
|
end
|
||||||
|
|
||||||
subgraph Validate["normalizeAssetPayload validations"]
|
subgraph Validate["normalizeAssetPayload validations"]
|
||||||
V1[asset_category + subcategory match]
|
V1[item_category + subcategory match]
|
||||||
V2[plant / dept / warehouse / user refs]
|
V2[plant / dept / warehouse / user refs]
|
||||||
V3[vendor / PO / GRN / grn_item refs]
|
V3[vendor / PO / GRN / grn_item refs]
|
||||||
V4[disposal_date required if DISPOSED/SCRAPPED]
|
V4[disposal_date required if DISPOSED/SCRAPPED]
|
||||||
@ -418,7 +418,7 @@ flowchart TD
|
|||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
subgraph Inputs
|
subgraph Inputs
|
||||||
CAT[asset_categories defaults]
|
CAT[item_categories defaults]
|
||||||
FORM[User form fields]
|
FORM[User form fields]
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -573,9 +573,11 @@ erDiagram
|
|||||||
items ||--o{ purchase_order_items : ordered
|
items ||--o{ purchase_order_items : ordered
|
||||||
items ||--o{ grn_items : received
|
items ||--o{ grn_items : received
|
||||||
|
|
||||||
asset_categories ||--o{ asset_subcategories : has
|
item_categories ||--o{ item_subcategories : has
|
||||||
asset_categories ||--o{ assets : classifies
|
item_categories ||--o{ items : classifies
|
||||||
asset_subcategories ||--o{ assets : sub_classifies
|
item_categories ||--o{ assets : classifies
|
||||||
|
item_subcategories ||--o{ items : sub_classifies
|
||||||
|
item_subcategories ||--o{ assets : sub_classifies
|
||||||
|
|
||||||
assets ||--o{ asset_amc_contracts : has
|
assets ||--o{ asset_amc_contracts : has
|
||||||
assets ||--o{ asset_insurance_policies : insured_by
|
assets ||--o{ asset_insurance_policies : insured_by
|
||||||
|
|||||||
@ -136,44 +136,6 @@ model asset_service_visits {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
/// This table contains check constraints and requires additional setup for migrations. Visit https://pris.ly/d/check-constraints for more info.
|
||||||
model asset_categories {
|
|
||||||
id BigInt @id @default(autoincrement())
|
|
||||||
code String @unique @db.VarChar(20)
|
|
||||||
name String @db.VarChar(150)
|
|
||||||
code_prefix String @unique @db.VarChar(10)
|
|
||||||
default_useful_life_years Int?
|
|
||||||
default_depreciation_method String? @db.VarChar(20)
|
|
||||||
is_active Boolean @default(true)
|
|
||||||
created_by BigInt?
|
|
||||||
updated_by BigInt?
|
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
deleted_at DateTime? @db.Timestamptz(6)
|
|
||||||
users_asset_categories_created_byTousers users? @relation("asset_categories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
|
||||||
users_asset_categories_updated_byTousers users? @relation("asset_categories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
|
||||||
asset_subcategories asset_subcategories[]
|
|
||||||
assets assets[]
|
|
||||||
}
|
|
||||||
|
|
||||||
model asset_subcategories {
|
|
||||||
id BigInt @id @default(autoincrement())
|
|
||||||
asset_category_id BigInt
|
|
||||||
code String @unique @db.VarChar(30)
|
|
||||||
name String @db.VarChar(150)
|
|
||||||
is_active Boolean @default(true)
|
|
||||||
created_by BigInt?
|
|
||||||
updated_by BigInt?
|
|
||||||
created_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
updated_at DateTime @default(now()) @db.Timestamptz(6)
|
|
||||||
deleted_at DateTime? @db.Timestamptz(6)
|
|
||||||
users_asset_subcategories_created_byTousers users? @relation("asset_subcategories_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
|
||||||
asset_categories asset_categories @relation(fields: [asset_category_id], references: [id], onUpdate: NoAction)
|
|
||||||
users_asset_subcategories_updated_byTousers users? @relation("asset_subcategories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
|
||||||
assets assets[]
|
|
||||||
|
|
||||||
@@index([asset_category_id], map: "idx_asset_subcategories_category_id")
|
|
||||||
}
|
|
||||||
|
|
||||||
model asset_transfers {
|
model asset_transfers {
|
||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
asset_id BigInt
|
asset_id BigInt
|
||||||
@ -208,8 +170,8 @@ model assets {
|
|||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
asset_code String @unique @db.VarChar(30)
|
asset_code String @unique @db.VarChar(30)
|
||||||
asset_name String @db.VarChar(200)
|
asset_name String @db.VarChar(200)
|
||||||
asset_category_id BigInt
|
item_category_id BigInt
|
||||||
asset_subcategory_id BigInt?
|
item_subcategory_id BigInt?
|
||||||
brand_model String? @db.VarChar(200)
|
brand_model String? @db.VarChar(200)
|
||||||
manufacturer String? @db.VarChar(200)
|
manufacturer String? @db.VarChar(200)
|
||||||
serial_number String? @db.VarChar(100)
|
serial_number String? @db.VarChar(100)
|
||||||
@ -248,8 +210,8 @@ model assets {
|
|||||||
asset_insurance_policies asset_insurance_policies[]
|
asset_insurance_policies asset_insurance_policies[]
|
||||||
asset_service_visits asset_service_visits[]
|
asset_service_visits asset_service_visits[]
|
||||||
asset_transfers asset_transfers[]
|
asset_transfers asset_transfers[]
|
||||||
asset_categories asset_categories @relation(fields: [asset_category_id], references: [id], onUpdate: NoAction)
|
item_categories item_categories @relation(fields: [item_category_id], references: [id], onUpdate: NoAction)
|
||||||
asset_subcategories asset_subcategories? @relation(fields: [asset_subcategory_id], references: [id], onUpdate: NoAction)
|
item_subcategories item_subcategories? @relation(fields: [item_subcategory_id], references: [id], onUpdate: NoAction)
|
||||||
users_assets_assigned_to_user_idTousers users? @relation("assets_assigned_to_user_idTousers", fields: [assigned_to_user_id], references: [id], onUpdate: NoAction)
|
users_assets_assigned_to_user_idTousers users? @relation("assets_assigned_to_user_idTousers", fields: [assigned_to_user_id], references: [id], onUpdate: NoAction)
|
||||||
users_assets_created_byTousers users? @relation("assets_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
users_assets_created_byTousers users? @relation("assets_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||||
departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction)
|
departments departments? @relation(fields: [department_id], references: [id], onUpdate: NoAction)
|
||||||
@ -261,8 +223,8 @@ model assets {
|
|||||||
vendors_assets_vendor_idTovendors vendors? @relation("assets_vendor_idTovendors", fields: [vendor_id], references: [id], onUpdate: NoAction)
|
vendors_assets_vendor_idTovendors vendors? @relation("assets_vendor_idTovendors", fields: [vendor_id], references: [id], onUpdate: NoAction)
|
||||||
warehouse locations? @relation("AssetWarehouseLocation", fields: [warehouse_id], references: [id], onUpdate: NoAction)
|
warehouse locations? @relation("AssetWarehouseLocation", fields: [warehouse_id], references: [id], onUpdate: NoAction)
|
||||||
|
|
||||||
@@index([asset_category_id], map: "idx_assets_category_id")
|
@@index([item_category_id], map: "idx_assets_item_category_id")
|
||||||
@@index([asset_subcategory_id], map: "idx_assets_subcategory_id")
|
@@index([item_subcategory_id], map: "idx_assets_item_subcategory_id")
|
||||||
@@index([department_id], map: "idx_assets_dept_id")
|
@@index([department_id], map: "idx_assets_dept_id")
|
||||||
@@index([plant_id], map: "idx_assets_plant_id")
|
@@index([plant_id], map: "idx_assets_plant_id")
|
||||||
}
|
}
|
||||||
@ -480,6 +442,9 @@ model item_categories {
|
|||||||
id BigInt @id @default(autoincrement())
|
id BigInt @id @default(autoincrement())
|
||||||
code String @unique @db.VarChar(30)
|
code String @unique @db.VarChar(30)
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
|
code_prefix String? @unique @db.VarChar(10)
|
||||||
|
default_useful_life_years Int?
|
||||||
|
default_depreciation_method String? @db.VarChar(20)
|
||||||
is_active Boolean @default(true)
|
is_active Boolean @default(true)
|
||||||
created_by BigInt?
|
created_by BigInt?
|
||||||
updated_by BigInt?
|
updated_by BigInt?
|
||||||
@ -490,6 +455,7 @@ model item_categories {
|
|||||||
users_item_categories_updated_byTousers users? @relation("item_categories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
users_item_categories_updated_byTousers users? @relation("item_categories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||||
item_subcategories item_subcategories[]
|
item_subcategories item_subcategories[]
|
||||||
items items[]
|
items items[]
|
||||||
|
assets assets[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model item_subcategories {
|
model item_subcategories {
|
||||||
@ -507,6 +473,7 @@ model item_subcategories {
|
|||||||
item_categories item_categories @relation(fields: [item_category_id], references: [id], onUpdate: NoAction)
|
item_categories item_categories @relation(fields: [item_category_id], references: [id], onUpdate: NoAction)
|
||||||
users_item_subcategories_updated_byTousers users? @relation("item_subcategories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
users_item_subcategories_updated_byTousers users? @relation("item_subcategories_updated_byTousers", fields: [updated_by], references: [id], onUpdate: NoAction)
|
||||||
items items[]
|
items items[]
|
||||||
|
assets assets[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model items {
|
model items {
|
||||||
@ -856,10 +823,6 @@ model users {
|
|||||||
asset_insurance_policies_asset_insurance_policies_updated_byTousers asset_insurance_policies[] @relation("asset_insurance_policies_updated_byTousers")
|
asset_insurance_policies_asset_insurance_policies_updated_byTousers asset_insurance_policies[] @relation("asset_insurance_policies_updated_byTousers")
|
||||||
asset_service_visits_asset_service_visits_created_byTousers asset_service_visits[] @relation("asset_service_visits_created_byTousers")
|
asset_service_visits_asset_service_visits_created_byTousers asset_service_visits[] @relation("asset_service_visits_created_byTousers")
|
||||||
asset_service_visits_asset_service_visits_updated_byTousers asset_service_visits[] @relation("asset_service_visits_updated_byTousers")
|
asset_service_visits_asset_service_visits_updated_byTousers asset_service_visits[] @relation("asset_service_visits_updated_byTousers")
|
||||||
asset_categories_asset_categories_created_byTousers asset_categories[] @relation("asset_categories_created_byTousers")
|
|
||||||
asset_categories_asset_categories_updated_byTousers asset_categories[] @relation("asset_categories_updated_byTousers")
|
|
||||||
asset_subcategories_asset_subcategories_created_byTousers asset_subcategories[] @relation("asset_subcategories_created_byTousers")
|
|
||||||
asset_subcategories_asset_subcategories_updated_byTousers asset_subcategories[] @relation("asset_subcategories_updated_byTousers")
|
|
||||||
asset_transfers_asset_transfers_from_user_idTousers asset_transfers[] @relation("asset_transfers_from_user_idTousers")
|
asset_transfers_asset_transfers_from_user_idTousers asset_transfers[] @relation("asset_transfers_from_user_idTousers")
|
||||||
asset_transfers_asset_transfers_to_user_idTousers asset_transfers[] @relation("asset_transfers_to_user_idTousers")
|
asset_transfers_asset_transfers_to_user_idTousers asset_transfers[] @relation("asset_transfers_to_user_idTousers")
|
||||||
asset_transfers_asset_transfers_transferred_byTousers asset_transfers[] @relation("asset_transfers_transferred_byTousers")
|
asset_transfers_asset_transfers_transferred_byTousers asset_transfers[] @relation("asset_transfers_transferred_byTousers")
|
||||||
|
|||||||
@ -26,7 +26,7 @@ CREATE VIEW v_asset_expiry_alerts AS
|
|||||||
END AS alert_level
|
END AS alert_level
|
||||||
FROM asset_amc_contracts amc
|
FROM asset_amc_contracts amc
|
||||||
JOIN assets a ON a.id = amc.asset_id
|
JOIN assets a ON a.id = amc.asset_id
|
||||||
JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id
|
JOIN item_categories ac_cat ON ac_cat.id = a.item_category_id
|
||||||
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
||||||
LEFT JOIN departments d ON d.id = a.department_id
|
LEFT JOIN departments d ON d.id = a.department_id
|
||||||
LEFT JOIN vendors v ON v.id = amc.vendor_id
|
LEFT JOIN vendors v ON v.id = amc.vendor_id
|
||||||
@ -47,7 +47,7 @@ UNION ALL
|
|||||||
END
|
END
|
||||||
FROM asset_insurance_policies ins
|
FROM asset_insurance_policies ins
|
||||||
JOIN assets a ON a.id = ins.asset_id
|
JOIN assets a ON a.id = ins.asset_id
|
||||||
JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id
|
JOIN item_categories ac_cat ON ac_cat.id = a.item_category_id
|
||||||
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
||||||
LEFT JOIN departments d ON d.id = a.department_id
|
LEFT JOIN departments d ON d.id = a.department_id
|
||||||
WHERE ins.is_active = TRUE AND ins.deleted_at IS NULL AND a.deleted_at IS NULL
|
WHERE ins.is_active = TRUE AND ins.deleted_at IS NULL AND a.deleted_at IS NULL
|
||||||
@ -66,7 +66,7 @@ UNION ALL
|
|||||||
WHEN (a.warranty_expiry_date - CURRENT_DATE) <= 90 THEN 'INFO'
|
WHEN (a.warranty_expiry_date - CURRENT_DATE) <= 90 THEN 'INFO'
|
||||||
END
|
END
|
||||||
FROM assets a
|
FROM assets a
|
||||||
JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id
|
JOIN item_categories ac_cat ON ac_cat.id = a.item_category_id
|
||||||
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
||||||
LEFT JOIN departments d ON d.id = a.department_id
|
LEFT JOIN departments d ON d.id = a.department_id
|
||||||
WHERE a.warranty_expiry_date IS NOT NULL AND a.deleted_at IS NULL
|
WHERE a.warranty_expiry_date IS NOT NULL AND a.deleted_at IS NULL
|
||||||
@ -95,7 +95,7 @@ SELECT
|
|||||||
amc.contract_no AS amc_contract_no,
|
amc.contract_no AS amc_contract_no,
|
||||||
amc.end_date AS amc_end_date
|
amc.end_date AS amc_end_date
|
||||||
FROM assets a
|
FROM assets a
|
||||||
JOIN asset_categories ac_cat ON ac_cat.id = a.asset_category_id
|
JOIN item_categories ac_cat ON ac_cat.id = a.item_category_id
|
||||||
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
||||||
LEFT JOIN departments d ON d.id = a.department_id
|
LEFT JOIN departments d ON d.id = a.department_id
|
||||||
JOIN LATERAL (
|
JOIN LATERAL (
|
||||||
@ -144,7 +144,7 @@ SELECT
|
|||||||
(lsv.next_service_date - CURRENT_DATE) AS service_due_in_days,
|
(lsv.next_service_date - CURRENT_DATE) AS service_due_in_days,
|
||||||
a.created_at
|
a.created_at
|
||||||
FROM assets a
|
FROM assets a
|
||||||
JOIN asset_categories ac ON ac.id = a.asset_category_id
|
JOIN item_categories ac ON ac.id = a.item_category_id
|
||||||
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
JOIN locations p ON p.id = a.plant_id AND p.type = 'plant'
|
||||||
LEFT JOIN departments d ON d.id = a.department_id
|
LEFT JOIN departments d ON d.id = a.department_id
|
||||||
LEFT JOIN locations w ON w.id = a.warehouse_id AND w.type = 'warehouse'
|
LEFT JOIN locations w ON w.id = a.warehouse_id AND w.type = 'warehouse'
|
||||||
|
|||||||
234
scripts/patch-unify-item-asset-categories.sql
Normal file
234
scripts/patch-unify-item-asset-categories.sql
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
-- Unify asset categories onto item_categories / item_subcategories
|
||||||
|
-- Run against PostgreSQL before deploying BE that expects item_* FKs on assets.
|
||||||
|
--
|
||||||
|
-- Pre-check (optional):
|
||||||
|
-- SELECT ac.code, ac.name AS asset_cat, ic.name AS item_cat
|
||||||
|
-- FROM asset_categories ac
|
||||||
|
-- JOIN item_categories ic ON UPPER(ac.code) = UPPER(ic.code)
|
||||||
|
-- AND ac.deleted_at IS NULL AND ic.deleted_at IS NULL;
|
||||||
|
|
||||||
|
BEGIN;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 1) Extend item_categories with asset defaults
|
||||||
|
-- =========================================================
|
||||||
|
ALTER TABLE item_categories
|
||||||
|
ADD COLUMN IF NOT EXISTS code_prefix VARCHAR(10),
|
||||||
|
ADD COLUMN IF NOT EXISTS default_useful_life_years INT,
|
||||||
|
ADD COLUMN IF NOT EXISTS default_depreciation_method VARCHAR(20);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'item_categories_code_prefix_key'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE item_categories
|
||||||
|
ADD CONSTRAINT item_categories_code_prefix_key UNIQUE (code_prefix);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 2) Mapping tables
|
||||||
|
-- =========================================================
|
||||||
|
CREATE TEMP TABLE map_asset_category (
|
||||||
|
old_asset_category_id BIGINT PRIMARY KEY,
|
||||||
|
new_item_category_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TEMP TABLE map_asset_subcategory (
|
||||||
|
old_asset_subcategory_id BIGINT PRIMARY KEY,
|
||||||
|
new_item_subcategory_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 3) Upsert asset_categories -> item_categories
|
||||||
|
-- =========================================================
|
||||||
|
WITH src AS (
|
||||||
|
SELECT *
|
||||||
|
FROM asset_categories
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
),
|
||||||
|
matched AS (
|
||||||
|
SELECT s.id AS old_id, ic.id AS new_id
|
||||||
|
FROM src s
|
||||||
|
JOIN item_categories ic
|
||||||
|
ON UPPER(ic.code) = UPPER(s.code)
|
||||||
|
AND ic.deleted_at IS NULL
|
||||||
|
),
|
||||||
|
inserted AS (
|
||||||
|
INSERT INTO item_categories (
|
||||||
|
code, name, code_prefix,
|
||||||
|
default_useful_life_years, default_depreciation_method,
|
||||||
|
is_active, created_by, updated_by, created_at, updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
s.code,
|
||||||
|
s.name,
|
||||||
|
s.code_prefix,
|
||||||
|
s.default_useful_life_years,
|
||||||
|
s.default_depreciation_method,
|
||||||
|
s.is_active,
|
||||||
|
s.created_by,
|
||||||
|
s.updated_by,
|
||||||
|
s.created_at,
|
||||||
|
s.updated_at
|
||||||
|
FROM src s
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM matched m WHERE m.old_id = s.id
|
||||||
|
)
|
||||||
|
RETURNING id, code
|
||||||
|
)
|
||||||
|
INSERT INTO map_asset_category (old_asset_category_id, new_item_category_id)
|
||||||
|
SELECT old_id, new_id FROM matched
|
||||||
|
UNION ALL
|
||||||
|
SELECT s.id, i.id
|
||||||
|
FROM src s
|
||||||
|
JOIN inserted i ON UPPER(i.code) = UPPER(s.code);
|
||||||
|
|
||||||
|
UPDATE item_categories ic
|
||||||
|
SET
|
||||||
|
code_prefix = COALESCE(ic.code_prefix, ac.code_prefix),
|
||||||
|
default_useful_life_years = COALESCE(ic.default_useful_life_years, ac.default_useful_life_years),
|
||||||
|
default_depreciation_method = COALESCE(ic.default_depreciation_method, ac.default_depreciation_method),
|
||||||
|
updated_at = NOW()
|
||||||
|
FROM asset_categories ac
|
||||||
|
JOIN map_asset_category m ON m.old_asset_category_id = ac.id
|
||||||
|
WHERE ic.id = m.new_item_category_id
|
||||||
|
AND ac.deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 4) Upsert asset_subcategories -> item_subcategories
|
||||||
|
-- =========================================================
|
||||||
|
WITH src AS (
|
||||||
|
SELECT asub.*, m.new_item_category_id
|
||||||
|
FROM asset_subcategories asub
|
||||||
|
JOIN map_asset_category m ON m.old_asset_category_id = asub.asset_category_id
|
||||||
|
WHERE asub.deleted_at IS NULL
|
||||||
|
),
|
||||||
|
matched AS (
|
||||||
|
SELECT s.id AS old_id, isub.id AS new_id
|
||||||
|
FROM src s
|
||||||
|
JOIN item_subcategories isub
|
||||||
|
ON UPPER(isub.code) = UPPER(s.code)
|
||||||
|
AND isub.deleted_at IS NULL
|
||||||
|
),
|
||||||
|
inserted AS (
|
||||||
|
INSERT INTO item_subcategories (
|
||||||
|
item_category_id, code, name,
|
||||||
|
is_active, created_by, updated_by, created_at, updated_at
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
s.new_item_category_id,
|
||||||
|
s.code,
|
||||||
|
s.name,
|
||||||
|
s.is_active,
|
||||||
|
s.created_by,
|
||||||
|
s.updated_by,
|
||||||
|
s.created_at,
|
||||||
|
s.updated_at
|
||||||
|
FROM src s
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM matched m WHERE m.old_id = s.id
|
||||||
|
)
|
||||||
|
RETURNING id, code
|
||||||
|
)
|
||||||
|
INSERT INTO map_asset_subcategory (old_asset_subcategory_id, new_item_subcategory_id)
|
||||||
|
SELECT old_id, new_id FROM matched
|
||||||
|
UNION ALL
|
||||||
|
SELECT s.id, i.id
|
||||||
|
FROM src s
|
||||||
|
JOIN inserted i ON UPPER(i.code) = UPPER(s.code);
|
||||||
|
|
||||||
|
UPDATE item_subcategories isub
|
||||||
|
SET
|
||||||
|
item_category_id = mcat.new_item_category_id,
|
||||||
|
updated_at = NOW()
|
||||||
|
FROM map_asset_subcategory msub
|
||||||
|
JOIN asset_subcategories asub ON asub.id = msub.old_asset_subcategory_id
|
||||||
|
JOIN map_asset_category mcat ON mcat.old_asset_category_id = asub.asset_category_id
|
||||||
|
WHERE isub.id = msub.new_item_subcategory_id;
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 5) Remap assets to item_* columns
|
||||||
|
-- =========================================================
|
||||||
|
ALTER TABLE assets
|
||||||
|
ADD COLUMN IF NOT EXISTS item_category_id BIGINT,
|
||||||
|
ADD COLUMN IF NOT EXISTS item_subcategory_id BIGINT;
|
||||||
|
|
||||||
|
UPDATE assets a
|
||||||
|
SET
|
||||||
|
item_category_id = mapped.new_item_category_id,
|
||||||
|
item_subcategory_id = mapped.new_item_subcategory_id,
|
||||||
|
updated_at = NOW()
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
a2.id AS asset_id,
|
||||||
|
mcat.new_item_category_id,
|
||||||
|
msub.new_item_subcategory_id
|
||||||
|
FROM assets a2
|
||||||
|
JOIN map_asset_category mcat
|
||||||
|
ON mcat.old_asset_category_id = a2.asset_category_id
|
||||||
|
LEFT JOIN map_asset_subcategory msub
|
||||||
|
ON msub.old_asset_subcategory_id = a2.asset_subcategory_id
|
||||||
|
) mapped
|
||||||
|
WHERE a.id = mapped.asset_id;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
DECLARE missing_count BIGINT;
|
||||||
|
BEGIN
|
||||||
|
SELECT COUNT(*) INTO missing_count
|
||||||
|
FROM assets
|
||||||
|
WHERE deleted_at IS NULL
|
||||||
|
AND item_category_id IS NULL;
|
||||||
|
|
||||||
|
IF missing_count > 0 THEN
|
||||||
|
RAISE EXCEPTION '% active assets could not be mapped to item_category_id', missing_count;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
ALTER TABLE assets
|
||||||
|
ALTER COLUMN item_category_id SET NOT NULL;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'assets_item_category_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE assets
|
||||||
|
ADD CONSTRAINT assets_item_category_id_fkey
|
||||||
|
FOREIGN KEY (item_category_id) REFERENCES item_categories (id);
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'assets_item_subcategory_id_fkey'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE assets
|
||||||
|
ADD CONSTRAINT assets_item_subcategory_id_fkey
|
||||||
|
FOREIGN KEY (item_subcategory_id) REFERENCES item_subcategories (id);
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_assets_item_category_id ON assets (item_category_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_assets_item_subcategory_id ON assets (item_subcategory_id);
|
||||||
|
|
||||||
|
-- =========================================================
|
||||||
|
-- 6) Drop dependent views, then old asset category FKs / columns / tables
|
||||||
|
-- =========================================================
|
||||||
|
DROP VIEW IF EXISTS v_asset_expiry_alerts CASCADE;
|
||||||
|
DROP VIEW IF EXISTS v_asset_next_service CASCADE;
|
||||||
|
DROP VIEW IF EXISTS v_assets CASCADE;
|
||||||
|
|
||||||
|
ALTER TABLE assets DROP CONSTRAINT IF EXISTS assets_asset_category_id_fkey;
|
||||||
|
ALTER TABLE assets DROP CONSTRAINT IF EXISTS assets_asset_subcategory_id_fkey;
|
||||||
|
DROP INDEX IF EXISTS idx_assets_category_id;
|
||||||
|
DROP INDEX IF EXISTS idx_assets_subcategory_id;
|
||||||
|
ALTER TABLE assets DROP COLUMN IF EXISTS asset_category_id;
|
||||||
|
ALTER TABLE assets DROP COLUMN IF EXISTS asset_subcategory_id;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS asset_subcategories;
|
||||||
|
DROP TABLE IF EXISTS asset_categories;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
-- Recreate reporting views after this script:
|
||||||
|
-- psql ... -f scripts/patch-assets-views.sql
|
||||||
@ -5,11 +5,11 @@ components:
|
|||||||
schemas:
|
schemas:
|
||||||
AssetsCreateBody:
|
AssetsCreateBody:
|
||||||
type: object
|
type: object
|
||||||
required: [asset_name, asset_category_id, asset_subcategory_id, plant_id]
|
required: [asset_name, item_category_id, item_subcategory_id, plant_id]
|
||||||
properties:
|
properties:
|
||||||
asset_name: { type: string, example: 'CNC Lathe Machine' }
|
asset_name: { type: string, example: 'CNC Lathe Machine' }
|
||||||
asset_category_id: { type: integer, example: 1 }
|
item_category_id: { type: integer, example: 1 }
|
||||||
asset_subcategory_id: { type: integer, example: 1 }
|
item_subcategory_id: { type: integer, example: 1 }
|
||||||
plant_id: { type: integer, example: 1 }
|
plant_id: { type: integer, example: 1 }
|
||||||
brand_model: { type: string }
|
brand_model: { type: string }
|
||||||
manufacturer: { type: string }
|
manufacturer: { type: string }
|
||||||
@ -43,8 +43,8 @@ components:
|
|||||||
minProperties: 1
|
minProperties: 1
|
||||||
properties:
|
properties:
|
||||||
asset_name: { type: string }
|
asset_name: { type: string }
|
||||||
asset_category_id: { type: integer }
|
item_category_id: { type: integer }
|
||||||
asset_subcategory_id: { type: integer }
|
item_subcategory_id: { type: integer }
|
||||||
plant_id: { type: integer }
|
plant_id: { type: integer }
|
||||||
brand_model: { type: string }
|
brand_model: { type: string }
|
||||||
manufacturer: { type: string }
|
manufacturer: { type: string }
|
||||||
@ -410,8 +410,8 @@ paths:
|
|||||||
- { name: search, in: query, schema: { type: string } }
|
- { name: search, in: query, schema: { type: string } }
|
||||||
- { name: status, in: query, schema: { type: string } }
|
- { name: status, in: query, schema: { type: string } }
|
||||||
- { name: condition, in: query, schema: { type: string } }
|
- { name: condition, in: query, schema: { type: string } }
|
||||||
- { name: asset_category_id, in: query, schema: { type: integer } }
|
- { name: item_category_id, in: query, schema: { type: integer } }
|
||||||
- { name: asset_subcategory_id, in: query, schema: { type: integer } }
|
- { name: item_subcategory_id, in: query, schema: { type: integer } }
|
||||||
- { name: plant_id, in: query, schema: { type: integer } }
|
- { name: plant_id, in: query, schema: { type: integer } }
|
||||||
- { name: department_id, in: query, schema: { type: integer } }
|
- { name: department_id, in: query, schema: { type: integer } }
|
||||||
- { name: is_active, in: query, schema: { type: boolean } }
|
- { name: is_active, in: query, schema: { type: boolean } }
|
||||||
|
|||||||
@ -3,13 +3,11 @@ tags:
|
|||||||
- name: UOM
|
- name: UOM
|
||||||
- name: Item Categories
|
- name: Item Categories
|
||||||
- name: Item Subcategories
|
- name: Item Subcategories
|
||||||
- name: Asset Subcategories
|
|
||||||
- name: Items
|
- name: Items
|
||||||
- name: Brands
|
- name: Brands
|
||||||
- name: GST Rates
|
- name: GST Rates
|
||||||
- name: Payment Terms
|
- name: Payment Terms
|
||||||
- name: Delivery Terms
|
- name: Delivery Terms
|
||||||
- name: Asset Categories
|
|
||||||
- name: Departments
|
- name: Departments
|
||||||
- name: Designations
|
- name: Designations
|
||||||
- name: Locations
|
- name: Locations
|
||||||
@ -85,6 +83,9 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
code: { type: string, example: 'RAW' }
|
code: { type: string, example: 'RAW' }
|
||||||
name: { type: string, example: 'Raw Material' }
|
name: { type: string, example: 'Raw Material' }
|
||||||
|
code_prefix: { type: string, nullable: true, example: 'MCH', description: Optional; used for asset code series when category is used for assets }
|
||||||
|
default_useful_life_years: { type: integer, nullable: true, example: 15 }
|
||||||
|
default_depreciation_method: { type: string, nullable: true, enum: [SLM, WDV, OTHER], example: SLM }
|
||||||
is_active: { type: boolean, example: true }
|
is_active: { type: boolean, example: true }
|
||||||
ItemCategoriesUpdateBody:
|
ItemCategoriesUpdateBody:
|
||||||
type: object
|
type: object
|
||||||
@ -92,6 +93,9 @@ components:
|
|||||||
properties:
|
properties:
|
||||||
code: { type: string, example: 'RAW' }
|
code: { type: string, example: 'RAW' }
|
||||||
name: { type: string, example: 'Raw Material' }
|
name: { type: string, example: 'Raw Material' }
|
||||||
|
code_prefix: { type: string, nullable: true, example: 'MCH' }
|
||||||
|
default_useful_life_years: { type: integer, nullable: true, example: 15 }
|
||||||
|
default_depreciation_method: { type: string, nullable: true, enum: [SLM, WDV, OTHER], example: SLM }
|
||||||
is_active: { type: boolean, example: true }
|
is_active: { type: boolean, example: true }
|
||||||
ItemSubcategoriesCreateBody:
|
ItemSubcategoriesCreateBody:
|
||||||
type: object
|
type: object
|
||||||
@ -215,42 +219,6 @@ components:
|
|||||||
name: { type: string, example: 'Free on Road' }
|
name: { type: string, example: 'Free on Road' }
|
||||||
description: { type: string, example: 'Delivered to site' }
|
description: { type: string, example: 'Delivered to site' }
|
||||||
is_active: { type: boolean, example: true }
|
is_active: { type: boolean, example: true }
|
||||||
AssetCategoriesCreateBody:
|
|
||||||
type: object
|
|
||||||
required: [code, name, code_prefix, default_useful_life_years, default_depreciation_method]
|
|
||||||
properties:
|
|
||||||
code: { type: string, example: 'MCH' }
|
|
||||||
name: { type: string, example: 'Plant & Machinery' }
|
|
||||||
code_prefix: { type: string, example: 'MCH' }
|
|
||||||
default_useful_life_years: { type: integer, example: 15 }
|
|
||||||
default_depreciation_method: { type: string, enum: [SLM, WDV, OTHER], example: WDV }
|
|
||||||
is_active: { type: boolean, example: true }
|
|
||||||
AssetCategoriesUpdateBody:
|
|
||||||
type: object
|
|
||||||
minProperties: 1
|
|
||||||
properties:
|
|
||||||
code: { type: string, example: 'MCH' }
|
|
||||||
name: { type: string, example: 'Plant & Machinery' }
|
|
||||||
code_prefix: { type: string, example: 'MCH' }
|
|
||||||
default_useful_life_years: { type: integer, example: 15 }
|
|
||||||
default_depreciation_method: { type: string, enum: [SLM, WDV, OTHER], example: WDV }
|
|
||||||
is_active: { type: boolean, example: true }
|
|
||||||
AssetSubcategoriesCreateBody:
|
|
||||||
type: object
|
|
||||||
required: [asset_category_id, code, name]
|
|
||||||
properties:
|
|
||||||
asset_category_id: { type: integer, example: 1 }
|
|
||||||
code: { type: string, example: 'CNC' }
|
|
||||||
name: { type: string, example: 'CNC Machines' }
|
|
||||||
is_active: { type: boolean, example: true }
|
|
||||||
AssetSubcategoriesUpdateBody:
|
|
||||||
type: object
|
|
||||||
minProperties: 1
|
|
||||||
properties:
|
|
||||||
asset_category_id: { type: integer, example: 1 }
|
|
||||||
code: { type: string, example: 'CNC' }
|
|
||||||
name: { type: string, example: 'CNC Machines' }
|
|
||||||
is_active: { type: boolean, example: true }
|
|
||||||
DepartmentsCreateBody:
|
DepartmentsCreateBody:
|
||||||
type: object
|
type: object
|
||||||
required: [name]
|
required: [name]
|
||||||
@ -1079,148 +1047,6 @@ paths:
|
|||||||
description: Deleted
|
description: Deleted
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||||
"404": { description: Not found }
|
"404": { description: Not found }
|
||||||
/masters/asset-categories:
|
|
||||||
get:
|
|
||||||
tags: [Asset Categories]
|
|
||||||
summary: List Asset Categories
|
|
||||||
parameters:
|
|
||||||
- in: query
|
|
||||||
name: page
|
|
||||||
schema: { type: integer, default: 1 }
|
|
||||||
- in: query
|
|
||||||
name: limit
|
|
||||||
schema: { type: integer, default: 20, maximum: 100 }
|
|
||||||
- in: query
|
|
||||||
name: search
|
|
||||||
schema: { type: string }
|
|
||||||
- in: query
|
|
||||||
name: is_active
|
|
||||||
schema: { type: boolean }
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: List fetched
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
post:
|
|
||||||
tags: [Asset Categories]
|
|
||||||
summary: Create Asset Categorie
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema: { $ref: "#/components/schemas/AssetCategoriesCreateBody" }
|
|
||||||
responses:
|
|
||||||
"201":
|
|
||||||
description: Created
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
"409": { description: Duplicate key }
|
|
||||||
/masters/asset-categories/{id}:
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
schema: { type: string }
|
|
||||||
get:
|
|
||||||
tags: [Asset Categories]
|
|
||||||
summary: Get Asset Categorie by id
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Fetched
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
"404": { description: Not found }
|
|
||||||
put:
|
|
||||||
tags: [Asset Categories]
|
|
||||||
summary: Update Asset Categorie
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema: { $ref: "#/components/schemas/AssetCategoriesUpdateBody" }
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Updated
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
"404": { description: Not found }
|
|
||||||
delete:
|
|
||||||
tags: [Asset Categories]
|
|
||||||
summary: Delete Asset Categorie
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Deleted
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
"404": { description: Not found }
|
|
||||||
/masters/asset-subcategories:
|
|
||||||
get:
|
|
||||||
tags: [Asset Subcategories]
|
|
||||||
summary: List Asset Subcategories
|
|
||||||
parameters:
|
|
||||||
- in: query
|
|
||||||
name: page
|
|
||||||
schema: { type: integer, default: 1 }
|
|
||||||
- in: query
|
|
||||||
name: limit
|
|
||||||
schema: { type: integer, default: 20, maximum: 100 }
|
|
||||||
- in: query
|
|
||||||
name: search
|
|
||||||
schema: { type: string }
|
|
||||||
- in: query
|
|
||||||
name: is_active
|
|
||||||
schema: { type: boolean }
|
|
||||||
- in: query
|
|
||||||
name: asset_category_id
|
|
||||||
schema: { type: integer }
|
|
||||||
description: Filter by parent asset category
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: List fetched
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
post:
|
|
||||||
tags: [Asset Subcategories]
|
|
||||||
summary: Create Asset Subcategory
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema: { $ref: "#/components/schemas/AssetSubcategoriesCreateBody" }
|
|
||||||
responses:
|
|
||||||
"201":
|
|
||||||
description: Created
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
"409": { description: Duplicate key }
|
|
||||||
/masters/asset-subcategories/{id}:
|
|
||||||
parameters:
|
|
||||||
- in: path
|
|
||||||
name: id
|
|
||||||
required: true
|
|
||||||
schema: { type: string }
|
|
||||||
get:
|
|
||||||
tags: [Asset Subcategories]
|
|
||||||
summary: Get Asset Subcategory by id
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Fetched
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
"404": { description: Not found }
|
|
||||||
put:
|
|
||||||
tags: [Asset Subcategories]
|
|
||||||
summary: Update Asset Subcategory
|
|
||||||
requestBody:
|
|
||||||
required: true
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema: { $ref: "#/components/schemas/AssetSubcategoriesUpdateBody" }
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Updated
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
"404": { description: Not found }
|
|
||||||
delete:
|
|
||||||
tags: [Asset Subcategories]
|
|
||||||
summary: Delete Asset Subcategory
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: Deleted
|
|
||||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
|
||||||
"404": { description: Not found }
|
|
||||||
/masters/departments:
|
/masters/departments:
|
||||||
get:
|
get:
|
||||||
tags: [Departments]
|
tags: [Departments]
|
||||||
|
|||||||
@ -19,8 +19,8 @@ components:
|
|||||||
expiry_date: { type: string, format: date, nullable: true }
|
expiry_date: { type: string, format: date, nullable: true }
|
||||||
storage_location: { type: string, example: 'Rack A-12' }
|
storage_location: { type: string, example: 'Rack A-12' }
|
||||||
remarks: { type: string, example: 'Partial receipt' }
|
remarks: { type: string, example: 'Partial receipt' }
|
||||||
asset_category_id: { type: integer, nullable: true, example: 1, description: Required when PO item is an asset item }
|
item_category_id: { type: integer, nullable: true, example: 1, description: Optional override; defaults from PO item master when omitted }
|
||||||
asset_subcategory_id: { type: integer, nullable: true, example: 1, description: Required when PO item is an asset item }
|
item_subcategory_id: { type: integer, nullable: true, example: 1, description: Optional override; defaults from PO item master when omitted }
|
||||||
GrnCreateBody:
|
GrnCreateBody:
|
||||||
type: object
|
type: object
|
||||||
required: [grn_date, po_id, warehouse_id, items]
|
required: [grn_date, po_id, warehouse_id, items]
|
||||||
|
|||||||
@ -8,8 +8,8 @@ const SOFT_DELETE_TABLES = new Set([
|
|||||||
'users',
|
'users',
|
||||||
'purchase_orders',
|
'purchase_orders',
|
||||||
'grn',
|
'grn',
|
||||||
'asset_categories',
|
'item_categories',
|
||||||
'asset_subcategories',
|
'item_subcategories',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const toDateOnly = (value) => {
|
const toDateOnly = (value) => {
|
||||||
|
|||||||
@ -14,8 +14,17 @@ const {
|
|||||||
const { assertReference, toDateOnly } = require('./assets.helpers');
|
const { assertReference, toDateOnly } = require('./assets.helpers');
|
||||||
|
|
||||||
const assetInclude = {
|
const assetInclude = {
|
||||||
asset_categories: { select: { id: true, code: true, name: true, code_prefix: true } },
|
item_categories: {
|
||||||
asset_subcategories: { select: { id: true, code: true, name: true, asset_category_id: true } },
|
select: {
|
||||||
|
id: true,
|
||||||
|
code: true,
|
||||||
|
name: true,
|
||||||
|
code_prefix: true,
|
||||||
|
default_useful_life_years: true,
|
||||||
|
default_depreciation_method: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
item_subcategories: { select: { id: true, code: true, name: true, item_category_id: true } },
|
||||||
plant: { select: { id: true, code: true, name: true } },
|
plant: { select: { id: true, code: true, name: true } },
|
||||||
departments: { select: { id: true, name: true } },
|
departments: { select: { id: true, name: true } },
|
||||||
warehouse: { select: { id: true, code: true, name: true } },
|
warehouse: { select: { id: true, code: true, name: true } },
|
||||||
@ -50,8 +59,8 @@ const assetSeriesCode = (categoryCode) => `ASSET_${categoryCode}`;
|
|||||||
const sanitizeAsset = (asset) => {
|
const sanitizeAsset = (asset) => {
|
||||||
if (!asset) return null;
|
if (!asset) return null;
|
||||||
const {
|
const {
|
||||||
asset_categories,
|
item_categories,
|
||||||
asset_subcategories,
|
item_subcategories,
|
||||||
plant,
|
plant,
|
||||||
departments,
|
departments,
|
||||||
warehouse,
|
warehouse,
|
||||||
@ -82,8 +91,8 @@ const sanitizeAsset = (asset) => {
|
|||||||
purchase_cost: purchaseCost,
|
purchase_cost: purchaseCost,
|
||||||
salvage_value: salvageValue,
|
salvage_value: salvageValue,
|
||||||
depreciation_rate: depreciationRate,
|
depreciation_rate: depreciationRate,
|
||||||
asset_category: asset_categories || null,
|
item_category: item_categories || null,
|
||||||
asset_subcategory: asset_subcategories || null,
|
item_subcategory: item_subcategories || null,
|
||||||
plant: plant || null,
|
plant: plant || null,
|
||||||
department: departments || null,
|
department: departments || null,
|
||||||
warehouse: warehouse || null,
|
warehouse: warehouse || null,
|
||||||
@ -101,8 +110,8 @@ const sanitizeAsset = (asset) => {
|
|||||||
useful_life_years: rest.useful_life_years,
|
useful_life_years: rest.useful_life_years,
|
||||||
purchase_date: rest.purchase_date,
|
purchase_date: rest.purchase_date,
|
||||||
}),
|
}),
|
||||||
asset_categories: undefined,
|
item_categories: undefined,
|
||||||
asset_subcategories: undefined,
|
item_subcategories: undefined,
|
||||||
departments: undefined,
|
departments: undefined,
|
||||||
users_assets_assigned_to_user_idTousers: undefined,
|
users_assets_assigned_to_user_idTousers: undefined,
|
||||||
vendors_assets_vendor_idTovendors: undefined,
|
vendors_assets_vendor_idTovendors: undefined,
|
||||||
@ -147,29 +156,29 @@ const assertDisposalFields = (status, disposalDate) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const assertAssetSubcategory = async (subcategoryId, categoryId) => {
|
const assertItemSubcategory = async (subcategoryId, categoryId) => {
|
||||||
const subcategory = await assertReference(
|
const subcategory = await assertReference(
|
||||||
'asset_subcategories',
|
'item_subcategories',
|
||||||
subcategoryId,
|
subcategoryId,
|
||||||
'asset_subcategory_id'
|
'item_subcategory_id'
|
||||||
);
|
);
|
||||||
if (subcategory.asset_category_id.toString() !== BigInt(categoryId).toString()) {
|
if (subcategory.item_category_id.toString() !== BigInt(categoryId).toString()) {
|
||||||
throw new ApiError(422, 'asset_subcategory_id does not belong to the selected asset_category_id');
|
throw new ApiError(422, 'item_subcategory_id does not belong to the selected item_category_id');
|
||||||
}
|
}
|
||||||
return subcategory;
|
return subcategory;
|
||||||
};
|
};
|
||||||
|
|
||||||
const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||||
const category = await assertReference(
|
const category = await assertReference(
|
||||||
'asset_categories',
|
'item_categories',
|
||||||
payload.asset_category_id,
|
payload.item_category_id,
|
||||||
'asset_category_id'
|
'item_category_id'
|
||||||
);
|
);
|
||||||
if (isCreate && !payload.asset_subcategory_id) {
|
if (isCreate && !payload.item_subcategory_id) {
|
||||||
throw new ApiError(422, 'asset_subcategory_id is required');
|
throw new ApiError(422, 'item_subcategory_id is required');
|
||||||
}
|
}
|
||||||
if (payload.asset_subcategory_id) {
|
if (payload.item_subcategory_id) {
|
||||||
await assertAssetSubcategory(payload.asset_subcategory_id, payload.asset_category_id);
|
await assertItemSubcategory(payload.item_subcategory_id, payload.item_category_id);
|
||||||
}
|
}
|
||||||
await assertPlant(payload.plant_id, 'plant_id');
|
await assertPlant(payload.plant_id, 'plant_id');
|
||||||
if (payload.department_id)
|
if (payload.department_id)
|
||||||
@ -212,9 +221,9 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
asset_name: payload.asset_name,
|
asset_name: payload.asset_name,
|
||||||
asset_category_id: BigInt(payload.asset_category_id),
|
item_category_id: BigInt(payload.item_category_id),
|
||||||
asset_subcategory_id: payload.asset_subcategory_id
|
item_subcategory_id: payload.item_subcategory_id
|
||||||
? BigInt(payload.asset_subcategory_id)
|
? BigInt(payload.item_subcategory_id)
|
||||||
: null,
|
: null,
|
||||||
brand_model: payload.brand_model || null,
|
brand_model: payload.brand_model || null,
|
||||||
manufacturer: payload.manufacturer || null,
|
manufacturer: payload.manufacturer || null,
|
||||||
@ -294,9 +303,9 @@ const listAssets = async (query) => {
|
|||||||
deleted_at: null,
|
deleted_at: null,
|
||||||
...(query.status ? { status: query.status } : {}),
|
...(query.status ? { status: query.status } : {}),
|
||||||
...(query.condition ? { condition: query.condition } : {}),
|
...(query.condition ? { condition: query.condition } : {}),
|
||||||
...(query.asset_category_id ? { asset_category_id: BigInt(query.asset_category_id) } : {}),
|
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
|
||||||
...(query.asset_subcategory_id
|
...(query.item_subcategory_id
|
||||||
? { asset_subcategory_id: BigInt(query.asset_subcategory_id) }
|
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
|
||||||
: {}),
|
: {}),
|
||||||
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
...(query.plant_id ? { plant_id: BigInt(query.plant_id) } : {}),
|
||||||
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||||
@ -333,11 +342,11 @@ const updateAsset = async (id, payload, userId, requestId) => {
|
|||||||
const existing = await getAssetOrThrow(id);
|
const existing = await getAssetOrThrow(id);
|
||||||
const merged = {
|
const merged = {
|
||||||
asset_name: payload.asset_name ?? existing.asset_name,
|
asset_name: payload.asset_name ?? existing.asset_name,
|
||||||
asset_category_id: payload.asset_category_id ?? existing.asset_category_id,
|
item_category_id: payload.item_category_id ?? existing.item_category_id,
|
||||||
asset_subcategory_id:
|
item_subcategory_id:
|
||||||
payload.asset_subcategory_id !== undefined
|
payload.item_subcategory_id !== undefined
|
||||||
? payload.asset_subcategory_id
|
? payload.item_subcategory_id
|
||||||
: existing.asset_subcategory_id,
|
: existing.item_subcategory_id,
|
||||||
brand_model: payload.brand_model !== undefined ? payload.brand_model : existing.brand_model,
|
brand_model: payload.brand_model !== undefined ? payload.brand_model : existing.brand_model,
|
||||||
manufacturer: payload.manufacturer !== undefined ? payload.manufacturer : existing.manufacturer,
|
manufacturer: payload.manufacturer !== undefined ? payload.manufacturer : existing.manufacturer,
|
||||||
serial_number:
|
serial_number:
|
||||||
|
|||||||
@ -16,8 +16,8 @@ const {
|
|||||||
|
|
||||||
const assetFields = {
|
const assetFields = {
|
||||||
asset_name: Joi.string().max(200).required(),
|
asset_name: Joi.string().max(200).required(),
|
||||||
asset_category_id: Joi.number().integer().positive().required(),
|
item_category_id: Joi.number().integer().positive().required(),
|
||||||
asset_subcategory_id: Joi.number().integer().positive().required(),
|
item_subcategory_id: Joi.number().integer().positive().required(),
|
||||||
brand_model: Joi.string().max(200).allow(null, '').optional(),
|
brand_model: Joi.string().max(200).allow(null, '').optional(),
|
||||||
manufacturer: Joi.string().max(200).allow(null, '').optional(),
|
manufacturer: Joi.string().max(200).allow(null, '').optional(),
|
||||||
serial_number: Joi.string().max(100).allow(null, '').optional(),
|
serial_number: Joi.string().max(100).allow(null, '').optional(),
|
||||||
@ -59,8 +59,8 @@ const createAssetSchema = Joi.object(assetFields);
|
|||||||
|
|
||||||
const updateAssetSchema = Joi.object({
|
const updateAssetSchema = Joi.object({
|
||||||
asset_name: assetFields.asset_name.optional(),
|
asset_name: assetFields.asset_name.optional(),
|
||||||
asset_category_id: assetFields.asset_category_id.optional(),
|
item_category_id: assetFields.item_category_id.optional(),
|
||||||
asset_subcategory_id: assetFields.asset_subcategory_id.optional(),
|
item_subcategory_id: assetFields.item_subcategory_id.optional(),
|
||||||
brand_model: assetFields.brand_model,
|
brand_model: assetFields.brand_model,
|
||||||
manufacturer: assetFields.manufacturer,
|
manufacturer: assetFields.manufacturer,
|
||||||
serial_number: assetFields.serial_number,
|
serial_number: assetFields.serial_number,
|
||||||
@ -101,8 +101,8 @@ const listAssetsQuerySchema = Joi.object({
|
|||||||
condition: Joi.string()
|
condition: Joi.string()
|
||||||
.valid(...ASSET_CONDITIONS)
|
.valid(...ASSET_CONDITIONS)
|
||||||
.optional(),
|
.optional(),
|
||||||
asset_category_id: Joi.number().integer().positive().optional(),
|
item_category_id: Joi.number().integer().positive().optional(),
|
||||||
asset_subcategory_id: Joi.number().integer().positive().optional(),
|
item_subcategory_id: Joi.number().integer().positive().optional(),
|
||||||
plant_id: Joi.number().integer().positive().optional(),
|
plant_id: Joi.number().integer().positive().optional(),
|
||||||
department_id: Joi.number().integer().positive().optional(),
|
department_id: Joi.number().integer().positive().optional(),
|
||||||
is_active: Joi.boolean().optional(),
|
is_active: Joi.boolean().optional(),
|
||||||
|
|||||||
@ -4,7 +4,7 @@ const { recalculatePoStatus } = require('./grn.poStatus');
|
|||||||
|
|
||||||
const createAssetsForLine = async (
|
const createAssetsForLine = async (
|
||||||
tx,
|
tx,
|
||||||
{ grn, grnItem, item, assetCategory, assetSubcategory, plantId, assetCodes, userId }
|
{ grn, grnItem, item, itemCategory, itemSubcategory, plantId, assetCodes, userId }
|
||||||
) => {
|
) => {
|
||||||
const createdAssets = [];
|
const createdAssets = [];
|
||||||
|
|
||||||
@ -13,8 +13,8 @@ const createAssetsForLine = async (
|
|||||||
data: {
|
data: {
|
||||||
asset_code: assetCodes[index],
|
asset_code: assetCodes[index],
|
||||||
asset_name: assetCodes.length > 1 ? `${item.item_name} #${index + 1}` : item.item_name,
|
asset_name: assetCodes.length > 1 ? `${item.item_name} #${index + 1}` : item.item_name,
|
||||||
asset_category_id: assetCategory.id,
|
item_category_id: itemCategory.id,
|
||||||
asset_subcategory_id: assetSubcategory.id,
|
item_subcategory_id: itemSubcategory.id,
|
||||||
plant_id: BigInt(plantId),
|
plant_id: BigInt(plantId),
|
||||||
warehouse_id: grn.warehouse_id,
|
warehouse_id: grn.warehouse_id,
|
||||||
vendor_id: grn.vendor_id,
|
vendor_id: grn.vendor_id,
|
||||||
@ -23,8 +23,8 @@ const createAssetsForLine = async (
|
|||||||
grn_item_id: grnItem.id,
|
grn_item_id: grnItem.id,
|
||||||
purchase_date: grn.grn_date,
|
purchase_date: grn.grn_date,
|
||||||
purchase_cost: grnItem.rate,
|
purchase_cost: grnItem.rate,
|
||||||
useful_life_years: assetCategory.default_useful_life_years,
|
useful_life_years: itemCategory.default_useful_life_years,
|
||||||
depreciation_method: assetCategory.default_depreciation_method,
|
depreciation_method: itemCategory.default_depreciation_method,
|
||||||
condition: 'NEW',
|
condition: 'NEW',
|
||||||
status: 'IN_USE',
|
status: 'IN_USE',
|
||||||
created_by: userId ? BigInt(userId) : null,
|
created_by: userId ? BigInt(userId) : null,
|
||||||
@ -67,8 +67,8 @@ const createGrnWithReceipt = async ({ grnNumber, header, items, assetPlans, user
|
|||||||
grn,
|
grn,
|
||||||
grnItem,
|
grnItem,
|
||||||
item: plan.item,
|
item: plan.item,
|
||||||
assetCategory: plan.assetCategory,
|
itemCategory: plan.itemCategory,
|
||||||
assetSubcategory: plan.assetSubcategory,
|
itemSubcategory: plan.itemSubcategory,
|
||||||
plantId: plan.plantId,
|
plantId: plan.plantId,
|
||||||
assetCodes: plan.assetCodes,
|
assetCodes: plan.assetCodes,
|
||||||
userId,
|
userId,
|
||||||
|
|||||||
@ -108,6 +108,8 @@ const getReceivablePoOrThrow = async (poId) => {
|
|||||||
item_code: true,
|
item_code: true,
|
||||||
item_name: true,
|
item_name: true,
|
||||||
is_asset_item: true,
|
is_asset_item: true,
|
||||||
|
item_category_id: true,
|
||||||
|
item_subcategory_id: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -175,48 +177,51 @@ const validateAndBuildItems = async (po, payloadItems) => {
|
|||||||
|
|
||||||
const item = poItem.items;
|
const item = poItem.items;
|
||||||
if (item.is_asset_item && acceptedQty > 0) {
|
if (item.is_asset_item && acceptedQty > 0) {
|
||||||
if (!row.asset_category_id) {
|
const categoryId = row.item_category_id ?? item.item_category_id;
|
||||||
|
const subcategoryId = row.item_subcategory_id ?? item.item_subcategory_id;
|
||||||
|
|
||||||
|
if (!categoryId) {
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
422,
|
422,
|
||||||
`Line ${row.line_no}: asset_category_id is required for asset items`
|
`Line ${row.line_no}: item_category_id is required for asset items (set on item master or GRN line)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!row.asset_subcategory_id) {
|
if (!subcategoryId) {
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
422,
|
422,
|
||||||
`Line ${row.line_no}: asset_subcategory_id is required for asset items`
|
`Line ${row.line_no}: item_subcategory_id is required for asset items (set on item master or GRN line)`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetCategory = await prisma.asset_categories.findFirst({
|
const itemCategory = await prisma.item_categories.findFirst({
|
||||||
where: { id: BigInt(row.asset_category_id), deleted_at: null, is_active: true },
|
where: { id: BigInt(categoryId), deleted_at: null, is_active: true },
|
||||||
});
|
});
|
||||||
if (!assetCategory) throw new ApiError(422, `Line ${row.line_no}: invalid asset_category_id`);
|
if (!itemCategory) throw new ApiError(422, `Line ${row.line_no}: invalid item_category_id`);
|
||||||
|
|
||||||
const assetSubcategory = await prisma.asset_subcategories.findFirst({
|
const itemSubcategory = await prisma.item_subcategories.findFirst({
|
||||||
where: { id: BigInt(row.asset_subcategory_id), deleted_at: null, is_active: true },
|
where: { id: BigInt(subcategoryId), deleted_at: null, is_active: true },
|
||||||
});
|
});
|
||||||
if (!assetSubcategory) {
|
if (!itemSubcategory) {
|
||||||
throw new ApiError(422, `Line ${row.line_no}: invalid asset_subcategory_id`);
|
throw new ApiError(422, `Line ${row.line_no}: invalid item_subcategory_id`);
|
||||||
}
|
}
|
||||||
if (assetSubcategory.asset_category_id.toString() !== assetCategory.id.toString()) {
|
if (itemSubcategory.item_category_id.toString() !== itemCategory.id.toString()) {
|
||||||
throw new ApiError(
|
throw new ApiError(
|
||||||
422,
|
422,
|
||||||
`Line ${row.line_no}: asset_subcategory_id does not belong to asset_category_id`
|
`Line ${row.line_no}: item_subcategory_id does not belong to item_category_id`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const units = Math.floor(acceptedQty);
|
const units = Math.floor(acceptedQty);
|
||||||
const assetCodes = [];
|
const assetCodes = [];
|
||||||
for (let unit = 0; unit < units; unit += 1) {
|
for (let unit = 0; unit < units; unit += 1) {
|
||||||
assetCodes.push(await nextDocumentNumber(assetSeriesCode(assetCategory.code)));
|
assetCodes.push(await nextDocumentNumber(assetSeriesCode(itemCategory.code)));
|
||||||
}
|
}
|
||||||
|
|
||||||
assetPlans.push({
|
assetPlans.push({
|
||||||
po_item_id: poItem.id,
|
po_item_id: poItem.id,
|
||||||
item,
|
item,
|
||||||
assetCategory,
|
itemCategory,
|
||||||
assetSubcategory,
|
itemSubcategory,
|
||||||
plantId: po.plant_id,
|
plantId: po.plant_id,
|
||||||
assetCodes,
|
assetCodes,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -14,8 +14,8 @@ const grnItemSchema = Joi.object({
|
|||||||
expiry_date: Joi.date().iso().allow(null).optional(),
|
expiry_date: Joi.date().iso().allow(null).optional(),
|
||||||
storage_location: Joi.string().max(100).allow(null, '').optional(),
|
storage_location: Joi.string().max(100).allow(null, '').optional(),
|
||||||
remarks: Joi.string().allow(null, '').optional(),
|
remarks: Joi.string().allow(null, '').optional(),
|
||||||
asset_category_id: Joi.number().integer().positive().allow(null).optional(),
|
item_category_id: Joi.number().integer().positive().allow(null).optional(),
|
||||||
asset_subcategory_id: Joi.number().integer().positive().allow(null).optional(),
|
item_subcategory_id: Joi.number().integer().positive().allow(null).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const createGrnSchema = Joi.object({
|
const createGrnSchema = Joi.object({
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
const asyncHandler = require('../../../utils/asyncHandler');
|
|
||||||
const ApiResponse = require('../../../utils/ApiResponse');
|
|
||||||
const service = require('./asset-categories.service');
|
|
||||||
|
|
||||||
const create = asyncHandler(async (req, res) => {
|
|
||||||
const data = await service.createAssetCategories(req.body, req.user?.id, req.id);
|
|
||||||
res.status(201).json(new ApiResponse(201, data, 'asset_categories created successfully'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = asyncHandler(async (req, res) => {
|
|
||||||
const result = await service.listAssetCategories(req.query);
|
|
||||||
res.json(new ApiResponse(200, result.data, 'asset_categories list fetched', result.meta));
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOne = asyncHandler(async (req, res) => {
|
|
||||||
const data = await service.getAssetCategoriesById(req.params.id);
|
|
||||||
res.json(new ApiResponse(200, data, 'asset_categories fetched'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const update = asyncHandler(async (req, res) => {
|
|
||||||
const data = await service.updateAssetCategories(req.params.id, req.body, req.user?.id, req.id);
|
|
||||||
res.json(new ApiResponse(200, data, 'asset_categories updated successfully'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const remove = asyncHandler(async (req, res) => {
|
|
||||||
await service.deleteAssetCategories(req.params.id, req.user?.id, req.id);
|
|
||||||
res.json(new ApiResponse(200, null, 'asset_categories deleted successfully'));
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = { create, list, getOne, update, remove };
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const authenticate = require('../../../middlewares/auth.middleware');
|
|
||||||
const authorize = require('../../../middlewares/rbac.middleware');
|
|
||||||
const validate = require('../../../middlewares/validate.middleware');
|
|
||||||
const controller = require('./asset-categories.controller');
|
|
||||||
const { createSchema, updateSchema, listQuerySchema } = require('./asset-categories.validation');
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.use(authenticate);
|
|
||||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
|
||||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
|
||||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
|
||||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
|
||||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@ -1,68 +0,0 @@
|
|||||||
const { buildMasterService } = require('../_shared/master.factory');
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
modelName: 'asset_categories',
|
|
||||||
tableName: 'asset_categories',
|
|
||||||
uniqueField: 'code',
|
|
||||||
softDelete: true,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'code',
|
|
||||||
type: 'string',
|
|
||||||
uppercase: true,
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'string',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'code_prefix',
|
|
||||||
type: 'string',
|
|
||||||
uppercase: true,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'default_useful_life_years',
|
|
||||||
type: 'int',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'default_depreciation_method',
|
|
||||||
type: 'string',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'is_active',
|
|
||||||
type: 'boolean',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'created_by',
|
|
||||||
type: 'int',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'updated_by',
|
|
||||||
type: 'int',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const service = buildMasterService(config);
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createAssetCategories: service.createOne,
|
|
||||||
listAssetCategories: service.list,
|
|
||||||
getAssetCategoriesById: service.getOne,
|
|
||||||
updateAssetCategories: service.updateOne,
|
|
||||||
deleteAssetCategories: service.removeOne,
|
|
||||||
};
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
const Joi = require('joi');
|
|
||||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
|
||||||
const { DEPRECIATION_METHODS } = require('../../assets/assets.constants');
|
|
||||||
|
|
||||||
const createSchema = Joi.object({
|
|
||||||
code: masterCode({ max: 30 }),
|
|
||||||
name: masterName({ max: 150 }),
|
|
||||||
code_prefix: masterCode({ max: 20 }),
|
|
||||||
default_useful_life_years: Joi.number().integer().optional(),
|
|
||||||
default_depreciation_method: Joi.string()
|
|
||||||
.valid(...DEPRECIATION_METHODS)
|
|
||||||
.allow(null, '')
|
|
||||||
.optional(),
|
|
||||||
is_active: Joi.boolean().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateSchema = Joi.object({
|
|
||||||
code: masterCode({ max: 30 }),
|
|
||||||
name: masterName({ max: 150 }),
|
|
||||||
code_prefix: masterCode({ max: 20 }),
|
|
||||||
default_useful_life_years: Joi.number().integer().optional(),
|
|
||||||
default_depreciation_method: Joi.string()
|
|
||||||
.valid(...DEPRECIATION_METHODS)
|
|
||||||
.allow(null, '')
|
|
||||||
.optional(),
|
|
||||||
is_active: Joi.boolean().optional(),
|
|
||||||
}).min(1);
|
|
||||||
|
|
||||||
module.exports = { createSchema, updateSchema, listQuerySchema };
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
const asyncHandler = require('../../../utils/asyncHandler');
|
|
||||||
const ApiResponse = require('../../../utils/ApiResponse');
|
|
||||||
const service = require('./asset-subcategories.service');
|
|
||||||
|
|
||||||
const create = asyncHandler(async (req, res) => {
|
|
||||||
const data = await service.createAssetSubcategories(req.body, req.user?.id, req.id);
|
|
||||||
res.status(201).json(new ApiResponse(201, data, 'asset_subcategories created successfully'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const list = asyncHandler(async (req, res) => {
|
|
||||||
const result = await service.listAssetSubcategories(req.query);
|
|
||||||
res.json(new ApiResponse(200, result.data, 'asset_subcategories list fetched', result.meta));
|
|
||||||
});
|
|
||||||
|
|
||||||
const getOne = asyncHandler(async (req, res) => {
|
|
||||||
const data = await service.getAssetSubcategoriesById(req.params.id);
|
|
||||||
res.json(new ApiResponse(200, data, 'asset_subcategories fetched'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const update = asyncHandler(async (req, res) => {
|
|
||||||
const data = await service.updateAssetSubcategories(
|
|
||||||
req.params.id,
|
|
||||||
req.body,
|
|
||||||
req.user?.id,
|
|
||||||
req.id
|
|
||||||
);
|
|
||||||
res.json(new ApiResponse(200, data, 'asset_subcategories updated successfully'));
|
|
||||||
});
|
|
||||||
|
|
||||||
const remove = asyncHandler(async (req, res) => {
|
|
||||||
await service.deleteAssetSubcategories(req.params.id, req.user?.id, req.id);
|
|
||||||
res.json(new ApiResponse(200, null, 'asset_subcategories deleted successfully'));
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = { create, list, getOne, update, remove };
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
const express = require('express');
|
|
||||||
const authenticate = require('../../../middlewares/auth.middleware');
|
|
||||||
const authorize = require('../../../middlewares/rbac.middleware');
|
|
||||||
const validate = require('../../../middlewares/validate.middleware');
|
|
||||||
const controller = require('./asset-subcategories.controller');
|
|
||||||
const { createSchema, updateSchema, listQuerySchema } = require('./asset-subcategories.validation');
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
router.use(authenticate);
|
|
||||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
|
||||||
router.get('/:id', authorize('MASTERS', 'view'), controller.getOne);
|
|
||||||
router.post('/', authorize('MASTERS', 'create'), validate(createSchema), controller.create);
|
|
||||||
router.put('/:id', authorize('MASTERS', 'edit'), validate(updateSchema), controller.update);
|
|
||||||
router.delete('/:id', authorize('MASTERS', 'delete'), controller.remove);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@ -1,158 +0,0 @@
|
|||||||
const prisma = require('../../../config/prisma');
|
|
||||||
const ApiError = require('../../../utils/ApiError');
|
|
||||||
const { getPagination } = require('../../../utils/pagination');
|
|
||||||
const { buildMasterService } = require('../_shared/master.factory');
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
modelName: 'asset_subcategories',
|
|
||||||
tableName: 'asset_subcategories',
|
|
||||||
uniqueField: 'code',
|
|
||||||
softDelete: true,
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: 'asset_category_id',
|
|
||||||
type: 'int',
|
|
||||||
asBigInt: true,
|
|
||||||
filterable: true,
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'code',
|
|
||||||
type: 'string',
|
|
||||||
uppercase: true,
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'name',
|
|
||||||
type: 'string',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'is_active',
|
|
||||||
type: 'boolean',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'created_by',
|
|
||||||
type: 'int',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'updated_by',
|
|
||||||
type: 'int',
|
|
||||||
uppercase: false,
|
|
||||||
searchable: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const service = buildMasterService(config);
|
|
||||||
|
|
||||||
const categoryInclude = {
|
|
||||||
asset_categories: { select: { id: true, code: true, name: true } },
|
|
||||||
};
|
|
||||||
|
|
||||||
const sanitizeSubcategory = (row) => {
|
|
||||||
if (!row) return null;
|
|
||||||
const { asset_categories, ...rest } = row;
|
|
||||||
return {
|
|
||||||
...rest,
|
|
||||||
asset_category: asset_categories || null,
|
|
||||||
asset_category_name: asset_categories?.name || null,
|
|
||||||
asset_categories: undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const assertAssetCategory = async (assetCategoryId) => {
|
|
||||||
const category = await prisma.asset_categories.findFirst({
|
|
||||||
where: { id: BigInt(assetCategoryId), deleted_at: null },
|
|
||||||
});
|
|
||||||
if (!category) throw new ApiError(422, 'Invalid asset_category_id');
|
|
||||||
if (category.is_active === false) throw new ApiError(422, 'asset_category_id is inactive');
|
|
||||||
return category;
|
|
||||||
};
|
|
||||||
|
|
||||||
const withCategory = async (row) => {
|
|
||||||
if (!row) return null;
|
|
||||||
const full = await prisma.asset_subcategories.findFirst({
|
|
||||||
where: { id: row.id },
|
|
||||||
include: categoryInclude,
|
|
||||||
});
|
|
||||||
return sanitizeSubcategory(full || row);
|
|
||||||
};
|
|
||||||
|
|
||||||
const listAssetSubcategories = async (query) => {
|
|
||||||
const { page, limit, skip } = getPagination(query);
|
|
||||||
|
|
||||||
const where = {
|
|
||||||
deleted_at: null,
|
|
||||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
|
||||||
...(query.asset_category_id
|
|
||||||
? { asset_category_id: BigInt(query.asset_category_id) }
|
|
||||||
: {}),
|
|
||||||
...(query.search
|
|
||||||
? {
|
|
||||||
OR: [
|
|
||||||
{ code: { contains: query.search, mode: 'insensitive' } },
|
|
||||||
{ name: { contains: query.search, mode: 'insensitive' } },
|
|
||||||
{
|
|
||||||
asset_categories: {
|
|
||||||
name: { contains: query.search, mode: 'insensitive' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
};
|
|
||||||
|
|
||||||
const [rows, total] = await Promise.all([
|
|
||||||
prisma.asset_subcategories.findMany({
|
|
||||||
where,
|
|
||||||
include: categoryInclude,
|
|
||||||
orderBy: { created_at: 'desc' },
|
|
||||||
skip,
|
|
||||||
take: limit,
|
|
||||||
}),
|
|
||||||
prisma.asset_subcategories.count({ where }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
|
||||||
data: rows.map(sanitizeSubcategory),
|
|
||||||
meta: { page, limit, total },
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAssetSubcategoriesById = async (id) => {
|
|
||||||
const row = await prisma.asset_subcategories.findFirst({
|
|
||||||
where: { id: BigInt(id), deleted_at: null },
|
|
||||||
include: categoryInclude,
|
|
||||||
});
|
|
||||||
if (!row) throw new ApiError(404, 'asset_subcategories not found');
|
|
||||||
return sanitizeSubcategory(row);
|
|
||||||
};
|
|
||||||
|
|
||||||
const createAssetSubcategories = async (payload, userId, requestId) => {
|
|
||||||
await assertAssetCategory(payload.asset_category_id);
|
|
||||||
const created = await service.createOne(payload, userId, requestId);
|
|
||||||
return withCategory(created);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateAssetSubcategories = async (id, payload, userId, requestId) => {
|
|
||||||
if (payload.asset_category_id !== undefined) {
|
|
||||||
await assertAssetCategory(payload.asset_category_id);
|
|
||||||
}
|
|
||||||
const updated = await service.updateOne(id, payload, userId, requestId);
|
|
||||||
return withCategory(updated);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createAssetSubcategories,
|
|
||||||
listAssetSubcategories,
|
|
||||||
getAssetSubcategoriesById,
|
|
||||||
updateAssetSubcategories,
|
|
||||||
deleteAssetSubcategories: service.removeOne,
|
|
||||||
};
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
const Joi = require('joi');
|
|
||||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
|
||||||
|
|
||||||
const createSchema = Joi.object({
|
|
||||||
asset_category_id: Joi.number().integer().positive().required(),
|
|
||||||
code: masterCode({ max: 30, required: true }),
|
|
||||||
name: masterName({ max: 150, required: true }),
|
|
||||||
is_active: Joi.boolean().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const updateSchema = Joi.object({
|
|
||||||
asset_category_id: Joi.number().integer().positive().optional(),
|
|
||||||
code: masterCode({ max: 30 }),
|
|
||||||
name: masterName({ max: 150 }),
|
|
||||||
is_active: Joi.boolean().optional(),
|
|
||||||
}).min(1);
|
|
||||||
|
|
||||||
const listAssetSubcategoriesQuerySchema = listQuerySchema.keys({
|
|
||||||
asset_category_id: Joi.number().integer().positive().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
createSchema,
|
|
||||||
updateSchema,
|
|
||||||
listQuerySchema: listAssetSubcategoriesQuerySchema,
|
|
||||||
};
|
|
||||||
@ -8,8 +8,6 @@ router.use('/brands', require('./brands/brands.routes'));
|
|||||||
router.use('/gst-rates', require('./gst-rates/gst-rates.routes'));
|
router.use('/gst-rates', require('./gst-rates/gst-rates.routes'));
|
||||||
router.use('/payment-terms', require('./payment-terms/payment-terms.routes'));
|
router.use('/payment-terms', require('./payment-terms/payment-terms.routes'));
|
||||||
router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes'));
|
router.use('/delivery-terms', require('./delivery-terms/delivery-terms.routes'));
|
||||||
router.use('/asset-categories', require('./asset-categories/asset-categories.routes'));
|
|
||||||
router.use('/asset-subcategories', require('./asset-subcategories/asset-subcategories.routes'));
|
|
||||||
router.use('/departments', require('./departments/departments.routes'));
|
router.use('/departments', require('./departments/departments.routes'));
|
||||||
router.use('/designations', require('./designations/designations.routes'));
|
router.use('/designations', require('./designations/designations.routes'));
|
||||||
router.use('/document-series', require('./document-series/document-series.routes'));
|
router.use('/document-series', require('./document-series/document-series.routes'));
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
const { buildMasterService } = require('../_shared/master.factory');
|
const { buildMasterService } = require('../_shared/master.factory');
|
||||||
|
|
||||||
|
const emptyToNull = (value) => (value === '' ? null : value);
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
modelName: 'item_categories',
|
modelName: 'item_categories',
|
||||||
tableName: 'item_categories',
|
tableName: 'item_categories',
|
||||||
@ -18,6 +20,24 @@ const config = {
|
|||||||
uppercase: false,
|
uppercase: false,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'code_prefix',
|
||||||
|
type: 'string',
|
||||||
|
uppercase: true,
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'default_useful_life_years',
|
||||||
|
type: 'int',
|
||||||
|
uppercase: false,
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'default_depreciation_method',
|
||||||
|
type: 'string',
|
||||||
|
uppercase: false,
|
||||||
|
searchable: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'is_active',
|
name: 'is_active',
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
@ -41,10 +61,24 @@ const config = {
|
|||||||
|
|
||||||
const service = buildMasterService(config);
|
const service = buildMasterService(config);
|
||||||
|
|
||||||
|
const normalizeAssetDefaults = (payload) => {
|
||||||
|
if (!payload || typeof payload !== 'object') return payload;
|
||||||
|
const out = { ...payload };
|
||||||
|
if (Object.prototype.hasOwnProperty.call(out, 'code_prefix')) {
|
||||||
|
out.code_prefix = emptyToNull(out.code_prefix);
|
||||||
|
}
|
||||||
|
if (Object.prototype.hasOwnProperty.call(out, 'default_depreciation_method')) {
|
||||||
|
out.default_depreciation_method = emptyToNull(out.default_depreciation_method);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
createItemCategories: service.createOne,
|
createItemCategories: (payload, userId, requestId) =>
|
||||||
|
service.createOne(normalizeAssetDefaults(payload), userId, requestId),
|
||||||
listItemCategories: service.list,
|
listItemCategories: service.list,
|
||||||
getItemCategoriesById: service.getOne,
|
getItemCategoriesById: service.getOne,
|
||||||
updateItemCategories: service.updateOne,
|
updateItemCategories: (id, payload, userId, requestId) =>
|
||||||
|
service.updateOne(id, normalizeAssetDefaults(payload), userId, requestId),
|
||||||
deleteItemCategories: service.removeOne,
|
deleteItemCategories: service.removeOne,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,15 +1,28 @@
|
|||||||
const Joi = require('joi');
|
const Joi = require('joi');
|
||||||
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
const { masterCode, masterName, listQuerySchema } = require('../_shared/masters.validation');
|
||||||
|
const { DEPRECIATION_METHODS } = require('../../assets/assets.constants');
|
||||||
|
|
||||||
const createSchema = Joi.object({
|
const createSchema = Joi.object({
|
||||||
code: masterCode({ max: 30 }),
|
code: masterCode({ max: 30 }),
|
||||||
name: masterName({ max: 150 }),
|
name: masterName({ max: 150 }),
|
||||||
|
code_prefix: masterCode({ max: 10 }).allow(null, ''),
|
||||||
|
default_useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
|
||||||
|
default_depreciation_method: Joi.string()
|
||||||
|
.valid(...DEPRECIATION_METHODS)
|
||||||
|
.allow(null, '')
|
||||||
|
.optional(),
|
||||||
is_active: Joi.boolean().optional(),
|
is_active: Joi.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateSchema = Joi.object({
|
const updateSchema = Joi.object({
|
||||||
code: masterCode({ max: 30 }),
|
code: masterCode({ max: 30 }),
|
||||||
name: masterName({ max: 150 }),
|
name: masterName({ max: 150 }),
|
||||||
|
code_prefix: masterCode({ max: 10 }).allow(null, ''),
|
||||||
|
default_useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
|
||||||
|
default_depreciation_method: Joi.string()
|
||||||
|
.valid(...DEPRECIATION_METHODS)
|
||||||
|
.allow(null, '')
|
||||||
|
.optional(),
|
||||||
is_active: Joi.boolean().optional(),
|
is_active: Joi.boolean().optional(),
|
||||||
}).min(1);
|
}).min(1);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user