316 lines
10 KiB
JavaScript
316 lines
10 KiB
JavaScript
const prisma = require('../../../config/prisma');
|
|
const ApiError = require('../../../utils/ApiError');
|
|
const auditLog = require('../../../utils/auditLog');
|
|
const { getPagination, isDropdownCall } = require('../../../utils/pagination');
|
|
const { parseId } = require('../../../utils/parseId');
|
|
const { rowsToCsv } = require('../../../utils/csv');
|
|
const {
|
|
LOCATION_TYPES,
|
|
toPlantResponse,
|
|
toWarehouseResponse,
|
|
toLocationResponse,
|
|
} = require('../../../utils/locations');
|
|
const { LOCATION_STATE_OPTIONS } = require('./locations.constants');
|
|
|
|
const TABLE_NAME = 'locations';
|
|
|
|
const normalizeString = (value) => {
|
|
if (value === null || value === undefined) return value;
|
|
return String(value).trim();
|
|
};
|
|
|
|
const buildPlantData = (payload) => ({
|
|
type: LOCATION_TYPES.PLANT,
|
|
code: payload.code ? String(payload.code).trim().toUpperCase() : undefined,
|
|
name: normalizeString(payload.name),
|
|
gstin: normalizeString(payload.gstin),
|
|
address: normalizeString(payload.address),
|
|
city: normalizeString(payload.city),
|
|
state: normalizeString(payload.state),
|
|
pincode: normalizeString(payload.pincode),
|
|
phone: normalizeString(payload.phone),
|
|
is_active: payload.is_active,
|
|
location: null,
|
|
});
|
|
|
|
const buildWarehouseData = (payload) => ({
|
|
type: LOCATION_TYPES.WAREHOUSE,
|
|
code: payload.code ? String(payload.code).trim().toUpperCase() : undefined,
|
|
name: normalizeString(payload.name),
|
|
location: normalizeString(payload.location),
|
|
is_active: payload.is_active,
|
|
gstin: null,
|
|
address: null,
|
|
city: null,
|
|
state: null,
|
|
pincode: null,
|
|
phone: null,
|
|
});
|
|
|
|
const assertUniqueCode = async (code, excludeId = null) => {
|
|
if (!code) return;
|
|
const existing = await prisma.locations.findFirst({
|
|
where: {
|
|
code,
|
|
deleted_at: null,
|
|
...(excludeId ? { id: { not: parseId(excludeId) } } : {}),
|
|
},
|
|
});
|
|
if (existing) throw new ApiError(409, 'Location code already exists');
|
|
};
|
|
|
|
const createLocation = async (type, payload, userId, requestId) => {
|
|
const data =
|
|
type === LOCATION_TYPES.PLANT ? buildPlantData(payload) : buildWarehouseData(payload);
|
|
|
|
if (!data.code || !data.name) throw new ApiError(422, 'code and name are required');
|
|
await assertUniqueCode(data.code);
|
|
|
|
data.created_by = userId ? BigInt(userId) : null;
|
|
data.updated_by = userId ? BigInt(userId) : null;
|
|
|
|
const created = await prisma.locations.create({ data });
|
|
|
|
const response =
|
|
type === LOCATION_TYPES.PLANT ? toPlantResponse(created) : toWarehouseResponse(created);
|
|
|
|
await auditLog({
|
|
tableName: TABLE_NAME,
|
|
recordId: created.id,
|
|
action: 'CREATE',
|
|
oldValue: null,
|
|
newValue: response,
|
|
userId,
|
|
requestId,
|
|
});
|
|
|
|
return response;
|
|
};
|
|
|
|
const buildLocationsWhere = (query, type = null) => {
|
|
const effectiveType = type || query.type || null;
|
|
const where = {
|
|
deleted_at: null,
|
|
...(effectiveType ? { type: effectiveType } : {}),
|
|
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
|
|
...(query.search
|
|
? {
|
|
OR: [
|
|
{ code: { contains: query.search, mode: 'insensitive' } },
|
|
{ name: { contains: query.search, mode: 'insensitive' } },
|
|
],
|
|
}
|
|
: {}),
|
|
};
|
|
return { where, effectiveType };
|
|
};
|
|
|
|
const listLocations = async (query, type = null) => {
|
|
const { where, effectiveType } = buildLocationsWhere(query, type);
|
|
|
|
const mapRow = (row) => {
|
|
if (effectiveType === LOCATION_TYPES.PLANT) return toPlantResponse(row);
|
|
if (effectiveType === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
|
|
return toLocationResponse(row);
|
|
};
|
|
|
|
if (isDropdownCall(query)) {
|
|
where.is_active = true;
|
|
const rows = await prisma.locations.findMany({
|
|
where,
|
|
orderBy: { created_at: 'desc' },
|
|
});
|
|
const data = rows.map(mapRow);
|
|
return { data, meta: { page: 1, limit: data.length, total: data.length, dropdown: true } };
|
|
}
|
|
|
|
const { page, limit, skip } = getPagination(query);
|
|
|
|
const [rows, total] = await Promise.all([
|
|
prisma.locations.findMany({
|
|
where,
|
|
orderBy: { created_at: 'desc' },
|
|
skip,
|
|
take: limit,
|
|
}),
|
|
prisma.locations.count({ where }),
|
|
]);
|
|
|
|
return { data: rows.map(mapRow), meta: { page, limit, total } };
|
|
};
|
|
|
|
const exportLocations = async (query, type = null) => {
|
|
const { where, effectiveType } = buildLocationsWhere(query, type);
|
|
const rows = await prisma.locations.findMany({
|
|
where,
|
|
orderBy: { created_at: 'desc' },
|
|
});
|
|
|
|
const mapRow = (row) => {
|
|
if (effectiveType === LOCATION_TYPES.PLANT) return toPlantResponse(row);
|
|
if (effectiveType === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
|
|
return toLocationResponse(row);
|
|
};
|
|
|
|
const columns =
|
|
effectiveType === LOCATION_TYPES.WAREHOUSE
|
|
? [
|
|
{ key: 'code', header: 'Code' },
|
|
{ key: 'name', header: 'Name' },
|
|
{ key: 'location', header: 'Location' },
|
|
{ key: 'is_active', header: 'Is Active' },
|
|
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
|
]
|
|
: effectiveType === LOCATION_TYPES.PLANT
|
|
? [
|
|
{ key: 'code', header: 'Code' },
|
|
{ key: 'name', header: 'Name' },
|
|
{ key: 'gstin', header: 'GSTIN' },
|
|
{ key: 'city', header: 'City' },
|
|
{ key: 'state', header: 'State' },
|
|
{ key: 'is_active', header: 'Is Active' },
|
|
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
|
]
|
|
: [
|
|
{ key: 'type', header: 'Type' },
|
|
{ key: 'code', header: 'Code' },
|
|
{ key: 'name', header: 'Name' },
|
|
{ key: 'is_active', header: 'Is Active' },
|
|
{ key: 'created_at', header: 'Created At', type: 'datetime' },
|
|
];
|
|
|
|
return rowsToCsv(columns, rows.map(mapRow));
|
|
};
|
|
|
|
const getLocationById = async (id, type = null) => {
|
|
const row = await prisma.locations.findFirst({
|
|
where: {
|
|
id: parseId(id),
|
|
deleted_at: null,
|
|
...(type ? { type } : {}),
|
|
},
|
|
});
|
|
if (!row) throw new ApiError(404, type ? `${type} not found` : 'Location not found');
|
|
|
|
if (type === LOCATION_TYPES.PLANT) return toPlantResponse(row);
|
|
if (type === LOCATION_TYPES.WAREHOUSE) return toWarehouseResponse(row);
|
|
return toLocationResponse(row);
|
|
};
|
|
|
|
const updateLocation = async (id, type, payload, userId, requestId) => {
|
|
const idBigInt = parseId(id);
|
|
const existing = await prisma.locations.findFirst({
|
|
where: { id: idBigInt, deleted_at: null, type },
|
|
});
|
|
if (!existing) throw new ApiError(404, `${type} not found`);
|
|
|
|
const data =
|
|
type === LOCATION_TYPES.PLANT ? buildPlantData(payload) : buildWarehouseData(payload);
|
|
Object.keys(data).forEach((key) => {
|
|
if (data[key] === undefined) delete data[key];
|
|
});
|
|
|
|
if (data.code) await assertUniqueCode(data.code, id);
|
|
|
|
data.updated_by = userId ? BigInt(userId) : null;
|
|
|
|
const updated = await prisma.locations.update({
|
|
where: { id: idBigInt },
|
|
data,
|
|
});
|
|
|
|
const oldValue =
|
|
type === LOCATION_TYPES.PLANT ? toPlantResponse(existing) : toWarehouseResponse(existing);
|
|
const newValue =
|
|
type === LOCATION_TYPES.PLANT ? toPlantResponse(updated) : toWarehouseResponse(updated);
|
|
|
|
await auditLog({
|
|
tableName: TABLE_NAME,
|
|
recordId: id,
|
|
action: 'UPDATE',
|
|
oldValue,
|
|
newValue,
|
|
userId,
|
|
requestId,
|
|
});
|
|
|
|
return newValue;
|
|
};
|
|
|
|
const deleteLocation = async (id, type, userId, requestId) => {
|
|
const idBigInt = parseId(id);
|
|
const existing = await prisma.locations.findFirst({
|
|
where: { id: idBigInt, deleted_at: null, type },
|
|
});
|
|
if (!existing) throw new ApiError(404, `${type} not found`);
|
|
|
|
await prisma.locations.update({
|
|
where: { id: idBigInt },
|
|
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null },
|
|
});
|
|
|
|
const oldValue =
|
|
type === LOCATION_TYPES.PLANT ? toPlantResponse(existing) : toWarehouseResponse(existing);
|
|
|
|
await auditLog({
|
|
tableName: TABLE_NAME,
|
|
recordId: id,
|
|
action: 'DELETE',
|
|
oldValue,
|
|
newValue: { deleted_at: new Date() },
|
|
userId,
|
|
requestId,
|
|
});
|
|
};
|
|
|
|
const deleteLocationById = async (id, userId, requestId) => {
|
|
const existing = await prisma.locations.findFirst({
|
|
where: { id: parseId(id), deleted_at: null },
|
|
});
|
|
if (!existing) throw new ApiError(404, 'Location not found');
|
|
return deleteLocation(id, existing.type, userId, requestId);
|
|
};
|
|
|
|
const updateLocationById = async (id, payload, userId, requestId) => {
|
|
const existing = await prisma.locations.findFirst({
|
|
where: { id: parseId(id), deleted_at: null },
|
|
});
|
|
if (!existing) throw new ApiError(404, 'Location not found');
|
|
return updateLocation(id, existing.type, payload, userId, requestId);
|
|
};
|
|
|
|
module.exports = {
|
|
LOCATION_TYPES,
|
|
createPlant: (payload, userId, requestId) =>
|
|
createLocation(LOCATION_TYPES.PLANT, payload, userId, requestId),
|
|
listPlants: (query) => listLocations(query, LOCATION_TYPES.PLANT),
|
|
exportPlants: (query) => exportLocations(query, LOCATION_TYPES.PLANT),
|
|
getPlantById: (id) => getLocationById(id, LOCATION_TYPES.PLANT),
|
|
updatePlant: (id, payload, userId, requestId) =>
|
|
updateLocation(id, LOCATION_TYPES.PLANT, payload, userId, requestId),
|
|
deletePlant: (id, userId, requestId) =>
|
|
deleteLocation(id, LOCATION_TYPES.PLANT, userId, requestId),
|
|
createWarehouse: (payload, userId, requestId) =>
|
|
createLocation(LOCATION_TYPES.WAREHOUSE, payload, userId, requestId),
|
|
listWarehouses: (query) => listLocations(query, LOCATION_TYPES.WAREHOUSE),
|
|
exportWarehouses: (query) => exportLocations(query, LOCATION_TYPES.WAREHOUSE),
|
|
getWarehouseById: (id) => getLocationById(id, LOCATION_TYPES.WAREHOUSE),
|
|
updateWarehouse: (id, payload, userId, requestId) =>
|
|
updateLocation(id, LOCATION_TYPES.WAREHOUSE, payload, userId, requestId),
|
|
deleteWarehouse: (id, userId, requestId) =>
|
|
deleteLocation(id, LOCATION_TYPES.WAREHOUSE, userId, requestId),
|
|
createLocation: (payload, userId, requestId) => {
|
|
if (!payload.type) throw new ApiError(422, 'type is required');
|
|
if (![LOCATION_TYPES.PLANT, LOCATION_TYPES.WAREHOUSE].includes(payload.type)) {
|
|
throw new ApiError(422, 'type must be plant or warehouse');
|
|
}
|
|
return createLocation(payload.type, payload, userId, requestId);
|
|
},
|
|
listLocations: (query) => listLocations(query),
|
|
exportLocations: (query) => exportLocations(query),
|
|
getLocationById: (id) => getLocationById(id),
|
|
updateLocationById,
|
|
deleteLocationById,
|
|
listStateOptions: () => LOCATION_STATE_OPTIONS,
|
|
};
|