Asset module maintanance module emabled
This commit is contained in:
parent
983505d6cf
commit
355f5a9f6d
@ -176,10 +176,14 @@ model assets {
|
||||
department_id BigInt?
|
||||
location_detail String? @db.VarChar(200)
|
||||
assigned_to_user_id BigInt?
|
||||
maintenance_incharge_user_id BigInt?
|
||||
maintenance_frequency_in_days Int?
|
||||
maintenance_checklist_json Json? @db.JsonB
|
||||
vendor_id BigInt?
|
||||
po_id BigInt?
|
||||
grn_id BigInt?
|
||||
grn_item_id BigInt?
|
||||
commencement_date DateTime? @db.Date
|
||||
purchase_date DateTime? @db.Date
|
||||
purchase_cost Decimal @default(0) @db.Decimal(15, 4)
|
||||
useful_life_years Int?
|
||||
@ -203,11 +207,13 @@ model assets {
|
||||
asset_amc_contracts asset_amc_contracts[]
|
||||
asset_attachments asset_attachments[]
|
||||
asset_insurance_policies asset_insurance_policies[]
|
||||
asset_maintenance_logs asset_maintenance_logs[]
|
||||
asset_service_visits asset_service_visits[]
|
||||
asset_transfers asset_transfers[]
|
||||
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_maintenance_incharge_idTousers users? @relation("assets_maintenance_incharge_idTousers", fields: [maintenance_incharge_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)
|
||||
grn grn? @relation(fields: [grn_id], references: [id], onUpdate: NoAction)
|
||||
@ -221,6 +227,28 @@ model assets {
|
||||
@@index([item_subcategory_id], map: "idx_assets_item_subcategory_id")
|
||||
@@index([department_id], map: "idx_assets_dept_id")
|
||||
@@index([location_id], map: "idx_assets_location_id")
|
||||
@@index([maintenance_incharge_user_id], map: "idx_assets_maintenance_incharge")
|
||||
}
|
||||
|
||||
model asset_maintenance_logs {
|
||||
id BigInt @id @default(autoincrement())
|
||||
asset_id BigInt
|
||||
performed_by BigInt?
|
||||
performed_date DateTime @db.Date
|
||||
next_due_date DateTime? @db.Date
|
||||
checklist_json Json? @db.JsonB
|
||||
remarks String?
|
||||
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)
|
||||
assets assets @relation(fields: [asset_id], references: [id], onDelete: Cascade, onUpdate: NoAction)
|
||||
users_asset_maintenance_logs_performed_byTousers users? @relation("asset_maintenance_logs_performed_byTousers", fields: [performed_by], references: [id], onUpdate: NoAction)
|
||||
users_asset_maintenance_logs_created_byTousers users? @relation("asset_maintenance_logs_created_byTousers", fields: [created_by], references: [id], onUpdate: NoAction)
|
||||
|
||||
@@index([asset_id], map: "idx_asset_maintenance_logs_asset_id")
|
||||
@@index([performed_by], map: "idx_asset_maintenance_logs_performed_by")
|
||||
}
|
||||
|
||||
model audit_logs {
|
||||
@ -466,6 +494,7 @@ model items {
|
||||
is_asset_item Boolean @default(false)
|
||||
description String?
|
||||
specification String?
|
||||
tags Json? @db.JsonB
|
||||
min_order_qty Decimal? @db.Decimal(15, 4)
|
||||
reorder_level Decimal? @db.Decimal(15, 4)
|
||||
is_active Boolean @default(true)
|
||||
@ -795,12 +824,15 @@ model users {
|
||||
asset_amc_contracts_asset_amc_contracts_updated_byTousers asset_amc_contracts[] @relation("asset_amc_contracts_updated_byTousers")
|
||||
asset_insurance_policies_asset_insurance_policies_created_byTousers asset_insurance_policies[] @relation("asset_insurance_policies_created_byTousers")
|
||||
asset_insurance_policies_asset_insurance_policies_updated_byTousers asset_insurance_policies[] @relation("asset_insurance_policies_updated_byTousers")
|
||||
asset_maintenance_logs_asset_maintenance_logs_performed_byTousers asset_maintenance_logs[] @relation("asset_maintenance_logs_performed_byTousers")
|
||||
asset_maintenance_logs_asset_maintenance_logs_created_byTousers asset_maintenance_logs[] @relation("asset_maintenance_logs_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_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")
|
||||
assets_assets_assigned_to_user_idTousers assets[] @relation("assets_assigned_to_user_idTousers")
|
||||
assets_assets_maintenance_incharge_idTousers assets[] @relation("assets_maintenance_incharge_idTousers")
|
||||
assets_assets_created_byTousers assets[] @relation("assets_created_byTousers")
|
||||
assets_assets_updated_byTousers assets[] @relation("assets_updated_byTousers")
|
||||
audit_logs audit_logs[]
|
||||
|
||||
58
scripts/patch-items-tags-assets-maintenance.sql
Normal file
58
scripts/patch-items-tags-assets-maintenance.sql
Normal file
@ -0,0 +1,58 @@
|
||||
-- Items tags + Assets maintenance / commencement fields.
|
||||
-- Idempotent: safe to re-run.
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- 1. items.tags (JSONB array of strings)
|
||||
ALTER TABLE items ADD COLUMN IF NOT EXISTS tags JSONB;
|
||||
|
||||
-- 2. assets maintenance + commencement columns
|
||||
ALTER TABLE assets ADD COLUMN IF NOT EXISTS maintenance_incharge_user_id BIGINT;
|
||||
ALTER TABLE assets ADD COLUMN IF NOT EXISTS maintenance_frequency_in_days INTEGER;
|
||||
ALTER TABLE assets ADD COLUMN IF NOT EXISTS maintenance_checklist_json JSONB;
|
||||
ALTER TABLE assets ADD COLUMN IF NOT EXISTS commencement_date DATE;
|
||||
|
||||
-- Backfill commencement_date from purchase_date for existing assets
|
||||
UPDATE assets
|
||||
SET commencement_date = purchase_date
|
||||
WHERE commencement_date IS NULL
|
||||
AND purchase_date IS NOT NULL;
|
||||
|
||||
ALTER TABLE assets DROP CONSTRAINT IF EXISTS fk_assets_maintenance_incharge;
|
||||
ALTER TABLE assets
|
||||
ADD CONSTRAINT fk_assets_maintenance_incharge
|
||||
FOREIGN KEY (maintenance_incharge_user_id) REFERENCES users(id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_assets_maintenance_incharge
|
||||
ON assets(maintenance_incharge_user_id);
|
||||
|
||||
-- 3. asset_maintenance_logs table
|
||||
CREATE TABLE IF NOT EXISTS asset_maintenance_logs (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
asset_id BIGINT NOT NULL REFERENCES assets(id) ON DELETE CASCADE,
|
||||
performed_by BIGINT REFERENCES users(id),
|
||||
performed_date DATE NOT NULL,
|
||||
next_due_date DATE,
|
||||
checklist_json JSONB,
|
||||
remarks TEXT,
|
||||
created_by BIGINT,
|
||||
updated_by BIGINT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_maintenance_logs_asset_id
|
||||
ON asset_maintenance_logs(asset_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_asset_maintenance_logs_performed_by
|
||||
ON asset_maintenance_logs(performed_by);
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- Verify:
|
||||
-- SELECT column_name FROM information_schema.columns
|
||||
-- WHERE table_name='items' AND column_name='tags';
|
||||
-- SELECT column_name FROM information_schema.columns
|
||||
-- WHERE table_name='assets'
|
||||
-- AND column_name IN ('maintenance_incharge_user_id','maintenance_frequency_in_days','maintenance_checklist_json','commencement_date');
|
||||
-- SELECT to_regclass('public.asset_maintenance_logs');
|
||||
52
src/constants/indianStates.js
Normal file
52
src/constants/indianStates.js
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Indian states / UTs — shared by vendor source_of_supply and location.state.
|
||||
* Keep values identical so GST intra/inter-state comparisons stay consistent.
|
||||
*/
|
||||
const INDIAN_STATE_OPTIONS = [
|
||||
{ value: 'Andhra Pradesh', label: 'Andhra Pradesh' },
|
||||
{ value: 'Arunachal Pradesh', label: 'Arunachal Pradesh' },
|
||||
{ value: 'Assam', label: 'Assam' },
|
||||
{ value: 'Bihar', label: 'Bihar' },
|
||||
{ value: 'Chhattisgarh', label: 'Chhattisgarh' },
|
||||
{ value: 'Goa', label: 'Goa' },
|
||||
{ value: 'Gujarat', label: 'Gujarat' },
|
||||
{ value: 'Haryana', label: 'Haryana' },
|
||||
{ value: 'Himachal Pradesh', label: 'Himachal Pradesh' },
|
||||
{ value: 'Jharkhand', label: 'Jharkhand' },
|
||||
{ value: 'Karnataka', label: 'Karnataka' },
|
||||
{ value: 'Kerala', label: 'Kerala' },
|
||||
{ value: 'Madhya Pradesh', label: 'Madhya Pradesh' },
|
||||
{ value: 'Maharashtra', label: 'Maharashtra' },
|
||||
{ value: 'Manipur', label: 'Manipur' },
|
||||
{ value: 'Meghalaya', label: 'Meghalaya' },
|
||||
{ value: 'Mizoram', label: 'Mizoram' },
|
||||
{ value: 'Nagaland', label: 'Nagaland' },
|
||||
{ value: 'Odisha', label: 'Odisha' },
|
||||
{ value: 'Punjab', label: 'Punjab' },
|
||||
{ value: 'Rajasthan', label: 'Rajasthan' },
|
||||
{ value: 'Sikkim', label: 'Sikkim' },
|
||||
{ value: 'Tamil Nadu', label: 'Tamil Nadu' },
|
||||
{ value: 'Telangana', label: 'Telangana' },
|
||||
{ value: 'Tripura', label: 'Tripura' },
|
||||
{ value: 'Uttar Pradesh', label: 'Uttar Pradesh' },
|
||||
{ value: 'Uttarakhand', label: 'Uttarakhand' },
|
||||
{ value: 'West Bengal', label: 'West Bengal' },
|
||||
{ value: 'Andaman and Nicobar Islands', label: 'Andaman and Nicobar Islands' },
|
||||
{ value: 'Chandigarh', label: 'Chandigarh' },
|
||||
{
|
||||
value: 'Dadra and Nagar Haveli and Daman and Diu',
|
||||
label: 'Dadra and Nagar Haveli and Daman and Diu',
|
||||
},
|
||||
{ value: 'Delhi', label: 'Delhi' },
|
||||
{ value: 'Jammu and Kashmir', label: 'Jammu and Kashmir' },
|
||||
{ value: 'Ladakh', label: 'Ladakh' },
|
||||
{ value: 'Lakshadweep', label: 'Lakshadweep' },
|
||||
{ value: 'Puducherry', label: 'Puducherry' },
|
||||
];
|
||||
|
||||
const INDIAN_STATE_VALUES = INDIAN_STATE_OPTIONS.map((item) => item.value);
|
||||
|
||||
module.exports = {
|
||||
INDIAN_STATE_OPTIONS,
|
||||
INDIAN_STATE_VALUES,
|
||||
};
|
||||
@ -18,10 +18,24 @@ components:
|
||||
department_id: { type: integer, nullable: true }
|
||||
location_detail: { type: string }
|
||||
assigned_to_user_id: { type: integer, nullable: true }
|
||||
maintenance_incharge_user_id: { type: integer, nullable: true, description: 'User responsible for periodic maintenance checklist' }
|
||||
maintenance_frequency_in_days: { type: integer, nullable: true, example: 30, description: 'Days between maintenance checklists' }
|
||||
maintenance_checklist_json:
|
||||
type: array
|
||||
nullable: true
|
||||
description: 'Checklist TEMPLATE stored on the asset'
|
||||
items:
|
||||
type: object
|
||||
required: [key, label]
|
||||
properties:
|
||||
key: { type: string, example: 'oil_level' }
|
||||
label: { type: string, example: 'Check oil level' }
|
||||
required: { type: boolean, example: true }
|
||||
vendor_id: { type: integer, nullable: true }
|
||||
po_id: { type: integer, nullable: true }
|
||||
grn_id: { type: integer, nullable: true }
|
||||
grn_item_id: { type: integer, nullable: true }
|
||||
commencement_date: { type: string, format: date, nullable: true, description: 'Asset usage start date — used for depreciation (falls back to purchase_date)' }
|
||||
purchase_date: { type: string, format: date, nullable: true }
|
||||
purchase_cost: { type: number, example: 850000 }
|
||||
useful_life_years: { type: integer, example: 10 }
|
||||
@ -52,10 +66,23 @@ components:
|
||||
department_id: { type: integer, nullable: true }
|
||||
location_detail: { type: string }
|
||||
assigned_to_user_id: { type: integer, nullable: true }
|
||||
maintenance_incharge_user_id: { type: integer, nullable: true }
|
||||
maintenance_frequency_in_days: { type: integer, nullable: true }
|
||||
maintenance_checklist_json:
|
||||
type: array
|
||||
nullable: true
|
||||
items:
|
||||
type: object
|
||||
required: [key, label]
|
||||
properties:
|
||||
key: { type: string }
|
||||
label: { type: string }
|
||||
required: { type: boolean }
|
||||
vendor_id: { type: integer, nullable: true }
|
||||
po_id: { type: integer, nullable: true }
|
||||
grn_id: { type: integer, nullable: true }
|
||||
grn_item_id: { type: integer, nullable: true }
|
||||
commencement_date: { type: string, format: date, nullable: true }
|
||||
purchase_date: { type: string, format: date, nullable: true }
|
||||
purchase_cost: { type: number }
|
||||
useful_life_years: { type: integer }
|
||||
@ -80,8 +107,27 @@ components:
|
||||
purchase_cost: { type: number, example: 850000 }
|
||||
salvage_value: { type: number, example: 50000 }
|
||||
useful_life_years: { type: integer, example: 10 }
|
||||
commencement_date: { type: string, format: date, description: 'Preferred depreciation start date' }
|
||||
purchase_date: { type: string, format: date }
|
||||
as_of_date: { type: string, format: date }
|
||||
MaintenanceLogCreateBody:
|
||||
type: object
|
||||
required: [performed_date, checklist_json]
|
||||
properties:
|
||||
performed_date: { type: string, format: date }
|
||||
next_due_date: { type: string, format: date, nullable: true, description: 'Optional override; defaults to performed_date + frequency' }
|
||||
remarks: { type: string }
|
||||
checklist_json:
|
||||
type: array
|
||||
minItems: 1
|
||||
items:
|
||||
type: object
|
||||
required: [key, status]
|
||||
properties:
|
||||
key: { type: string, example: 'oil_level' }
|
||||
label: { type: string, example: 'Check oil level' }
|
||||
status: { type: string, enum: [OK, NOT_OK, NA] }
|
||||
remarks: { type: string, nullable: true }
|
||||
TransferAssetBody:
|
||||
type: object
|
||||
required: [transfer_date]
|
||||
@ -420,6 +466,26 @@ paths:
|
||||
'200':
|
||||
description: Service alerts fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/maintenance/my:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: List assets assigned to me as maintenance incharge
|
||||
description: |
|
||||
Returns assets where `maintenance_incharge_user_id` equals the logged-in user.
|
||||
Use `due_only=true` to show only assets whose next checklist is due/overdue.
|
||||
Each asset includes a `maintenance` summary (`is_due`, `next_due_date`, `checklist`, etc.).
|
||||
parameters:
|
||||
- { name: page, in: query, schema: { type: integer } }
|
||||
- { name: limit, in: query, schema: { type: integer } }
|
||||
- { name: search, in: query, schema: { type: string } }
|
||||
- { name: due_only, in: query, schema: { type: boolean } }
|
||||
- { name: status, in: query, schema: { type: string } }
|
||||
- { name: location_id, in: query, schema: { type: integer } }
|
||||
- { name: is_active, in: query, schema: { type: boolean } }
|
||||
responses:
|
||||
'200':
|
||||
description: Maintenance assets fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/export:
|
||||
get:
|
||||
tags: [Assets]
|
||||
@ -435,6 +501,8 @@ paths:
|
||||
- { name: item_subcategory_id, in: query, schema: { type: integer } }
|
||||
- { name: location_id, in: query, schema: { type: integer } }
|
||||
- { name: department_id, in: query, schema: { type: integer } }
|
||||
- { name: maintenance_incharge_user_id, in: query, schema: { type: integer } }
|
||||
- { name: due_only, in: query, schema: { type: boolean }, description: 'When true, only assets whose maintenance is due/overdue' }
|
||||
- { name: is_active, in: query, schema: { type: boolean } }
|
||||
responses:
|
||||
'200':
|
||||
@ -456,6 +524,8 @@ paths:
|
||||
- { name: item_subcategory_id, in: query, schema: { type: integer } }
|
||||
- { name: location_id, in: query, schema: { type: integer } }
|
||||
- { name: department_id, in: query, schema: { type: integer } }
|
||||
- { name: maintenance_incharge_user_id, in: query, schema: { type: integer } }
|
||||
- { name: due_only, in: query, schema: { type: boolean } }
|
||||
- { name: is_active, in: query, schema: { type: boolean } }
|
||||
responses:
|
||||
'200':
|
||||
@ -527,6 +597,58 @@ paths:
|
||||
'200':
|
||||
description: Transfer history fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{id}/maintenance-logs:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: List maintenance checklist logs for an asset
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: page, in: query, schema: { type: integer } }
|
||||
- { name: limit, in: query, schema: { type: integer } }
|
||||
responses:
|
||||
'200':
|
||||
description: Maintenance logs fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
post:
|
||||
tags: [Assets]
|
||||
summary: Submit a maintenance checklist (incharge only)
|
||||
description: |
|
||||
Only the asset's `maintenance_incharge_user_id` may submit.
|
||||
Required checklist keys from `maintenance_checklist_json` must be present with status OK/NOT_OK/NA.
|
||||
`next_due_date` defaults to performed_date + maintenance_frequency_in_days.
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: { $ref: '#/components/schemas/MaintenanceLogCreateBody' }
|
||||
responses:
|
||||
'201':
|
||||
description: Checklist submitted
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
'403': { description: Not the maintenance incharge }
|
||||
/assets/{id}/maintenance-logs/{logId}:
|
||||
get:
|
||||
tags: [Assets]
|
||||
summary: Get a maintenance log by id
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: logId, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Maintenance log fetched
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
delete:
|
||||
tags: [Assets]
|
||||
summary: Soft-delete a maintenance log (incharge only)
|
||||
parameters:
|
||||
- { name: id, in: path, required: true, schema: { type: string } }
|
||||
- { name: logId, in: path, required: true, schema: { type: string } }
|
||||
responses:
|
||||
'200':
|
||||
description: Maintenance log deleted
|
||||
content: { application/json: { schema: { $ref: '#/components/schemas/ApiResponse' } } }
|
||||
/assets/{assetId}/attachments:
|
||||
get:
|
||||
tags: [Assets]
|
||||
|
||||
@ -170,6 +170,11 @@ components:
|
||||
hsn_code_id: { type: integer, nullable: true, example: 1 }
|
||||
gst_rate_id: { type: integer, nullable: true, example: 4 }
|
||||
is_asset_item: { type: boolean, example: false }
|
||||
tags:
|
||||
type: array
|
||||
nullable: true
|
||||
items: { type: string, example: 'critical' }
|
||||
description: 'Free-form text tags (max 50 tags, each max 50 chars)'
|
||||
description: { type: string, example: 'Raw material' }
|
||||
specification: { type: string, example: '96% purity' }
|
||||
min_order_qty: { type: number, nullable: true, example: 100.0 }
|
||||
@ -186,6 +191,10 @@ components:
|
||||
hsn_code_id: { type: integer, nullable: true, example: 1 }
|
||||
gst_rate_id: { type: integer, nullable: true, example: 4 }
|
||||
is_asset_item: { type: boolean, example: false }
|
||||
tags:
|
||||
type: array
|
||||
nullable: true
|
||||
items: { type: string, example: 'critical' }
|
||||
description: { type: string, example: 'Raw material' }
|
||||
specification: { type: string, example: '96% purity' }
|
||||
min_order_qty: { type: number, nullable: true, example: 100.0 }
|
||||
@ -1372,6 +1381,17 @@ paths:
|
||||
description: Deleted
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
"404": { description: Not found }
|
||||
/masters/locations/states:
|
||||
get:
|
||||
tags: [Locations]
|
||||
summary: List Indian state / UT options for location.state
|
||||
description: |
|
||||
Same values as `GET /vendors/source-of-supply`.
|
||||
Use for plant/location state dropdowns so GST place-of-supply comparisons stay consistent.
|
||||
responses:
|
||||
"200":
|
||||
description: State options fetched
|
||||
content: { application/json: { schema: { $ref: "#/components/schemas/ApiResponse" } } }
|
||||
/masters/locations:
|
||||
get:
|
||||
tags: [Locations]
|
||||
|
||||
299
src/modules/assets/assets-maintenance.service.js
Normal file
299
src/modules/assets/assets-maintenance.service.js
Normal file
@ -0,0 +1,299 @@
|
||||
const prisma = require('../../config/prisma');
|
||||
const ApiError = require('../../utils/ApiError');
|
||||
const auditLog = require('../../utils/auditLog');
|
||||
const { getPagination, isDropdownCall } = require('../../utils/pagination');
|
||||
const { MAINTENANCE_CHECKLIST_STATUSES } = require('./assets.constants');
|
||||
const { toDateOnly, getAssetOrThrow } = require('./assets.helpers');
|
||||
const { sanitizeAsset } = require('./assets.service');
|
||||
|
||||
const logInclude = {
|
||||
users_asset_maintenance_logs_performed_byTousers: {
|
||||
select: { id: true, full_name: true, employee_code: true },
|
||||
},
|
||||
users_asset_maintenance_logs_created_byTousers: {
|
||||
select: { id: true, full_name: true },
|
||||
},
|
||||
};
|
||||
|
||||
const assetListInclude = {
|
||||
item_categories: { select: { id: true, code: true, name: true } },
|
||||
item_subcategories: { select: { id: true, code: true, name: true } },
|
||||
location: { select: { id: true, code: true, name: true, type: true } },
|
||||
departments: { select: { id: true, name: true } },
|
||||
users_assets_assigned_to_user_idTousers: {
|
||||
select: { id: true, full_name: true, employee_code: true },
|
||||
},
|
||||
users_assets_maintenance_incharge_idTousers: {
|
||||
select: { id: true, full_name: true, employee_code: true },
|
||||
},
|
||||
asset_maintenance_logs: {
|
||||
where: { deleted_at: null },
|
||||
orderBy: [{ performed_date: 'desc' }, { created_at: 'desc' }],
|
||||
take: 1,
|
||||
select: { id: true, performed_date: true, next_due_date: true },
|
||||
},
|
||||
};
|
||||
|
||||
const sanitizeLog = (row) => {
|
||||
if (!row) return null;
|
||||
const {
|
||||
users_asset_maintenance_logs_performed_byTousers,
|
||||
users_asset_maintenance_logs_created_byTousers,
|
||||
...rest
|
||||
} = row;
|
||||
return {
|
||||
...rest,
|
||||
performed_by_user: users_asset_maintenance_logs_performed_byTousers || null,
|
||||
created_by_user: users_asset_maintenance_logs_created_byTousers || null,
|
||||
};
|
||||
};
|
||||
|
||||
const startOfUtcDay = (value) => {
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return null;
|
||||
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||
};
|
||||
|
||||
const addDays = (value, days) => {
|
||||
const base = startOfUtcDay(value);
|
||||
if (!base || days === null || days === undefined) return null;
|
||||
base.setUTCDate(base.getUTCDate() + Number(days));
|
||||
return base;
|
||||
};
|
||||
|
||||
const normalizeChecklistTemplate = (template) => {
|
||||
if (template === null || template === undefined) return null;
|
||||
if (!Array.isArray(template)) {
|
||||
throw new ApiError(422, 'maintenance_checklist_json must be an array');
|
||||
}
|
||||
const keys = new Set();
|
||||
return template.map((item, index) => {
|
||||
const key = String(item.key || '').trim();
|
||||
const label = String(item.label || '').trim();
|
||||
if (!key) throw new ApiError(422, `Checklist item ${index + 1}: key is required`);
|
||||
if (!label) throw new ApiError(422, `Checklist item ${index + 1}: label is required`);
|
||||
if (keys.has(key)) throw new ApiError(422, `Duplicate checklist key: ${key}`);
|
||||
keys.add(key);
|
||||
return {
|
||||
key,
|
||||
label,
|
||||
required: item.required !== false,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const validateChecklistResults = (template, results) => {
|
||||
if (!Array.isArray(results) || !results.length) {
|
||||
throw new ApiError(422, 'checklist_json is required and must contain at least one item');
|
||||
}
|
||||
|
||||
const templateItems = Array.isArray(template) ? template : [];
|
||||
const templateMap = new Map(templateItems.map((item) => [item.key, item]));
|
||||
const resultKeys = new Set();
|
||||
const normalized = [];
|
||||
|
||||
for (const row of results) {
|
||||
const key = String(row.key || '').trim();
|
||||
if (!key) throw new ApiError(422, 'Each checklist result requires a key');
|
||||
if (resultKeys.has(key)) throw new ApiError(422, `Duplicate checklist result key: ${key}`);
|
||||
resultKeys.add(key);
|
||||
|
||||
if (!MAINTENANCE_CHECKLIST_STATUSES.includes(row.status)) {
|
||||
throw new ApiError(422, `Invalid checklist status for ${key}`);
|
||||
}
|
||||
|
||||
const templateItem = templateMap.get(key);
|
||||
normalized.push({
|
||||
key,
|
||||
label: row.label || templateItem?.label || key,
|
||||
status: row.status,
|
||||
remarks: row.remarks || null,
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of templateItems) {
|
||||
if (item.required !== false && !resultKeys.has(item.key)) {
|
||||
throw new ApiError(422, `Required checklist item missing: ${item.key}`);
|
||||
}
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
const assertInchargeOrThrow = (asset, userId) => {
|
||||
if (!userId) throw new ApiError(403, 'Authentication required');
|
||||
if (
|
||||
!asset.maintenance_incharge_user_id ||
|
||||
asset.maintenance_incharge_user_id.toString() !== BigInt(userId).toString()
|
||||
) {
|
||||
throw new ApiError(403, 'Only the maintenance incharge can perform this action');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Assets assigned to the logged-in user as maintenance incharge.
|
||||
* Optional due_only=true returns only assets whose next maintenance is due/overdue.
|
||||
*/
|
||||
const listMyMaintenanceAssets = async (query, userId) => {
|
||||
if (!userId) throw new ApiError(403, 'Authentication required');
|
||||
|
||||
const where = {
|
||||
deleted_at: null,
|
||||
maintenance_incharge_user_id: BigInt(userId),
|
||||
...(query.status ? { status: query.status } : {}),
|
||||
...(query.location_id ? { location_id: BigInt(query.location_id) } : {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : { is_active: true }),
|
||||
...(query.search
|
||||
? {
|
||||
OR: [
|
||||
{ asset_code: { contains: query.search, mode: 'insensitive' } },
|
||||
{ asset_name: { contains: query.search, mode: 'insensitive' } },
|
||||
{ serial_number: { contains: query.search, mode: 'insensitive' } },
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
|
||||
const rows = await prisma.assets.findMany({
|
||||
where,
|
||||
include: assetListInclude,
|
||||
orderBy: { asset_code: 'asc' },
|
||||
});
|
||||
|
||||
let data = rows.map(sanitizeAsset);
|
||||
|
||||
if (query.due_only === true) {
|
||||
data = data.filter((row) => row.maintenance?.is_due === true);
|
||||
}
|
||||
|
||||
if (isDropdownCall(query)) {
|
||||
return { data, meta: { page: 1, limit: data.length, total: data.length, dropdown: true } };
|
||||
}
|
||||
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const total = data.length;
|
||||
const paged = data.slice(skip, skip + limit);
|
||||
return { data: paged, meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const listMaintenanceLogs = async (assetId, query) => {
|
||||
await getAssetOrThrow(assetId);
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
const where = { asset_id: BigInt(assetId), deleted_at: null };
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
prisma.asset_maintenance_logs.findMany({
|
||||
where,
|
||||
include: logInclude,
|
||||
orderBy: [{ performed_date: 'desc' }, { created_at: 'desc' }],
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
prisma.asset_maintenance_logs.count({ where }),
|
||||
]);
|
||||
|
||||
return { data: rows.map(sanitizeLog), meta: { page, limit, total } };
|
||||
};
|
||||
|
||||
const getMaintenanceLog = async (assetId, logId) => {
|
||||
await getAssetOrThrow(assetId);
|
||||
const row = await prisma.asset_maintenance_logs.findFirst({
|
||||
where: {
|
||||
id: BigInt(logId),
|
||||
asset_id: BigInt(assetId),
|
||||
deleted_at: null,
|
||||
},
|
||||
include: logInclude,
|
||||
});
|
||||
if (!row) throw new ApiError(404, 'Maintenance log not found');
|
||||
return sanitizeLog(row);
|
||||
};
|
||||
|
||||
const createMaintenanceLog = async (assetId, payload, userId, requestId) => {
|
||||
const asset = await getAssetOrThrow(assetId);
|
||||
assertInchargeOrThrow(asset, userId);
|
||||
|
||||
if (!asset.maintenance_frequency_in_days) {
|
||||
throw new ApiError(422, 'Asset has no maintenance_frequency_in_days configured');
|
||||
}
|
||||
|
||||
const template = normalizeChecklistTemplate(asset.maintenance_checklist_json);
|
||||
if (!template || !template.length) {
|
||||
throw new ApiError(422, 'Asset has no maintenance_checklist_json configured');
|
||||
}
|
||||
|
||||
const checklist = validateChecklistResults(template, payload.checklist_json);
|
||||
const performedDate = toDateOnly(payload.performed_date);
|
||||
const nextDueDate = payload.next_due_date
|
||||
? toDateOnly(payload.next_due_date)
|
||||
: addDays(performedDate, asset.maintenance_frequency_in_days);
|
||||
|
||||
const created = await prisma.asset_maintenance_logs.create({
|
||||
data: {
|
||||
asset_id: BigInt(assetId),
|
||||
performed_by: userId ? BigInt(userId) : null,
|
||||
performed_date: performedDate,
|
||||
next_due_date: nextDueDate,
|
||||
checklist_json: checklist,
|
||||
remarks: payload.remarks || null,
|
||||
created_by: userId ? BigInt(userId) : null,
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
include: logInclude,
|
||||
});
|
||||
|
||||
const sanitized = sanitizeLog(created);
|
||||
await auditLog({
|
||||
tableName: 'asset_maintenance_logs',
|
||||
recordId: created.id,
|
||||
action: 'CREATE',
|
||||
oldValue: null,
|
||||
newValue: sanitized,
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
|
||||
return sanitized;
|
||||
};
|
||||
|
||||
const deleteMaintenanceLog = async (assetId, logId, userId, requestId) => {
|
||||
const asset = await getAssetOrThrow(assetId);
|
||||
assertInchargeOrThrow(asset, userId);
|
||||
|
||||
const existing = await prisma.asset_maintenance_logs.findFirst({
|
||||
where: {
|
||||
id: BigInt(logId),
|
||||
asset_id: BigInt(assetId),
|
||||
deleted_at: null,
|
||||
},
|
||||
include: logInclude,
|
||||
});
|
||||
if (!existing) throw new ApiError(404, 'Maintenance log not found');
|
||||
|
||||
const deleted = await prisma.asset_maintenance_logs.update({
|
||||
where: { id: existing.id },
|
||||
data: {
|
||||
deleted_at: new Date(),
|
||||
updated_by: userId ? BigInt(userId) : null,
|
||||
},
|
||||
});
|
||||
|
||||
await auditLog({
|
||||
tableName: 'asset_maintenance_logs',
|
||||
recordId: logId,
|
||||
action: 'DELETE',
|
||||
oldValue: sanitizeLog(existing),
|
||||
newValue: deleted,
|
||||
userId,
|
||||
requestId,
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
listMyMaintenanceAssets,
|
||||
listMaintenanceLogs,
|
||||
getMaintenanceLog,
|
||||
createMaintenanceLog,
|
||||
deleteMaintenanceLog,
|
||||
normalizeChecklistTemplate,
|
||||
};
|
||||
@ -63,6 +63,12 @@ const VISIT_CONDITION_AFTER_OPTIONS = toOptions([
|
||||
['NEEDS_REPLACEMENT', 'Needs Replacement'],
|
||||
]);
|
||||
|
||||
const MAINTENANCE_CHECKLIST_STATUS_OPTIONS = toOptions([
|
||||
['OK', 'OK'],
|
||||
['NOT_OK', 'Not OK'],
|
||||
['NA', 'Not Applicable'],
|
||||
]);
|
||||
|
||||
const INSURANCE_POLICY_TYPE_OPTIONS = toOptions([
|
||||
['FIRE_AND_ALLIED', 'Fire and Allied'],
|
||||
['MACHINERY_BREAKDOWN', 'Machinery Breakdown'],
|
||||
@ -94,6 +100,7 @@ const VISIT_TYPES = VISIT_TYPE_OPTIONS.map((o) => o.value);
|
||||
const VISIT_STATUSES = VISIT_STATUS_OPTIONS.map((o) => o.value);
|
||||
const VISIT_CONDITION_AFTER = VISIT_CONDITION_AFTER_OPTIONS.map((o) => o.value);
|
||||
const INSURANCE_POLICY_TYPES = INSURANCE_POLICY_TYPE_OPTIONS.map((o) => o.value);
|
||||
const MAINTENANCE_CHECKLIST_STATUSES = MAINTENANCE_CHECKLIST_STATUS_OPTIONS.map((o) => o.value);
|
||||
|
||||
const ALERT_TYPES = ['AMC', 'INSURANCE', 'WARRANTY'];
|
||||
const ALERT_LEVELS = ['EXPIRED', 'CRITICAL', 'WARNING', 'INFO'];
|
||||
@ -109,6 +116,7 @@ const getAssetDropdownOptions = () => ({
|
||||
service_frequencies: SERVICE_FREQUENCY_OPTIONS,
|
||||
visit_statuses: VISIT_STATUS_OPTIONS,
|
||||
visit_conditions_after: VISIT_CONDITION_AFTER_OPTIONS,
|
||||
maintenance_checklist_statuses: MAINTENANCE_CHECKLIST_STATUS_OPTIONS,
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
@ -124,6 +132,7 @@ module.exports = {
|
||||
VISIT_STATUSES,
|
||||
VISIT_CONDITION_AFTER,
|
||||
INSURANCE_POLICY_TYPES,
|
||||
MAINTENANCE_CHECKLIST_STATUSES,
|
||||
ALERT_TYPES,
|
||||
ALERT_LEVELS,
|
||||
SERVICE_ALERT_STATUSES,
|
||||
@ -136,5 +145,6 @@ module.exports = {
|
||||
VISIT_STATUS_OPTIONS,
|
||||
VISIT_CONDITION_AFTER_OPTIONS,
|
||||
INSURANCE_POLICY_TYPE_OPTIONS,
|
||||
MAINTENANCE_CHECKLIST_STATUS_OPTIONS,
|
||||
getAssetDropdownOptions,
|
||||
};
|
||||
|
||||
@ -5,6 +5,7 @@ const amcService = require('./assets-amc.service');
|
||||
const visitsService = require('./assets-service-visits.service');
|
||||
const insuranceService = require('./assets-insurance.service');
|
||||
const alertsService = require('./assets-alerts.service');
|
||||
const maintenanceService = require('./assets-maintenance.service');
|
||||
const attachmentService = require('./assets.attachments.service');
|
||||
const path = require('path');
|
||||
|
||||
@ -280,6 +281,41 @@ const removeAttachment = asyncHandler(async (req, res) => {
|
||||
res.json(new ApiResponse(200, null, 'Asset attachment deleted successfully'));
|
||||
});
|
||||
|
||||
const listMyMaintenance = asyncHandler(async (req, res) => {
|
||||
const result = await maintenanceService.listMyMaintenanceAssets(req.query, req.user?.id);
|
||||
res.json(new ApiResponse(200, result.data, 'Maintenance assets fetched', result.meta));
|
||||
});
|
||||
|
||||
const listMaintenanceLogs = asyncHandler(async (req, res) => {
|
||||
const result = await maintenanceService.listMaintenanceLogs(req.params.id, req.query);
|
||||
res.json(new ApiResponse(200, result.data, 'Maintenance logs fetched', result.meta));
|
||||
});
|
||||
|
||||
const getMaintenanceLog = asyncHandler(async (req, res) => {
|
||||
const data = await maintenanceService.getMaintenanceLog(req.params.id, req.params.logId);
|
||||
res.json(new ApiResponse(200, data, 'Maintenance log fetched'));
|
||||
});
|
||||
|
||||
const createMaintenanceLog = asyncHandler(async (req, res) => {
|
||||
const data = await maintenanceService.createMaintenanceLog(
|
||||
req.params.id,
|
||||
req.body,
|
||||
req.user?.id,
|
||||
req.id
|
||||
);
|
||||
res.status(201).json(new ApiResponse(201, data, 'Maintenance checklist submitted successfully'));
|
||||
});
|
||||
|
||||
const removeMaintenanceLog = asyncHandler(async (req, res) => {
|
||||
await maintenanceService.deleteMaintenanceLog(
|
||||
req.params.id,
|
||||
req.params.logId,
|
||||
req.user?.id,
|
||||
req.id
|
||||
);
|
||||
res.json(new ApiResponse(200, null, 'Maintenance log deleted successfully'));
|
||||
});
|
||||
|
||||
module.exports = {
|
||||
create,
|
||||
list,
|
||||
@ -323,4 +359,9 @@ module.exports = {
|
||||
getAttachment,
|
||||
downloadAttachment,
|
||||
removeAttachment,
|
||||
listMyMaintenance,
|
||||
listMaintenanceLogs,
|
||||
getMaintenanceLog,
|
||||
createMaintenanceLog,
|
||||
removeMaintenanceLog,
|
||||
};
|
||||
|
||||
@ -66,6 +66,7 @@ const calculateDepreciation = ({
|
||||
purchase_cost: cost,
|
||||
salvage_value: salvage,
|
||||
useful_life_years: lifeYears,
|
||||
commencement_date: commencementDate,
|
||||
purchase_date: purchaseDate,
|
||||
as_of_date: asOfDate,
|
||||
} = {}) => {
|
||||
@ -79,7 +80,10 @@ const calculateDepreciation = ({
|
||||
useful_life_years: lifeYears,
|
||||
});
|
||||
|
||||
const elapsed = yearsElapsed(purchaseDate, asOfDate || new Date());
|
||||
// Depreciation starts from the asset's usage start date (commencement_date).
|
||||
// Fall back to purchase_date for assets created before this field existed.
|
||||
const depreciationStartDate = commencementDate || purchaseDate;
|
||||
const elapsed = yearsElapsed(depreciationStartDate, asOfDate || new Date());
|
||||
const cappedYears =
|
||||
lifeYears && Number(lifeYears) > 0 ? Math.min(elapsed, Number(lifeYears)) : elapsed;
|
||||
|
||||
@ -129,6 +133,7 @@ const calculateDepreciation = ({
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
useful_life_years: lifeYears ?? null,
|
||||
depreciation_start_date: depreciationStartDate || null,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@ -23,6 +23,9 @@ const {
|
||||
depreciationCalculateSchema,
|
||||
expiryAlertsQuerySchema,
|
||||
serviceAlertsQuerySchema,
|
||||
myMaintenanceAssetsQuerySchema,
|
||||
createMaintenanceLogSchema,
|
||||
listMaintenanceLogsQuerySchema,
|
||||
} = require('./assets.validation');
|
||||
|
||||
const router = express.Router();
|
||||
@ -35,6 +38,12 @@ router.get(
|
||||
validate(exportAssetsQuerySchema, 'query'),
|
||||
controller.exportCsv
|
||||
);
|
||||
router.get(
|
||||
'/maintenance/my',
|
||||
authorize('ASSET', 'view'),
|
||||
validate(myMaintenanceAssetsQuerySchema, 'query'),
|
||||
controller.listMyMaintenance
|
||||
);
|
||||
router.get(
|
||||
'/',
|
||||
authorize('ASSET', 'view'),
|
||||
@ -182,6 +191,29 @@ router.post(
|
||||
controller.transfer
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:id/maintenance-logs',
|
||||
authorize('ASSET', 'view'),
|
||||
validate(listMaintenanceLogsQuerySchema, 'query'),
|
||||
controller.listMaintenanceLogs
|
||||
);
|
||||
router.post(
|
||||
'/:id/maintenance-logs',
|
||||
authorize('ASSET', 'edit'),
|
||||
validate(createMaintenanceLogSchema),
|
||||
controller.createMaintenanceLog
|
||||
);
|
||||
router.get(
|
||||
'/:id/maintenance-logs/:logId',
|
||||
authorize('ASSET', 'view'),
|
||||
controller.getMaintenanceLog
|
||||
);
|
||||
router.delete(
|
||||
'/:id/maintenance-logs/:logId',
|
||||
authorize('ASSET', 'edit'),
|
||||
controller.removeMaintenanceLog
|
||||
);
|
||||
|
||||
router.get('/:id', authorize('ASSET', 'view'), controller.getOne);
|
||||
router.put('/:id', authorize('ASSET', 'edit'), validate(updateAssetSchema), controller.update);
|
||||
router.delete('/:id', authorize('ASSET', 'delete'), controller.remove);
|
||||
|
||||
@ -32,6 +32,15 @@ const assetInclude = {
|
||||
users_assets_assigned_to_user_idTousers: {
|
||||
select: { id: true, full_name: true, employee_code: true },
|
||||
},
|
||||
users_assets_maintenance_incharge_idTousers: {
|
||||
select: { id: true, full_name: true, employee_code: true },
|
||||
},
|
||||
asset_maintenance_logs: {
|
||||
where: { deleted_at: null },
|
||||
orderBy: [{ performed_date: 'desc' }, { created_at: 'desc' }],
|
||||
take: 1,
|
||||
select: { id: true, performed_date: true, next_due_date: true },
|
||||
},
|
||||
vendors_assets_vendor_idTovendors: { select: { id: true, vendor_code: true, vendor_name: true } },
|
||||
purchase_orders: { select: { id: true, po_number: true } },
|
||||
grn: { select: { id: true, grn_number: true } },
|
||||
@ -61,6 +70,52 @@ const transferInclude = {
|
||||
|
||||
const assetSeriesCode = (categoryCode) => `ASSET_${categoryCode}`;
|
||||
|
||||
const startOfUtcDay = (value) => {
|
||||
const d = new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return null;
|
||||
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
|
||||
};
|
||||
|
||||
const addDays = (value, days) => {
|
||||
const base = startOfUtcDay(value);
|
||||
if (!base || days === null || days === undefined) return null;
|
||||
base.setUTCDate(base.getUTCDate() + Number(days));
|
||||
return base;
|
||||
};
|
||||
|
||||
// Derive maintenance due state from the configured frequency, the latest log and
|
||||
// the asset's usage start date (commencement_date, falling back to purchase_date).
|
||||
const buildMaintenanceSummary = (rest, logs) => {
|
||||
const frequency =
|
||||
rest.maintenance_frequency_in_days !== null && rest.maintenance_frequency_in_days !== undefined
|
||||
? Number(rest.maintenance_frequency_in_days)
|
||||
: null;
|
||||
const lastLog = Array.isArray(logs) && logs.length ? logs[0] : null;
|
||||
const lastMaintenanceDate = lastLog?.performed_date ?? null;
|
||||
const baseline = lastMaintenanceDate || rest.commencement_date || rest.purchase_date || null;
|
||||
|
||||
let nextDueDate = lastLog?.next_due_date ?? null;
|
||||
if (!nextDueDate && frequency && baseline) {
|
||||
nextDueDate = addDays(baseline, frequency);
|
||||
}
|
||||
|
||||
const today = startOfUtcDay(new Date());
|
||||
const isDue = Boolean(frequency && nextDueDate && new Date(nextDueDate) <= today);
|
||||
const daysUntilDue =
|
||||
nextDueDate && today
|
||||
? Math.round((new Date(nextDueDate).getTime() - today.getTime()) / 86400000)
|
||||
: null;
|
||||
|
||||
return {
|
||||
frequency_in_days: frequency,
|
||||
checklist: rest.maintenance_checklist_json ?? null,
|
||||
last_maintenance_date: lastMaintenanceDate,
|
||||
next_due_date: nextDueDate,
|
||||
is_due: isDue,
|
||||
days_until_due: daysUntilDue,
|
||||
};
|
||||
};
|
||||
|
||||
const sanitizeAsset = (asset) => {
|
||||
if (!asset) return null;
|
||||
const {
|
||||
@ -69,6 +124,8 @@ const sanitizeAsset = (asset) => {
|
||||
location,
|
||||
departments,
|
||||
users_assets_assigned_to_user_idTousers,
|
||||
users_assets_maintenance_incharge_idTousers,
|
||||
asset_maintenance_logs,
|
||||
vendors_assets_vendor_idTovendors,
|
||||
purchase_orders,
|
||||
grn,
|
||||
@ -101,6 +158,8 @@ const sanitizeAsset = (asset) => {
|
||||
location: location || null,
|
||||
department: departments || null,
|
||||
assigned_to_user: users_assets_assigned_to_user_idTousers || null,
|
||||
maintenance_incharge_user: users_assets_maintenance_incharge_idTousers || null,
|
||||
maintenance: buildMaintenanceSummary(rest, asset_maintenance_logs),
|
||||
vendor: vendors_assets_vendor_idTovendors || null,
|
||||
purchase_order: purchase_orders || null,
|
||||
grn: grn || null,
|
||||
@ -113,12 +172,15 @@ const sanitizeAsset = (asset) => {
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
useful_life_years: rest.useful_life_years,
|
||||
commencement_date: rest.commencement_date,
|
||||
purchase_date: rest.purchase_date,
|
||||
}),
|
||||
item_categories: undefined,
|
||||
item_subcategories: undefined,
|
||||
departments: undefined,
|
||||
users_assets_assigned_to_user_idTousers: undefined,
|
||||
users_assets_maintenance_incharge_idTousers: undefined,
|
||||
asset_maintenance_logs: undefined,
|
||||
vendors_assets_vendor_idTovendors: undefined,
|
||||
purchase_orders: undefined,
|
||||
users_assets_created_byTousers: undefined,
|
||||
@ -187,6 +249,33 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
await assertReference('departments', payload.department_id, 'department_id');
|
||||
if (payload.assigned_to_user_id)
|
||||
await assertReference('users', payload.assigned_to_user_id, 'assigned_to_user_id');
|
||||
if (payload.maintenance_incharge_user_id)
|
||||
await assertReference(
|
||||
'users',
|
||||
payload.maintenance_incharge_user_id,
|
||||
'maintenance_incharge_user_id'
|
||||
);
|
||||
|
||||
let checklistJson = null;
|
||||
if (payload.maintenance_checklist_json !== undefined) {
|
||||
if (payload.maintenance_checklist_json === null) {
|
||||
checklistJson = null;
|
||||
} else if (!Array.isArray(payload.maintenance_checklist_json)) {
|
||||
throw new ApiError(422, 'maintenance_checklist_json must be an array');
|
||||
} else {
|
||||
const keys = new Set();
|
||||
checklistJson = payload.maintenance_checklist_json.map((item, index) => {
|
||||
const key = String(item.key || '').trim();
|
||||
const label = String(item.label || '').trim();
|
||||
if (!key) throw new ApiError(422, `Checklist item ${index + 1}: key is required`);
|
||||
if (!label) throw new ApiError(422, `Checklist item ${index + 1}: label is required`);
|
||||
if (keys.has(key)) throw new ApiError(422, `Duplicate checklist key: ${key}`);
|
||||
keys.add(key);
|
||||
return { key, label, required: item.required !== false };
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (payload.vendor_id) await assertReference('vendors', payload.vendor_id, 'vendor_id');
|
||||
if (payload.po_id) await assertReference('purchase_orders', payload.po_id, 'po_id');
|
||||
if (payload.grn_id) await assertReference('grn', payload.grn_id, 'grn_id');
|
||||
@ -234,10 +323,22 @@ const normalizeAssetPayload = async (payload, { isCreate = false } = {}) => {
|
||||
department_id: payload.department_id ? BigInt(payload.department_id) : null,
|
||||
location_detail: payload.location_detail || null,
|
||||
assigned_to_user_id: payload.assigned_to_user_id ? BigInt(payload.assigned_to_user_id) : null,
|
||||
maintenance_incharge_user_id: payload.maintenance_incharge_user_id
|
||||
? BigInt(payload.maintenance_incharge_user_id)
|
||||
: null,
|
||||
maintenance_frequency_in_days:
|
||||
payload.maintenance_frequency_in_days !== undefined &&
|
||||
payload.maintenance_frequency_in_days !== null &&
|
||||
payload.maintenance_frequency_in_days !== ''
|
||||
? Number(payload.maintenance_frequency_in_days)
|
||||
: null,
|
||||
maintenance_checklist_json:
|
||||
payload.maintenance_checklist_json !== undefined ? checklistJson : null,
|
||||
vendor_id: payload.vendor_id ? BigInt(payload.vendor_id) : null,
|
||||
po_id: payload.po_id ? BigInt(payload.po_id) : null,
|
||||
grn_id: payload.grn_id ? BigInt(payload.grn_id) : null,
|
||||
grn_item_id: payload.grn_item_id ? BigInt(payload.grn_item_id) : null,
|
||||
commencement_date: payload.commencement_date ? toDateOnly(payload.commencement_date) : null,
|
||||
purchase_date: payload.purchase_date ? toDateOnly(payload.purchase_date) : null,
|
||||
purchase_cost: purchaseCost,
|
||||
useful_life_years: usefulLifeYears,
|
||||
@ -307,6 +408,9 @@ const buildAssetsWhere = (query) => ({
|
||||
: {}),
|
||||
...(query.location_id ? { location_id: BigInt(query.location_id) } : {}),
|
||||
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
|
||||
...(query.maintenance_incharge_user_id
|
||||
? { maintenance_incharge_user_id: BigInt(query.maintenance_incharge_user_id) }
|
||||
: {}),
|
||||
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
||||
...(query.search
|
||||
? {
|
||||
@ -330,10 +434,27 @@ const listAssets = async (query) => {
|
||||
include: assetInclude,
|
||||
orderBy: { created_at: 'desc' },
|
||||
});
|
||||
const data = rows.map(sanitizeAsset);
|
||||
let data = rows.map(sanitizeAsset);
|
||||
if (query.due_only === true) {
|
||||
data = data.filter((row) => row.maintenance?.is_due === true);
|
||||
}
|
||||
return { data, meta: { page: 1, limit: data.length, total: data.length, dropdown: true } };
|
||||
}
|
||||
|
||||
if (query.due_only === true) {
|
||||
const rows = await prisma.assets.findMany({
|
||||
where,
|
||||
include: assetInclude,
|
||||
orderBy: { created_at: 'desc' },
|
||||
});
|
||||
const due = rows.map(sanitizeAsset).filter((row) => row.maintenance?.is_due === true);
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
return {
|
||||
data: due.slice(skip, skip + limit),
|
||||
meta: { page, limit, total: due.length },
|
||||
};
|
||||
}
|
||||
|
||||
const { page, limit, skip } = getPagination(query);
|
||||
|
||||
const [rows, total] = await Promise.all([
|
||||
@ -372,9 +493,16 @@ const exportAssets = async (query) => {
|
||||
{ key: (row) => row.vendor?.vendor_name || '', header: 'Vendor' },
|
||||
{ key: (row) => row.purchase_order?.po_number || '', header: 'PO Number' },
|
||||
{ key: (row) => row.grn?.grn_number || '', header: 'GRN Number' },
|
||||
{ key: 'commencement_date', header: 'Commencement Date', type: 'date' },
|
||||
{ key: 'purchase_date', header: 'Purchase Date', type: 'date' },
|
||||
{ key: 'purchase_cost', header: 'Purchase Cost' },
|
||||
{ key: 'warranty_expiry_date', header: 'Warranty Expiry', type: 'date' },
|
||||
{
|
||||
key: (row) => row.maintenance_incharge_user?.full_name || '',
|
||||
header: 'Maintenance Incharge',
|
||||
},
|
||||
{ key: (row) => row.maintenance?.frequency_in_days ?? '', header: 'Maintenance Freq (days)' },
|
||||
{ key: (row) => row.maintenance?.next_due_date || '', header: 'Maintenance Next Due', type: 'date' },
|
||||
{ key: 'is_active', header: 'Active' },
|
||||
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
||||
],
|
||||
@ -407,10 +535,26 @@ const updateAsset = async (id, payload, userId, requestId) => {
|
||||
payload.assigned_to_user_id !== undefined
|
||||
? payload.assigned_to_user_id
|
||||
: existing.assigned_to_user_id,
|
||||
maintenance_incharge_user_id:
|
||||
payload.maintenance_incharge_user_id !== undefined
|
||||
? payload.maintenance_incharge_user_id
|
||||
: existing.maintenance_incharge_user_id,
|
||||
maintenance_frequency_in_days:
|
||||
payload.maintenance_frequency_in_days !== undefined
|
||||
? payload.maintenance_frequency_in_days
|
||||
: existing.maintenance_frequency_in_days,
|
||||
maintenance_checklist_json:
|
||||
payload.maintenance_checklist_json !== undefined
|
||||
? payload.maintenance_checklist_json
|
||||
: existing.maintenance_checklist_json,
|
||||
vendor_id: payload.vendor_id !== undefined ? payload.vendor_id : existing.vendor_id,
|
||||
po_id: payload.po_id !== undefined ? payload.po_id : existing.po_id,
|
||||
grn_id: payload.grn_id !== undefined ? payload.grn_id : existing.grn_id,
|
||||
grn_item_id: payload.grn_item_id !== undefined ? payload.grn_item_id : existing.grn_item_id,
|
||||
commencement_date:
|
||||
payload.commencement_date !== undefined
|
||||
? payload.commencement_date
|
||||
: existing.commencement_date,
|
||||
purchase_date:
|
||||
payload.purchase_date !== undefined ? payload.purchase_date : existing.purchase_date,
|
||||
purchase_cost: payload.purchase_cost ?? Number(existing.purchase_cost),
|
||||
@ -613,4 +757,5 @@ module.exports = {
|
||||
listVisitConditionsAfter,
|
||||
listAssetOptions,
|
||||
previewDepreciation,
|
||||
sanitizeAsset,
|
||||
};
|
||||
|
||||
@ -10,10 +10,26 @@ const {
|
||||
VISIT_STATUSES,
|
||||
VISIT_CONDITION_AFTER,
|
||||
INSURANCE_POLICY_TYPES,
|
||||
MAINTENANCE_CHECKLIST_STATUSES,
|
||||
ALERT_TYPES,
|
||||
SERVICE_ALERT_STATUSES,
|
||||
} = require('./assets.constants');
|
||||
|
||||
const checklistTemplateItemSchema = Joi.object({
|
||||
key: Joi.string().trim().max(50).required(),
|
||||
label: Joi.string().trim().max(200).required(),
|
||||
required: Joi.boolean().default(true),
|
||||
}).unknown(true);
|
||||
|
||||
const checklistResultItemSchema = Joi.object({
|
||||
key: Joi.string().trim().max(50).required(),
|
||||
label: Joi.string().trim().max(200).optional(),
|
||||
status: Joi.string()
|
||||
.valid(...MAINTENANCE_CHECKLIST_STATUSES)
|
||||
.required(),
|
||||
remarks: Joi.string().allow(null, '').max(500).optional(),
|
||||
}).unknown(true);
|
||||
|
||||
const assetFields = {
|
||||
asset_name: Joi.string().max(200).required(),
|
||||
item_category_id: Joi.number().integer().positive().required(),
|
||||
@ -26,10 +42,18 @@ const assetFields = {
|
||||
department_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
location_detail: Joi.string().max(200).allow(null, '').optional(),
|
||||
assigned_to_user_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
maintenance_incharge_user_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
maintenance_frequency_in_days: Joi.number().integer().min(1).max(3650).allow(null).optional(),
|
||||
maintenance_checklist_json: Joi.array()
|
||||
.items(checklistTemplateItemSchema)
|
||||
.max(100)
|
||||
.allow(null)
|
||||
.optional(),
|
||||
vendor_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
po_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
grn_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
grn_item_id: Joi.number().integer().positive().allow(null).optional(),
|
||||
commencement_date: Joi.date().iso().allow(null).optional(),
|
||||
purchase_date: Joi.date().iso().allow(null).optional(),
|
||||
purchase_cost: Joi.number().min(0).default(0),
|
||||
useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
|
||||
@ -68,10 +92,14 @@ const updateAssetSchema = Joi.object({
|
||||
department_id: assetFields.department_id,
|
||||
location_detail: assetFields.location_detail,
|
||||
assigned_to_user_id: assetFields.assigned_to_user_id,
|
||||
maintenance_incharge_user_id: assetFields.maintenance_incharge_user_id,
|
||||
maintenance_frequency_in_days: assetFields.maintenance_frequency_in_days,
|
||||
maintenance_checklist_json: assetFields.maintenance_checklist_json,
|
||||
vendor_id: assetFields.vendor_id,
|
||||
po_id: assetFields.po_id,
|
||||
grn_id: assetFields.grn_id,
|
||||
grn_item_id: assetFields.grn_item_id,
|
||||
commencement_date: assetFields.commencement_date,
|
||||
purchase_date: assetFields.purchase_date,
|
||||
purchase_cost: assetFields.purchase_cost.optional(),
|
||||
useful_life_years: assetFields.useful_life_years,
|
||||
@ -103,6 +131,8 @@ const listAssetsQuerySchema = Joi.object({
|
||||
item_subcategory_id: Joi.number().integer().positive().optional(),
|
||||
location_id: Joi.number().integer().positive().optional(),
|
||||
department_id: Joi.number().integer().positive().optional(),
|
||||
maintenance_incharge_user_id: Joi.number().integer().positive().optional(),
|
||||
due_only: Joi.boolean().optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
@ -282,10 +312,35 @@ const depreciationCalculateSchema = Joi.object({
|
||||
purchase_cost: Joi.number().min(0).default(0),
|
||||
salvage_value: Joi.number().min(0).default(0),
|
||||
useful_life_years: Joi.number().integer().min(0).allow(null).optional(),
|
||||
commencement_date: Joi.date().iso().allow(null).optional(),
|
||||
purchase_date: Joi.date().iso().allow(null).optional(),
|
||||
as_of_date: Joi.date().iso().allow(null).optional(),
|
||||
});
|
||||
|
||||
const myMaintenanceAssetsQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
search: Joi.string().allow('').optional(),
|
||||
due_only: Joi.boolean().optional(),
|
||||
status: Joi.string()
|
||||
.valid(...ASSET_STATUSES)
|
||||
.optional(),
|
||||
location_id: Joi.number().integer().positive().optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
});
|
||||
|
||||
const createMaintenanceLogSchema = Joi.object({
|
||||
performed_date: Joi.date().iso().required(),
|
||||
checklist_json: Joi.array().items(checklistResultItemSchema).min(1).required(),
|
||||
remarks: Joi.string().allow(null, '').optional(),
|
||||
next_due_date: Joi.date().iso().allow(null).optional(),
|
||||
});
|
||||
|
||||
const listMaintenanceLogsQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
});
|
||||
|
||||
const expiryAlertsQuerySchema = Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1),
|
||||
limit: Joi.number().integer().min(1).max(100).default(20),
|
||||
@ -320,6 +375,9 @@ module.exports = {
|
||||
updateInsurancePolicySchema,
|
||||
renewInsurancePolicySchema,
|
||||
depreciationCalculateSchema,
|
||||
myMaintenanceAssetsQuerySchema,
|
||||
createMaintenanceLogSchema,
|
||||
listMaintenanceLogsQuerySchema,
|
||||
expiryAlertsQuerySchema,
|
||||
serviceAlertsQuerySchema,
|
||||
};
|
||||
|
||||
@ -90,6 +90,17 @@ const normalizePayload = (payload, { isCreate = false } = {}) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'tags')) {
|
||||
if (Array.isArray(data.tags)) {
|
||||
const cleaned = data.tags
|
||||
.map((tag) => (tag === null || tag === undefined ? '' : String(tag).trim()))
|
||||
.filter((tag) => tag !== '');
|
||||
data.tags = Array.from(new Set(cleaned));
|
||||
} else if (data.tags === null) {
|
||||
data.tags = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.min_order_qty !== undefined && data.min_order_qty !== null) {
|
||||
data.min_order_qty = Number(data.min_order_qty);
|
||||
}
|
||||
@ -253,6 +264,7 @@ const exportItems = async (query) => {
|
||||
{ key: (row) => row.uom?.code || '', header: 'UOM' },
|
||||
{ key: (row) => row.hsn_code?.code || '', header: 'HSN Code' },
|
||||
{ key: (row) => (row.gst_rate ? row.gst_rate.rate_pct : ''), header: 'GST %' },
|
||||
{ key: (row) => (Array.isArray(row.tags) ? row.tags.join(', ') : ''), header: 'Tags' },
|
||||
{ key: 'is_asset_item', header: 'Is Asset Item' },
|
||||
{ key: 'is_active', header: 'Is Active' },
|
||||
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
||||
|
||||
@ -11,6 +11,7 @@ const createSchema = Joi.object({
|
||||
is_asset_item: Joi.boolean().default(false),
|
||||
description: Joi.string().allow(null, '').optional(),
|
||||
specification: Joi.string().allow(null, '').optional(),
|
||||
tags: Joi.array().items(Joi.string().trim().max(50)).max(50).allow(null).optional(),
|
||||
min_order_qty: Joi.number().precision(4).allow(null).optional(),
|
||||
reorder_level: Joi.number().precision(4).allow(null).optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
@ -26,6 +27,7 @@ const updateSchema = Joi.object({
|
||||
is_asset_item: Joi.boolean().optional(),
|
||||
description: Joi.string().allow(null, '').optional(),
|
||||
specification: Joi.string().allow(null, '').optional(),
|
||||
tags: Joi.array().items(Joi.string().trim().max(50)).max(50).allow(null).optional(),
|
||||
min_order_qty: Joi.number().precision(4).allow(null).optional(),
|
||||
reorder_level: Joi.number().precision(4).allow(null).optional(),
|
||||
is_active: Joi.boolean().optional(),
|
||||
|
||||
12
src/modules/masters/locations/locations.constants.js
Normal file
12
src/modules/masters/locations/locations.constants.js
Normal file
@ -0,0 +1,12 @@
|
||||
const {
|
||||
INDIAN_STATE_OPTIONS,
|
||||
INDIAN_STATE_VALUES,
|
||||
} = require('../../../constants/indianStates');
|
||||
|
||||
module.exports = {
|
||||
INDIAN_STATE_OPTIONS,
|
||||
INDIAN_STATE_VALUES,
|
||||
/** Alias used by location state dropdown API */
|
||||
LOCATION_STATE_OPTIONS: INDIAN_STATE_OPTIONS,
|
||||
LOCATION_STATE_VALUES: INDIAN_STATE_VALUES,
|
||||
};
|
||||
@ -34,5 +34,10 @@ const exportCsv = asyncHandler(async (req, res) => {
|
||||
res.send(csv);
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove };
|
||||
const listStateOptions = asyncHandler(async (_req, res) => {
|
||||
const data = service.listStateOptions();
|
||||
res.json(new ApiResponse(200, data, 'Location state options fetched'));
|
||||
});
|
||||
|
||||
module.exports = { create, list, exportCsv, getOne, update, remove, listStateOptions };
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ const {
|
||||
const router = express.Router();
|
||||
|
||||
router.use(authenticate);
|
||||
router.get('/states', authorize('MASTERS', 'view'), controller.listStateOptions);
|
||||
router.get('/', authorize('MASTERS', 'view'), validate(listQuerySchema, 'query'), controller.list);
|
||||
router.get(
|
||||
'/export',
|
||||
|
||||
@ -11,6 +11,7 @@ const {
|
||||
toWarehouseResponse,
|
||||
toLocationResponse,
|
||||
} = require('../../../utils/locations');
|
||||
const { LOCATION_STATE_OPTIONS } = require('./locations.constants');
|
||||
|
||||
const TABLE_NAME = 'locations';
|
||||
|
||||
@ -347,4 +348,5 @@ module.exports = {
|
||||
getLocationById: (id) => getLocationById(id),
|
||||
updateLocationById,
|
||||
deleteLocationById,
|
||||
listStateOptions: () => LOCATION_STATE_OPTIONS,
|
||||
};
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
const Joi = require('joi');
|
||||
const { masterCode, masterName, listQuerySchema, toExportQuerySchema } = require('../_shared/masters.validation');
|
||||
const { LOCATION_STATE_VALUES } = require('./locations.constants');
|
||||
|
||||
const locationFields = {
|
||||
gstin: Joi.string().max(15).allow(null, '').optional(),
|
||||
address: Joi.string().allow(null, '').optional(),
|
||||
city: Joi.string().max(100).allow(null, '').optional(),
|
||||
state: Joi.string().max(100).allow(null, '').optional(),
|
||||
state: Joi.string()
|
||||
.valid(...LOCATION_STATE_VALUES)
|
||||
.allow(null, '')
|
||||
.optional(),
|
||||
pincode: Joi.string().max(10).allow(null, '').optional(),
|
||||
phone: Joi.string().max(15).allow(null, '').optional(),
|
||||
location: Joi.string().max(200).allow(null, '').optional(),
|
||||
|
||||
@ -56,6 +56,7 @@ const sanitizeDepreciationRow = (asset, asOfDate) => {
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
useful_life_years: asset.useful_life_years,
|
||||
commencement_date: asset.commencement_date,
|
||||
purchase_date: asset.purchase_date,
|
||||
as_of_date: asOfDate,
|
||||
});
|
||||
@ -65,6 +66,7 @@ const sanitizeDepreciationRow = (asset, asOfDate) => {
|
||||
asset_code: asset.asset_code,
|
||||
asset_name: asset.asset_name,
|
||||
status: asset.status,
|
||||
commencement_date: asset.commencement_date,
|
||||
purchase_date: asset.purchase_date,
|
||||
purchase_cost: purchaseCost,
|
||||
salvage_value: salvageValue,
|
||||
@ -203,6 +205,7 @@ const exportAssetDepreciation = async (query) => {
|
||||
{ key: (row) => row.item_subcategory?.name || '', header: 'Subcategory' },
|
||||
{ key: (row) => row.location?.name || '', header: 'Location' },
|
||||
{ key: (row) => row.department?.name || '', header: 'Department' },
|
||||
{ key: 'commencement_date', header: 'Commencement Date', type: 'date' },
|
||||
{ key: 'purchase_date', header: 'Purchase Date', type: 'date' },
|
||||
{ key: 'purchase_cost', header: 'Purchase Cost' },
|
||||
{ key: 'salvage_value', header: 'Salvage Value' },
|
||||
|
||||
43
src/modules/vendors/vendors.constants.js
vendored
43
src/modules/vendors/vendors.constants.js
vendored
@ -9,47 +9,12 @@ const GST_TREATMENTS = [
|
||||
{ value: 'OTHER', label: 'Other' },
|
||||
];
|
||||
|
||||
const SOURCE_OF_SUPPLY_OPTIONS = [
|
||||
{ value: 'Andhra Pradesh', label: 'Andhra Pradesh' },
|
||||
{ value: 'Arunachal Pradesh', label: 'Arunachal Pradesh' },
|
||||
{ value: 'Assam', label: 'Assam' },
|
||||
{ value: 'Bihar', label: 'Bihar' },
|
||||
{ value: 'Chhattisgarh', label: 'Chhattisgarh' },
|
||||
{ value: 'Goa', label: 'Goa' },
|
||||
{ value: 'Gujarat', label: 'Gujarat' },
|
||||
{ value: 'Haryana', label: 'Haryana' },
|
||||
{ value: 'Himachal Pradesh', label: 'Himachal Pradesh' },
|
||||
{ value: 'Jharkhand', label: 'Jharkhand' },
|
||||
{ value: 'Karnataka', label: 'Karnataka' },
|
||||
{ value: 'Kerala', label: 'Kerala' },
|
||||
{ value: 'Madhya Pradesh', label: 'Madhya Pradesh' },
|
||||
{ value: 'Maharashtra', label: 'Maharashtra' },
|
||||
{ value: 'Manipur', label: 'Manipur' },
|
||||
{ value: 'Meghalaya', label: 'Meghalaya' },
|
||||
{ value: 'Mizoram', label: 'Mizoram' },
|
||||
{ value: 'Nagaland', label: 'Nagaland' },
|
||||
{ value: 'Odisha', label: 'Odisha' },
|
||||
{ value: 'Punjab', label: 'Punjab' },
|
||||
{ value: 'Rajasthan', label: 'Rajasthan' },
|
||||
{ value: 'Sikkim', label: 'Sikkim' },
|
||||
{ value: 'Tamil Nadu', label: 'Tamil Nadu' },
|
||||
{ value: 'Telangana', label: 'Telangana' },
|
||||
{ value: 'Tripura', label: 'Tripura' },
|
||||
{ value: 'Uttar Pradesh', label: 'Uttar Pradesh' },
|
||||
{ value: 'Uttarakhand', label: 'Uttarakhand' },
|
||||
{ value: 'West Bengal', label: 'West Bengal' },
|
||||
{ value: 'Andaman and Nicobar Islands', label: 'Andaman and Nicobar Islands' },
|
||||
{ value: 'Chandigarh', label: 'Chandigarh' },
|
||||
{ value: 'Dadra and Nagar Haveli and Daman and Diu', label: 'Dadra and Nagar Haveli and Daman and Diu' },
|
||||
{ value: 'Delhi', label: 'Delhi' },
|
||||
{ value: 'Jammu and Kashmir', label: 'Jammu and Kashmir' },
|
||||
{ value: 'Ladakh', label: 'Ladakh' },
|
||||
{ value: 'Lakshadweep', label: 'Lakshadweep' },
|
||||
{ value: 'Puducherry', label: 'Puducherry' },
|
||||
];
|
||||
const {
|
||||
INDIAN_STATE_OPTIONS: SOURCE_OF_SUPPLY_OPTIONS,
|
||||
INDIAN_STATE_VALUES: SOURCE_OF_SUPPLY_VALUES,
|
||||
} = require('../../constants/indianStates');
|
||||
|
||||
const GST_TREATMENT_VALUES = GST_TREATMENTS.map((item) => item.value);
|
||||
const SOURCE_OF_SUPPLY_VALUES = SOURCE_OF_SUPPLY_OPTIONS.map((item) => item.value);
|
||||
|
||||
module.exports = {
|
||||
GST_TREATMENTS,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user