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/
|
||||
│ │ │ ├── payment-terms/
|
||||
│ │ │ ├── delivery-terms/
|
||||
│ │ │ ├── asset-categories/
|
||||
│ │ │ ├── departments/
|
||||
│ │ │ ├── designations/
|
||||
│ │ │ ├── plants/
|
||||
@ -1180,7 +1179,7 @@ module.exports = router;
|
||||
|
||||
### 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:
|
||||
|
||||
@ -1197,7 +1196,6 @@ router.use('/gst-rates', require('./gst-rates/gst-rates.routes'));
|
||||
router.use('/warehouses', require('./warehouses/warehouses.routes'));
|
||||
router.use('/payment-terms', require('./payment-terms/payment-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('/designations', require('./designations/designations.routes'));
|
||||
router.use('/plants', require('./plants/plants.routes'));
|
||||
@ -1276,7 +1274,6 @@ BigInt.prototype.toJSON = function () {
|
||||
/api/v1/masters/warehouses
|
||||
/api/v1/masters/payment-terms
|
||||
/api/v1/masters/delivery-terms
|
||||
/api/v1/masters/asset-categories
|
||||
/api/v1/masters/departments
|
||||
/api/v1/masters/designations
|
||||
/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 |
|
||||
|--------|------------|-----------|
|
||||
| [x] | UOM | `/masters/uom` |
|
||||
| [x] | Item Categories | `/masters/item-categories` |
|
||||
| [x] | Item Subcategories | `/masters/item-subcategories` |
|
||||
| [x] | Item Categories | `/masters/item-categories` | Optional asset defaults: `code_prefix`, `default_useful_life_years`, `default_depreciation_method` |
|
||||
| [x] | Item Subcategories | `/masters/item-subcategories` | Filter: `item_category_id` — shared by items + assets |
|
||||
| [x] | Items | `/masters/items` |
|
||||
| [x] | Brands | `/masters/brands` |
|
||||
| [x] | GST Rates | `/masters/gst-rates` |
|
||||
| [x] | Payment Terms | `/masters/payment-terms` |
|
||||
| [x] | Delivery Terms | `/masters/delivery-terms` |
|
||||
| [x] | Asset Categories | `/masters/asset-categories` |
|
||||
| [x] | Asset Subcategories | `/masters/asset-subcategories` | Filter: `asset_category_id` |
|
||||
| [~] | Asset Categories | removed — use Item Categories |
|
||||
| [~] | Asset Subcategories | removed — use Item Subcategories |
|
||||
| [x] | Departments | `/masters/departments` |
|
||||
| [x] | Designations | `/masters/designations` |
|
||||
| [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] | 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 {
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
padding: 1rem;
|
||||
cursor: grab;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: min(70vh, 560px);
|
||||
min-height: 280px;
|
||||
background: #fafbfc;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.diagram-viewport.can-pan { cursor: grab; }
|
||||
.diagram-viewport.is-panning { cursor: grabbing; }
|
||||
|
||||
.diagram-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diagram-canvas {
|
||||
display: inline-block;
|
||||
min-width: 100%;
|
||||
transform-origin: 0 0;
|
||||
transition: transform 0.12s ease-out;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.diagram-canvas .mermaid,
|
||||
.diagram-canvas svg {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
margin: 0;
|
||||
max-width: none !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.diagram-fullscreen {
|
||||
@ -302,25 +311,36 @@
|
||||
|
||||
.diagram-fullscreen-viewport {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 2rem;
|
||||
cursor: grab;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #0f172a;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.diagram-fullscreen-viewport.can-pan { cursor: grab; }
|
||||
.diagram-fullscreen-viewport.is-panning { cursor: grabbing; }
|
||||
|
||||
.diagram-fullscreen-stage {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.diagram-fullscreen-canvas {
|
||||
display: inline-block;
|
||||
transform-origin: center center;
|
||||
transform-origin: 0 0;
|
||||
will-change: transform;
|
||||
background: var(--surface);
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 25px 50px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.diagram-fullscreen-canvas svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.diagram-fullscreen-hint {
|
||||
position: absolute;
|
||||
bottom: 1rem;
|
||||
@ -357,7 +377,9 @@
|
||||
main { margin-left: 0; max-width: 100%; }
|
||||
.diagram { break-inside: avoid; }
|
||||
.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>
|
||||
</head>
|
||||
@ -462,7 +484,7 @@ flowchart LR
|
||||
M2[item-categories / subcategories / items]
|
||||
M3[brands / gst-rates]
|
||||
M4[payment-terms / delivery-terms]
|
||||
M5[asset-categories / subcategories]
|
||||
M5[item-categories / subcategories]
|
||||
M6[departments / designations]
|
||||
M7[locations / plants / warehouses]
|
||||
M8[document-series]
|
||||
@ -565,7 +587,10 @@ flowchart TD
|
||||
PT --> 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 --> GRN_AUTO[GRN auto-asset creation]
|
||||
ASUB --> ASSET
|
||||
@ -646,12 +671,12 @@ stateDiagram-v2
|
||||
<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_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>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_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>status</code></td><td><code>IN_USE</code></td></tr>
|
||||
</tbody>
|
||||
@ -667,7 +692,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph Validate["normalizeAssetPayload validations"]
|
||||
V1[asset_category + subcategory match]
|
||||
V1[item_category + subcategory match]
|
||||
V2[plant / dept / warehouse / user refs]
|
||||
V3[vendor / PO / GRN / grn_item refs]
|
||||
V4[disposal_date required if DISPOSED/SCRAPPED]
|
||||
@ -801,7 +826,7 @@ flowchart TD
|
||||
<pre class="mermaid">
|
||||
flowchart TD
|
||||
subgraph Inputs
|
||||
CAT[asset_categories defaults]
|
||||
CAT[item_categories defaults]
|
||||
FORM[User form fields]
|
||||
end
|
||||
|
||||
@ -951,9 +976,11 @@ erDiagram
|
||||
items ||--o{ purchase_order_items : ordered
|
||||
items ||--o{ grn_items : received
|
||||
|
||||
asset_categories ||--o{ asset_subcategories : has
|
||||
asset_categories ||--o{ assets : classifies
|
||||
asset_subcategories ||--o{ assets : sub_classifies
|
||||
item_categories ||--o{ item_subcategories : has
|
||||
item_categories ||--o{ items : 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_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>
|
||||
</div>
|
||||
<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 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>
|
||||
|
||||
<script>
|
||||
const ZOOM_MIN = 0.25;
|
||||
const ZOOM_MAX = 4;
|
||||
const ZOOM_STEP = 0.2;
|
||||
const ZOOM_MIN = 0.2;
|
||||
const ZOOM_MAX = 5;
|
||||
const ZOOM_STEP = 0.15;
|
||||
const ZOOM_DEFAULT = 1;
|
||||
|
||||
function clampZoom(value) {
|
||||
@ -1061,7 +1090,8 @@ erDiagram
|
||||
<button type="button" data-action="zoom-out" title="Zoom out (-)">−</button>
|
||||
<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-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>
|
||||
<button type="button" data-action="fullscreen" title="Fullscreen (F)">⛶ Fullscreen</button>
|
||||
`;
|
||||
@ -1072,100 +1102,204 @@ erDiagram
|
||||
return toolbar;
|
||||
}
|
||||
|
||||
function applyZoom(state) {
|
||||
state.canvas.style.transform = `scale(${state.zoom})`;
|
||||
state.label.textContent = `${Math.round(state.zoom * 100)}%`;
|
||||
class DiagramViewer {
|
||||
constructor({ viewport, stage, canvas, label, wheelNeedsModifier = true }) {
|
||||
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) {
|
||||
let panning = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let scrollLeft = 0;
|
||||
let scrollTop = 0;
|
||||
const diagramViewers = [];
|
||||
let fullscreenViewer = null;
|
||||
let fullscreenToolbarReady = false;
|
||||
|
||||
viewport.addEventListener('mousedown', (e) => {
|
||||
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) {
|
||||
function openFullscreen(sourceViewer) {
|
||||
const overlay = document.getElementById('diagram-fullscreen');
|
||||
const canvas = document.getElementById('diagram-fullscreen-canvas');
|
||||
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 toolbarHost = document.getElementById('diagram-fullscreen-toolbar');
|
||||
|
||||
title.textContent = diagramState.title;
|
||||
canvas.innerHTML = '';
|
||||
const clone = diagramState.canvas.cloneNode(true);
|
||||
clone.style.transform = '';
|
||||
canvas.appendChild(clone);
|
||||
title.textContent = sourceViewer.title || 'Diagram';
|
||||
canvas.innerHTML = sourceViewer.canvas.innerHTML;
|
||||
|
||||
const fsState = {
|
||||
zoom: diagramState.zoom,
|
||||
canvas: clone,
|
||||
label: toolbarHost.querySelector('[data-zoom-label]'),
|
||||
viewport,
|
||||
source: diagramState,
|
||||
};
|
||||
|
||||
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);
|
||||
if (!fullscreenToolbarReady) {
|
||||
const toolbar = createToolbar((action) => {
|
||||
if (action === 'fullscreen') return;
|
||||
if (fullscreenViewer) fullscreenViewer.handleAction(action);
|
||||
});
|
||||
toolbarHost.appendChild(toolbar);
|
||||
fullscreenToolbarReady = true;
|
||||
}
|
||||
|
||||
fsState.label = toolbarHost.querySelector('[data-zoom-label]');
|
||||
applyZoom(fsState);
|
||||
activeFullscreenState = fsState;
|
||||
if (!fullscreenViewer) {
|
||||
fullscreenViewer = new DiagramViewer({
|
||||
viewport,
|
||||
stage,
|
||||
canvas,
|
||||
label: toolbarHost.querySelector('[data-zoom-label]'),
|
||||
wheelNeedsModifier: false,
|
||||
});
|
||||
}
|
||||
|
||||
fullscreenViewer.fitToView();
|
||||
|
||||
overlay.classList.add('is-open');
|
||||
overlay.setAttribute('aria-hidden', 'false');
|
||||
document.body.style.overflow = 'hidden';
|
||||
viewport.scrollTop = 0;
|
||||
viewport.scrollLeft = 0;
|
||||
}
|
||||
|
||||
function closeFullscreen() {
|
||||
@ -1173,60 +1307,21 @@ erDiagram
|
||||
overlay.classList.remove('is-open');
|
||||
overlay.setAttribute('aria-hidden', 'true');
|
||||
document.body.style.overflow = '';
|
||||
activeFullscreenState = 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;
|
||||
}
|
||||
fullscreenViewer = null;
|
||||
}
|
||||
|
||||
function wrapDiagrams() {
|
||||
document.querySelectorAll('.diagram').forEach((diagram, index) => {
|
||||
const section = diagram.closest('main')?.querySelector(`h2, h3`);
|
||||
let title = `Diagram ${index + 1}`;
|
||||
const prevHeading = diagram.previousElementSibling;
|
||||
if (prevHeading && (prevHeading.tagName === 'H2' || prevHeading.tagName === 'H3')) {
|
||||
title = prevHeading.textContent.trim();
|
||||
}
|
||||
|
||||
const toolbar = createToolbar((action) => handleDiagramAction(state, action, false));
|
||||
const viewport = document.createElement('div');
|
||||
viewport.className = 'diagram-viewport';
|
||||
const stage = document.createElement('div');
|
||||
stage.className = 'diagram-stage';
|
||||
const canvas = document.createElement('div');
|
||||
canvas.className = 'diagram-canvas';
|
||||
|
||||
@ -1234,22 +1329,32 @@ erDiagram
|
||||
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(viewport);
|
||||
|
||||
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);
|
||||
diagramViewers.push(viewer);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1257,35 +1362,32 @@ erDiagram
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
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) {
|
||||
closeFullscreen();
|
||||
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 === '=') {
|
||||
e.preventDefault();
|
||||
handleDiagramAction(state, 'zoom-in', !!target);
|
||||
viewer.handleAction('zoom-in');
|
||||
} else if (e.key === '-' || e.key === '_') {
|
||||
e.preventDefault();
|
||||
handleDiagramAction(state, 'zoom-out', !!target);
|
||||
viewer.handleAction('zoom-out');
|
||||
} else if (e.key === '0') {
|
||||
e.preventDefault();
|
||||
handleDiagramAction(state, 'zoom-reset', !!target);
|
||||
} else if ((e.key === 'f' || e.key === 'F') && !target) {
|
||||
viewer.handleAction('zoom-reset');
|
||||
} else if ((e.key === 'f' || e.key === 'F') && !fsOpen) {
|
||||
e.preventDefault();
|
||||
openFullscreen(state);
|
||||
openFullscreen(viewer);
|
||||
}
|
||||
});
|
||||
|
||||
@ -1300,7 +1402,7 @@ erDiagram
|
||||
|
||||
wrapDiagrams();
|
||||
mermaid.run({ querySelector: '.mermaid' }).then(() => {
|
||||
diagramStates.forEach(applyZoom);
|
||||
diagramViewers.forEach((viewer) => viewer.fitToView());
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -69,15 +69,14 @@ flowchart LR
|
||||
ROLES["/roles CRUD + permissions matrix"]
|
||||
end
|
||||
|
||||
subgraph Masters["/masters/* (14 sub-masters)"]
|
||||
subgraph Masters["/masters/* (item categories shared with assets)"]
|
||||
M1[uom]
|
||||
M2[item-categories / subcategories / items]
|
||||
M3[brands / gst-rates]
|
||||
M4[payment-terms / delivery-terms]
|
||||
M5[asset-categories / subcategories]
|
||||
M6[departments / designations]
|
||||
M7[locations / plants / warehouses]
|
||||
M8[document-series]
|
||||
M5[departments / designations]
|
||||
M6[locations / plants / warehouses]
|
||||
M7[document-series]
|
||||
end
|
||||
|
||||
subgraph Procurement
|
||||
@ -180,11 +179,12 @@ flowchart TD
|
||||
PT --> PO
|
||||
DT[delivery_terms] --> PO
|
||||
|
||||
ACAT[asset_categories] --> ASUB[asset_subcategories]
|
||||
ACAT --> ASSET
|
||||
ACAT --> GRN_AUTO[GRN auto-asset creation]
|
||||
ASUB --> ASSET
|
||||
ASUB --> GRN_AUTO
|
||||
ICAT[item_categories] --> ISUB[item_subcategories]
|
||||
ISUB --> ITEMS[items]
|
||||
ICAT --> ASSET
|
||||
ICAT --> GRN_AUTO[GRN auto-asset creation]
|
||||
ISUB --> ASSET
|
||||
ISUB --> GRN_AUTO
|
||||
|
||||
ITEMS --> PO_ITEMS
|
||||
ITEMS --> GRN_ITEMS
|
||||
@ -260,12 +260,12 @@ stateDiagram-v2
|
||||
|---|---|
|
||||
| `asset_code` | `document_series` via `ASSET_{category.code}` |
|
||||
| `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 |
|
||||
| `warehouse_id`, `vendor_id`, `po_id`, `grn_id`, `grn_item_id` | From GRN |
|
||||
| `purchase_date` | `grn.grn_date` |
|
||||
| `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` |
|
||||
| `status` | `IN_USE` |
|
||||
|
||||
@ -281,7 +281,7 @@ flowchart TB
|
||||
end
|
||||
|
||||
subgraph Validate["normalizeAssetPayload validations"]
|
||||
V1[asset_category + subcategory match]
|
||||
V1[item_category + subcategory match]
|
||||
V2[plant / dept / warehouse / user refs]
|
||||
V3[vendor / PO / GRN / grn_item refs]
|
||||
V4[disposal_date required if DISPOSED/SCRAPPED]
|
||||
@ -418,7 +418,7 @@ flowchart TD
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Inputs
|
||||
CAT[asset_categories defaults]
|
||||
CAT[item_categories defaults]
|
||||
FORM[User form fields]
|
||||
end
|
||||
|
||||
@ -573,9 +573,11 @@ erDiagram
|
||||
items ||--o{ purchase_order_items : ordered
|
||||
items ||--o{ grn_items : received
|
||||
|
||||
asset_categories ||--o{ asset_subcategories : has
|
||||
asset_categories ||--o{ assets : classifies
|
||||
asset_subcategories ||--o{ assets : sub_classifies
|
||||
item_categories ||--o{ item_subcategories : has
|
||||
item_categories ||--o{ items : 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_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.
|
||||
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 {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_id BigInt
|
||||
@ -208,8 +170,8 @@ model assets {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_code String @unique @db.VarChar(30)
|
||||
asset_name String @db.VarChar(200)
|
||||
asset_category_id BigInt
|
||||
asset_subcategory_id BigInt?
|
||||
item_category_id BigInt
|
||||
item_subcategory_id BigInt?
|
||||
brand_model String? @db.VarChar(200)
|
||||
manufacturer String? @db.VarChar(200)
|
||||
serial_number String? @db.VarChar(100)
|
||||
@ -248,8 +210,8 @@ model assets {
|
||||
asset_insurance_policies asset_insurance_policies[]
|
||||
asset_service_visits asset_service_visits[]
|
||||
asset_transfers asset_transfers[]
|
||||
asset_categories asset_categories @relation(fields: [asset_category_id], references: [id], onUpdate: NoAction)
|
||||
asset_subcategories asset_subcategories? @relation(fields: [asset_subcategory_id], references: [id], onUpdate: NoAction)
|
||||
item_categories item_categories @relation(fields: [item_category_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_created_byTousers users? @relation("assets_created_byTousers", fields: [created_by], 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)
|
||||
warehouse locations? @relation("AssetWarehouseLocation", fields: [warehouse_id], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([asset_category_id], map: "idx_assets_category_id")
|
||||
@@index([asset_subcategory_id], map: "idx_assets_subcategory_id")
|
||||
@@index([item_category_id], map: "idx_assets_item_category_id")
|
||||
@@index([item_subcategory_id], map: "idx_assets_item_subcategory_id")
|
||||
@@index([department_id], map: "idx_assets_dept_id")
|
||||
@@index([plant_id], map: "idx_assets_plant_id")
|
||||
}
|
||||
@ -480,6 +442,9 @@ model item_categories {
|
||||
id BigInt @id @default(autoincrement())
|
||||
code String @unique @db.VarChar(30)
|
||||
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)
|
||||
created_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)
|
||||
item_subcategories item_subcategories[]
|
||||
items items[]
|
||||
assets assets[]
|
||||
}
|
||||
|
||||
model item_subcategories {
|
||||
@ -507,6 +473,7 @@ model item_subcategories {
|
||||
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)
|
||||
items items[]
|
||||
assets assets[]
|
||||
}
|
||||
|
||||
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_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_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_to_user_idTousers asset_transfers[] @relation("asset_transfers_to_user_idTousers")
|
||||
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
|
||||
FROM asset_amc_contracts amc
|
||||
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'
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
LEFT JOIN vendors v ON v.id = amc.vendor_id
|
||||
@ -47,7 +47,7 @@ UNION ALL
|
||||
END
|
||||
FROM asset_insurance_policies ins
|
||||
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'
|
||||
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
|
||||
@ -66,7 +66,7 @@ UNION ALL
|
||||
WHEN (a.warranty_expiry_date - CURRENT_DATE) <= 90 THEN 'INFO'
|
||||
END
|
||||
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'
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
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.end_date AS amc_end_date
|
||||
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'
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
JOIN LATERAL (
|
||||
@ -144,7 +144,7 @@ SELECT
|
||||
(lsv.next_service_date - CURRENT_DATE) AS service_due_in_days,
|
||||
a.created_at
|
||||
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'
|
||||
LEFT JOIN departments d ON d.id = a.department_id
|
||||
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:
|
||||
AssetsCreateBody:
|
||||
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:
|
||||
asset_name: { type: string, example: 'CNC Lathe Machine' }
|
||||
asset_category_id: { type: integer, example: 1 }
|
||||
asset_subcategory_id: { type: integer, example: 1 }
|
||||
item_category_id: { type: integer, example: 1 }
|
||||
item_subcategory_id: { type: integer, example: 1 }
|
||||
plant_id: { type: integer, example: 1 }
|
||||
brand_model: { type: string }
|
||||
manufacturer: { type: string }
|
||||
@ -43,8 +43,8 @@ components:
|
||||
minProperties: 1
|
||||
properties:
|
||||
asset_name: { type: string }
|
||||
asset_category_id: { type: integer }
|
||||
asset_subcategory_id: { type: integer }
|
||||
item_category_id: { type: integer }
|
||||
item_subcategory_id: { type: integer }
|
||||
plant_id: { type: integer }
|
||||
brand_model: { type: string }
|
||||
manufacturer: { type: string }
|
||||
@ -410,8 +410,8 @@ paths:
|
||||
- { name: search, in: query, schema: { type: string } }
|
||||
- { name: status, in: query, schema: { type: string } }
|
||||
- { name: condition, in: query, schema: { type: string } }
|
||||
- { name: asset_category_id, in: query, schema: { type: integer } }
|
||||
- { name: asset_subcategory_id, in: query, schema: { type: integer } }
|
||||
- { name: item_category_id, in: query, schema: { type: integer } }
|
||||
- { name: item_subcategory_id, in: query, schema: { type: integer } }
|
||||
- { name: plant_id, in: query, schema: { type: integer } }
|
||||
- { name: department_id, in: query, schema: { type: integer } }
|
||||
- { name: is_active, in: query, schema: { type: boolean } }
|
||||
|
||||
@ -3,13 +3,11 @@ tags:
|
||||
- name: UOM
|
||||
- name: Item Categories
|
||||
- name: Item Subcategories
|
||||
- name: Asset Subcategories
|
||||
- name: Items
|
||||
- name: Brands
|
||||
- name: GST Rates
|
||||
- name: Payment Terms
|
||||
- name: Delivery Terms
|
||||
- name: Asset Categories
|
||||
- name: Departments
|
||||
- name: Designations
|
||||
- name: Locations
|
||||
@ -85,6 +83,9 @@ components:
|
||||
properties:
|
||||
code: { type: string, example: 'RAW' }
|
||||
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 }
|
||||
ItemCategoriesUpdateBody:
|
||||
type: object
|
||||
@ -92,6 +93,9 @@ components:
|
||||
properties:
|
||||
code: { type: string, example: 'RAW' }
|
||||
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 }
|
||||
ItemSubcategoriesCreateBody:
|
||||
type: object
|
||||
@ -215,42 +219,6 @@ components:
|
||||
name: { type: string, example: 'Free on Road' }
|
||||
description: { type: string, example: 'Delivered to site' }
|
||||
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:
|
||||
type: object
|
||||
required: [name]
|
||||
@ -1079,148 +1047,6 @@ paths:
|
||||
description: Deleted
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"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:
|
||||
get:
|
||||
tags: [Departments]
|
||||
|
||||
@ -19,8 +19,8 @@ components:
|
||||
expiry_date: { type: string, format: date, nullable: true }
|
||||
storage_location: { type: string, example: 'Rack A-12' }
|
||||
remarks: { type: string, example: 'Partial receipt' }
|
||||
asset_category_id: { type: integer, nullable: true, example: 1, description: Required when PO item is an asset item }
|
||||
asset_subcategory_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 }
|
||||
item_subcategory_id: { type: integer, nullable: true, example: 1, description: Optional override; defaults from PO item master when omitted }
|
||||
GrnCreateBody:
|
||||
type: object
|
||||
required: [grn_date, po_id, warehouse_id, items]
|
||||
|
||||
@ -8,8 +8,8 @@ const SOFT_DELETE_TABLES = new Set([
|
||||
'users',
|
||||
'purchase_orders',
|
||||
'grn',
|
||||
'asset_categories',
|
||||
'asset_subcategories',
|
||||
'item_categories',
|
||||
'item_subcategories',
|
||||
]);
|
||||
|
||||
const toDateOnly = (value) => {
|
||||
|
||||
@ -14,8 +14,17 @@ const {
|
||||
const { assertReference, toDateOnly } = require('./assets.helpers');
|
||||
|
||||
const assetInclude = {
|
||||
asset_categories: { select: { id: true, code: true, name: true, code_prefix: true } },
|
||||
asset_subcategories: { select: { id: true, code: true, name: true, asset_category_id: true } },
|
||||
item_categories: {
|
||||
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 } },
|
||||
departments: { select: { id: true, name: true } },
|
||||
warehouse: { select: { id: true, code: true, name: true } },
|
||||
@ -50,8 +59,8 @@ const assetSeriesCode = (categoryCode) => `ASSET_${categoryCode}`;
|
||||
const sanitizeAsset = (asset) => {
|
||||
if (!asset) return null;
|
||||
const {
|
||||
asset_categories,
|
||||
asset_subcategories,
|
||||
item_categories,
|
||||
item_subcategories,
|
||||
plant,
|
||||
departments,
|
||||
warehouse,
|
||||
@ -82,8 +91,8 @@ const sanitizeAsset = (asset) => {
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
depreciation_rate: depreciationRate,
|
||||
asset_category: asset_categories || null,
|
||||
asset_subcategory: asset_subcategories || null,
|
||||
item_category: item_categories || null,
|
||||
item_subcategory: item_subcategories || null,
|
||||
plant: plant || null,
|
||||
department: departments || null,
|
||||
warehouse: warehouse || null,
|
||||
@ -101,8 +110,8 @@ const sanitizeAsset = (asset) => {
|
||||
useful_life_years: rest.useful_life_years,
|
||||
purchase_date: rest.purchase_date,
|
||||
}),
|
||||
asset_categories: undefined,
|
||||
asset_subcategories: undefined,
|
||||
item_categories: undefined,
|
||||
item_subcategories: undefined,
|
||||
departments: undefined,
|
||||
users_assets_assigned_to_user_idTousers: 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(
|
||||
'asset_subcategories',
|
||||
'item_subcategories',
|
||||
subcategoryId,
|
||||
'asset_subcategory_id'
|
||||
'item_subcategory_id'
|
||||
);
|
||||
if (subcategory.asset_category_id.toString() !== BigInt(categoryId).toString()) {
|
||||
throw new ApiError(422, 'asset_subcategory_id does not belong to the selected asset_category_id');
|
||||
if (subcategory.item_category_id.toString() !== BigInt(categoryId).toString()) {
|
||||
throw new ApiError(422, 'item_subcategory_id does not belong to the selected item_category_id');
|
||||
}
|
||||
return subcategory;
|
||||
};
|
||||
|
||||
const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
const category = await assertReference(
|
||||
'asset_categories',
|
||||
payload.asset_category_id,
|
||||
'asset_category_id'
|
||||
'item_categories',
|
||||
payload.item_category_id,
|
||||
'item_category_id'
|
||||
);
|
||||
if (isCreate && !payload.asset_subcategory_id) {
|
||||
throw new ApiError(422, 'asset_subcategory_id is required');
|
||||
if (isCreate && !payload.item_subcategory_id) {
|
||||
throw new ApiError(422, 'item_subcategory_id is required');
|
||||
}
|
||||
if (payload.asset_subcategory_id) {
|
||||
await assertAssetSubcategory(payload.asset_subcategory_id, payload.asset_category_id);
|
||||
if (payload.item_subcategory_id) {
|
||||
await assertItemSubcategory(payload.item_subcategory_id, payload.item_category_id);
|
||||
}
|
||||
await assertPlant(payload.plant_id, 'plant_id');
|
||||
if (payload.department_id)
|
||||
@ -212,9 +221,9 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
|
||||
return {
|
||||
asset_name: payload.asset_name,
|
||||
asset_category_id: BigInt(payload.asset_category_id),
|
||||
asset_subcategory_id: payload.asset_subcategory_id
|
||||
? BigInt(payload.asset_subcategory_id)
|
||||
item_category_id: BigInt(payload.item_category_id),
|
||||
item_subcategory_id: payload.item_subcategory_id
|
||||
? BigInt(payload.item_subcategory_id)
|
||||
: null,
|
||||
brand_model: payload.brand_model || null,
|
||||
manufacturer: payload.manufacturer || null,
|
||||
@ -294,9 +303,9 @@ const listAssets = async (query) => {
|
||||
deleted_at: null,
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.condition ? { condition: query.condition } : {}),
|
||||
...(query.asset_category_id ? { asset_category_id: BigInt(query.asset_category_id) } : {}),
|
||||
...(query.asset_subcategory_id
|
||||
? { asset_subcategory_id: BigInt(query.asset_subcategory_id) }
|
||||
...(query.item_category_id ? { item_category_id: BigInt(query.item_category_id) } : {}),
|
||||
...(query.item_subcategory_id
|
||||
? { item_subcategory_id: BigInt(query.item_subcategory_id) }
|
||||
: {}),
|
||||
...(query.plant_id ? { plant_id: BigInt(query.plant_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 merged = {
|
||||
asset_name: payload.asset_name ?? existing.asset_name,
|
||||
asset_category_id: payload.asset_category_id ?? existing.asset_category_id,
|
||||
asset_subcategory_id:
|
||||
payload.asset_subcategory_id !== undefined
|
||||
? payload.asset_subcategory_id
|
||||
: existing.asset_subcategory_id,
|
||||
item_category_id: payload.item_category_id ?? existing.item_category_id,
|
||||
item_subcategory_id:
|
||||
payload.item_subcategory_id !== undefined
|
||||
? payload.item_subcategory_id
|
||||
: existing.item_subcategory_id,
|
||||
brand_model: payload.brand_model !== undefined ? payload.brand_model : existing.brand_model,
|
||||
manufacturer: payload.manufacturer !== undefined ? payload.manufacturer : existing.manufacturer,
|
||||
serial_number:
|
||||
|
||||
@ -16,8 +16,8 @@ const {
|
||||
|
||||
const assetFields = {
|
||||
asset_name: Joi.string().max(200).required(),
|
||||
asset_category_id: Joi.number().integer().positive().required(),
|
||||
asset_subcategory_id: Joi.number().integer().positive().required(),
|
||||
item_category_id: Joi.number().integer().positive().required(),
|
||||
item_subcategory_id: Joi.number().integer().positive().required(),
|
||||
brand_model: Joi.string().max(200).allow(null, '').optional(),
|
||||
manufacturer: Joi.string().max(200).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({
|
||||
asset_name: assetFields.asset_name.optional(),
|
||||
asset_category_id: assetFields.asset_category_id.optional(),
|
||||
asset_subcategory_id: assetFields.asset_subcategory_id.optional(),
|
||||
item_category_id: assetFields.item_category_id.optional(),
|
||||
item_subcategory_id: assetFields.item_subcategory_id.optional(),
|
||||
brand_model: assetFields.brand_model,
|
||||
manufacturer: assetFields.manufacturer,
|
||||
serial_number: assetFields.serial_number,
|
||||
@ -101,8 +101,8 @@ const listAssetsQuerySchema = Joi.object({
|
||||
condition: Joi.string()
|
||||
.valid(...ASSET_CONDITIONS)
|
||||
.optional(),
|
||||
asset_category_id: Joi.number().integer().positive().optional(),
|
||||
asset_subcategory_id: Joi.number().integer().positive().optional(),
|
||||
item_category_id: Joi.number().integer().positive().optional(),
|
||||
item_subcategory_id: Joi.number().integer().positive().optional(),
|
||||
plant_id: Joi.number().integer().positive().optional(),
|
||||
department_id: Joi.number().integer().positive().optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
|
||||
@ -4,7 +4,7 @@ const { recalculatePoStatus } = require('./grn.poStatus');
|
||||
|
||||
const createAssetsForLine = async (
|
||||
tx,
|
||||
{ grn, grnItem, item, assetCategory, assetSubcategory, plantId, assetCodes, userId }
|
||||
{ grn, grnItem, item, itemCategory, itemSubcategory, plantId, assetCodes, userId }
|
||||
) => {
|
||||
const createdAssets = [];
|
||||
|
||||
@ -13,8 +13,8 @@ const createAssetsForLine = async (
|
||||
data: {
|
||||
asset_code: assetCodes[index],
|
||||
asset_name: assetCodes.length > 1 ? `${item.item_name} #${index + 1}` : item.item_name,
|
||||
asset_category_id: assetCategory.id,
|
||||
asset_subcategory_id: assetSubcategory.id,
|
||||
item_category_id: itemCategory.id,
|
||||
item_subcategory_id: itemSubcategory.id,
|
||||
plant_id: BigInt(plantId),
|
||||
warehouse_id: grn.warehouse_id,
|
||||
vendor_id: grn.vendor_id,
|
||||
@ -23,8 +23,8 @@ const createAssetsForLine = async (
|
||||
grn_item_id: grnItem.id,
|
||||
purchase_date: grn.grn_date,
|
||||
purchase_cost: grnItem.rate,
|
||||
useful_life_years: assetCategory.default_useful_life_years,
|
||||
depreciation_method: assetCategory.default_depreciation_method,
|
||||
useful_life_years: itemCategory.default_useful_life_years,
|
||||
depreciation_method: itemCategory.default_depreciation_method,
|
||||
condition: 'NEW',
|
||||
status: 'IN_USE',
|
||||
created_by: userId ? BigInt(userId) : null,
|
||||
@ -67,8 +67,8 @@ const createGrnWithReceipt = async ({ grnNumber, header, items, assetPlans, user
|
||||
grn,
|
||||
grnItem,
|
||||
item: plan.item,
|
||||
assetCategory: plan.assetCategory,
|
||||
assetSubcategory: plan.assetSubcategory,
|
||||
itemCategory: plan.itemCategory,
|
||||
itemSubcategory: plan.itemSubcategory,
|
||||
plantId: plan.plantId,
|
||||
assetCodes: plan.assetCodes,
|
||||
userId,
|
||||
|
||||
@ -108,6 +108,8 @@ const getReceivablePoOrThrow = async (poId) => {
|
||||
item_code: true,
|
||||
item_name: 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;
|
||||
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(
|
||||
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(
|
||||
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({
|
||||
where: { id: BigInt(row.asset_category_id), deleted_at: null, is_active: true },
|
||||
const itemCategory = await prisma.item_categories.findFirst({
|
||||
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({
|
||||
where: { id: BigInt(row.asset_subcategory_id), deleted_at: null, is_active: true },
|
||||
const itemSubcategory = await prisma.item_subcategories.findFirst({
|
||||
where: { id: BigInt(subcategoryId), deleted_at: null, is_active: true },
|
||||
});
|
||||
if (!assetSubcategory) {
|
||||
throw new ApiError(422, `Line ${row.line_no}: invalid asset_subcategory_id`);
|
||||
if (!itemSubcategory) {
|
||||
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(
|
||||
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 assetCodes = [];
|
||||
for (let unit = 0; unit < units; unit += 1) {
|
||||
assetCodes.push(await nextDocumentNumber(assetSeriesCode(assetCategory.code)));
|
||||
assetCodes.push(await nextDocumentNumber(assetSeriesCode(itemCategory.code)));
|
||||
}
|
||||
|
||||
assetPlans.push({
|
||||
po_item_id: poItem.id,
|
||||
item,
|
||||
assetCategory,
|
||||
assetSubcategory,
|
||||
itemCategory,
|
||||
itemSubcategory,
|
||||
plantId: po.plant_id,
|
||||
assetCodes,
|
||||
});
|
||||
|
||||
@ -14,8 +14,8 @@ const grnItemSchema = Joi.object({
|
||||
expiry_date: Joi.date().iso().allow(null).optional(),
|
||||
storage_location: Joi.string().max(100).allow(null, '').optional(),
|
||||
remarks: Joi.string().allow(null, '').optional(),
|
||||
asset_category_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
asset_subcategory_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
item_category_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
item_subcategory_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
});
|
||||
|
||||
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('/payment-terms', require('./payment-terms/payment-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('/designations', require('./designations/designations.routes'));
|
||||
router.use('/document-series', require('./document-series/document-series.routes'));
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
const { buildMasterService } = require('../_shared/master.factory');
|
||||
|
||||
const emptyToNull = (value) => (value === '' ? null : value);
|
||||
|
||||
const config = {
|
||||
modelName: 'item_categories',
|
||||
tableName: 'item_categories',
|
||||
@ -18,6 +20,24 @@ const config = {
|
||||
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',
|
||||
@ -41,10 +61,24 @@ const 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 = {
|
||||
createItemCategories: service.createOne,
|
||||
createItemCategories: (payload, userId, requestId) =>
|
||||
service.createOne(normalizeAssetDefaults(payload), userId, requestId),
|
||||
listItemCategories: service.list,
|
||||
getItemCategoriesById: service.getOne,
|
||||
updateItemCategories: service.updateOne,
|
||||
updateItemCategories: (id, payload, userId, requestId) =>
|
||||
service.updateOne(id, normalizeAssetDefaults(payload), userId, requestId),
|
||||
deleteItemCategories: service.removeOne,
|
||||
};
|
||||
|
||||
@ -1,15 +1,28 @@
|
||||
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: 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(),
|
||||
});
|
||||
|
||||
const updateSchema = Joi.object({
|
||||
code: masterCode({ max: 30 }),
|
||||
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(),
|
||||
}).min(1);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user