parts
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "Part" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"partNumber" TEXT NOT NULL,
|
||||
"product" TEXT NOT NULL,
|
||||
"price" REAL NOT NULL,
|
||||
"uom" TEXT NOT NULL,
|
||||
"model" TEXT NOT NULL,
|
||||
"createdById" TEXT NOT NULL,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "Part_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "Part_partNumber_key" ON "Part"("partNumber");
|
||||
@@ -23,6 +23,7 @@ model User {
|
||||
transfers StockTransfer[]
|
||||
events Event[]
|
||||
servicePoints ServicePoint[]
|
||||
parts Part[]
|
||||
loginRequests LoginRequest[] @relation("DealerLoginRequests")
|
||||
loginDecisions LoginRequest[] @relation("LoginDecisions")
|
||||
}
|
||||
@@ -141,3 +142,16 @@ model ServicePoint {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model Part {
|
||||
id String @id @default(cuid())
|
||||
partNumber String @unique
|
||||
product String
|
||||
price Float
|
||||
uom String
|
||||
model String
|
||||
createdById String
|
||||
createdBy User @relation(fields: [createdById], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
@@ -476,6 +476,58 @@ async function run(port) {
|
||||
'service point search failed'
|
||||
);
|
||||
|
||||
const partForbidden = await request(port, 'POST', '/api/parts', {
|
||||
token: dealerToken,
|
||||
body: {
|
||||
partNumber: 'CG125-B010',
|
||||
product: 'CLUTCH HOUSING - CG125',
|
||||
price: 9800,
|
||||
uom: 'PCS',
|
||||
model: 'CG - 125',
|
||||
},
|
||||
});
|
||||
assert(partForbidden.status === 403, 'dealer should not add parts');
|
||||
|
||||
const partMissing = await request(port, 'POST', '/api/parts', {
|
||||
token: repToken,
|
||||
body: { partNumber: 'CG125-B010', product: 'CLUTCH HOUSING - CG125' },
|
||||
});
|
||||
assert(partMissing.status === 400, 'part create should require every field');
|
||||
|
||||
const part = await request(port, 'POST', '/api/parts', {
|
||||
token: repToken,
|
||||
body: {
|
||||
partNumber: 'CG125-B010',
|
||||
product: 'CLUTCH HOUSING - CG125',
|
||||
price: 9800,
|
||||
uom: 'PCS',
|
||||
model: 'CG - 125',
|
||||
},
|
||||
});
|
||||
assert(part.status === 201, 'part create failed');
|
||||
|
||||
const duplicatePart = await request(port, 'POST', '/api/parts', {
|
||||
token: repToken,
|
||||
body: {
|
||||
partNumber: 'CG125-B010',
|
||||
product: 'CLUTCH HOUSING - CG125',
|
||||
price: 9800,
|
||||
uom: 'PCS',
|
||||
model: 'CG - 125',
|
||||
},
|
||||
});
|
||||
assert(duplicatePart.status === 409, 'duplicate part number should be rejected');
|
||||
|
||||
const foundPart = await request(port, 'GET', '/api/parts?model=CG%20-%20125&search=CLUTCH', {
|
||||
token: dealerToken,
|
||||
});
|
||||
assert(
|
||||
foundPart.status === 200 &&
|
||||
foundPart.json.data.some((item) => item.partNumber === 'CG125-B010' && item.price === 9800) &&
|
||||
foundPart.json.models.includes('CG - 125'),
|
||||
'part search failed'
|
||||
);
|
||||
|
||||
const reassigned = await request(port, 'PATCH', '/api/admin/assign-dealer', {
|
||||
token: adminToken,
|
||||
body: { salesRepId: rep.json.data.id, dealerId: dealerC.json.data.id, assigned: true },
|
||||
|
||||
@@ -8,6 +8,7 @@ const dealerRoutes = require('./routes/dealers');
|
||||
const stockRoutes = require('./routes/stocks');
|
||||
const eventRoutes = require('./routes/events');
|
||||
const servicePointRoutes = require('./routes/servicePoints');
|
||||
const partRoutes = require('./routes/parts');
|
||||
const adminRoutes = require('./routes/admin');
|
||||
|
||||
const app = express();
|
||||
@@ -24,6 +25,7 @@ app.use('/api/dealers', dealerRoutes);
|
||||
app.use('/api/stocks', stockRoutes);
|
||||
app.use('/api/events', eventRoutes);
|
||||
app.use('/api/service-points', servicePointRoutes);
|
||||
app.use('/api/parts', partRoutes);
|
||||
app.use('/api/admin', adminRoutes);
|
||||
|
||||
app.use((req, res) => {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
const express = require('express');
|
||||
const prisma = require('../db');
|
||||
const { asyncHandler, httpError } = require('../lib/errors');
|
||||
const { requireAuth, requireRole } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
function serializePart(part) {
|
||||
return {
|
||||
id: part.id,
|
||||
partNumber: part.partNumber,
|
||||
product: part.product,
|
||||
price: part.price,
|
||||
uom: part.uom,
|
||||
model: part.model,
|
||||
createdAt: part.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
function requiredText(value) {
|
||||
return String(value || '').trim();
|
||||
}
|
||||
|
||||
router.use(requireAuth);
|
||||
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res) => {
|
||||
const model = typeof req.query.model === 'string' ? req.query.model.trim() : '';
|
||||
const search = typeof req.query.search === 'string' ? req.query.search.trim() : '';
|
||||
const where = {};
|
||||
if (model) where.model = model;
|
||||
if (search) {
|
||||
where.OR = [{ partNumber: { contains: search } }, { product: { contains: search } }];
|
||||
}
|
||||
|
||||
const [parts, modelRows] = await Promise.all([
|
||||
prisma.part.findMany({
|
||||
where,
|
||||
orderBy: { partNumber: 'asc' },
|
||||
}),
|
||||
prisma.part.findMany({
|
||||
distinct: ['model'],
|
||||
select: { model: true },
|
||||
orderBy: { model: 'asc' },
|
||||
}),
|
||||
]);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: parts.map(serializePart),
|
||||
models: modelRows.map((row) => row.model),
|
||||
});
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/',
|
||||
requireRole('salesRep', 'superAdmin'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const partNumber = requiredText(req.body?.partNumber);
|
||||
const product = requiredText(req.body?.product);
|
||||
const uom = requiredText(req.body?.uom);
|
||||
const model = requiredText(req.body?.model);
|
||||
const rawPrice = req.body?.price;
|
||||
const price = typeof rawPrice === 'number' ? rawPrice : Number(requiredText(rawPrice));
|
||||
if (!partNumber || !product || !uom || !model || rawPrice === '' || rawPrice == null) {
|
||||
throw httpError(400, 'Part number, product, price, UOM, and model are required');
|
||||
}
|
||||
if (!Number.isFinite(price) || price < 0) {
|
||||
throw httpError(400, 'Price must be a non-negative number');
|
||||
}
|
||||
|
||||
const part = await prisma.part.create({
|
||||
data: {
|
||||
partNumber,
|
||||
product,
|
||||
price,
|
||||
uom,
|
||||
model,
|
||||
createdById: req.user.id,
|
||||
},
|
||||
});
|
||||
return res.status(201).json({ success: true, data: serializePart(part) });
|
||||
})
|
||||
);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user