feat: Shopping List Backend API Complete
📦 Supply Items Backend (Phase 3A) ✅ Controller (supplies.controller.ts): - getSupplyItems() - List all items - getShoppingList() - Items below threshold - getSupplyItem() - Single item detail - createSupplyItem() - Add new item - updateSupplyItem() - Edit item - deleteSupplyItem() - Remove item - markAsOrdered() - Track last ordered date - adjustQuantity() - Add/subtract stock ✅ Routes (supplies.routes.ts): - GET /api/supplies - GET /api/supplies/shopping-list - GET /api/supplies/:id - POST /api/supplies - PATCH /api/supplies/:id - DELETE /api/supplies/:id - POST /api/supplies/:id/order - POST /api/supplies/:id/adjust ✅ Server Integration: - Registered supplies routes Next: Frontend UI + Migration
This commit is contained in:
parent
d42331075d
commit
22574359ba
3 changed files with 232 additions and 0 deletions
193
backend/src/controllers/supplies.controller.ts
Normal file
193
backend/src/controllers/supplies.controller.ts
Normal file
|
|
@ -0,0 +1,193 @@
|
||||||
|
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
// Get all supply items
|
||||||
|
export async function getSupplyItems(request: FastifyRequest, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const items = await prisma.supplyItem.findMany({
|
||||||
|
orderBy: [
|
||||||
|
{ category: 'asc' },
|
||||||
|
{ name: 'asc' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.send(items);
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(500).send({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get shopping list (items below threshold)
|
||||||
|
export async function getShoppingList(request: FastifyRequest, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const items = await prisma.supplyItem.findMany({
|
||||||
|
where: {
|
||||||
|
quantity: {
|
||||||
|
lte: prisma.supplyItem.fields.minThreshold,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: [
|
||||||
|
{ category: 'asc' },
|
||||||
|
{ name: 'asc' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.send(items);
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(500).send({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get single supply item
|
||||||
|
export async function getSupplyItem(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
|
||||||
|
const item = await prisma.supplyItem.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!item) {
|
||||||
|
return reply.status(404).send({ message: 'Supply item not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return reply.send(item);
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(500).send({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create supply item
|
||||||
|
export async function createSupplyItem(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Body: {
|
||||||
|
name: string;
|
||||||
|
category: string;
|
||||||
|
quantity?: number;
|
||||||
|
minThreshold?: number;
|
||||||
|
unit: string;
|
||||||
|
location?: string;
|
||||||
|
notes?: string;
|
||||||
|
};
|
||||||
|
}>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { name, category, quantity, minThreshold, unit, location, notes } = request.body;
|
||||||
|
|
||||||
|
const item = await prisma.supplyItem.create({
|
||||||
|
data: {
|
||||||
|
name,
|
||||||
|
category: category as any,
|
||||||
|
quantity: quantity || 0,
|
||||||
|
minThreshold: minThreshold || 0,
|
||||||
|
unit,
|
||||||
|
location,
|
||||||
|
notes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.status(201).send(item);
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(500).send({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update supply item
|
||||||
|
export async function updateSupplyItem(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Params: { id: string };
|
||||||
|
Body: {
|
||||||
|
name?: string;
|
||||||
|
category?: string;
|
||||||
|
quantity?: number;
|
||||||
|
minThreshold?: number;
|
||||||
|
unit?: string;
|
||||||
|
location?: string;
|
||||||
|
notes?: string;
|
||||||
|
};
|
||||||
|
}>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
const data = request.body;
|
||||||
|
|
||||||
|
const item = await prisma.supplyItem.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
...data,
|
||||||
|
category: data.category as any,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.send(item);
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(500).send({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete supply item
|
||||||
|
export async function deleteSupplyItem(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
|
||||||
|
await prisma.supplyItem.delete({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.status(204).send();
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(500).send({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark item as ordered
|
||||||
|
export async function markAsOrdered(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
|
||||||
|
const item = await prisma.supplyItem.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
lastOrdered: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.send(item);
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(500).send({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adjust quantity (add or subtract)
|
||||||
|
export async function adjustQuantity(
|
||||||
|
request: FastifyRequest<{
|
||||||
|
Params: { id: string };
|
||||||
|
Body: { adjustment: number };
|
||||||
|
}>,
|
||||||
|
reply: FastifyReply
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { id } = request.params;
|
||||||
|
const { adjustment } = request.body;
|
||||||
|
|
||||||
|
const item = await prisma.supplyItem.findUnique({ where: { id } });
|
||||||
|
if (!item) {
|
||||||
|
return reply.status(404).send({ message: 'Supply item not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await prisma.supplyItem.update({
|
||||||
|
where: { id },
|
||||||
|
data: {
|
||||||
|
quantity: Math.max(0, item.quantity + adjustment),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return reply.send(updated);
|
||||||
|
} catch (error: any) {
|
||||||
|
return reply.status(500).send({ message: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
37
backend/src/routes/supplies.routes.ts
Normal file
37
backend/src/routes/supplies.routes.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { FastifyInstance } from 'fastify';
|
||||||
|
import {
|
||||||
|
getSupplyItems,
|
||||||
|
getShoppingList,
|
||||||
|
getSupplyItem,
|
||||||
|
createSupplyItem,
|
||||||
|
updateSupplyItem,
|
||||||
|
deleteSupplyItem,
|
||||||
|
markAsOrdered,
|
||||||
|
adjustQuantity,
|
||||||
|
} from '../controllers/supplies.controller';
|
||||||
|
|
||||||
|
export async function suppliesRoutes(server: FastifyInstance) {
|
||||||
|
// Get all supply items
|
||||||
|
server.get('/supplies', getSupplyItems);
|
||||||
|
|
||||||
|
// Get shopping list (items below threshold)
|
||||||
|
server.get('/supplies/shopping-list', getShoppingList);
|
||||||
|
|
||||||
|
// Get single supply item
|
||||||
|
server.get('/supplies/:id', getSupplyItem);
|
||||||
|
|
||||||
|
// Create supply item
|
||||||
|
server.post('/supplies', createSupplyItem);
|
||||||
|
|
||||||
|
// Update supply item
|
||||||
|
server.patch('/supplies/:id', updateSupplyItem);
|
||||||
|
|
||||||
|
// Delete supply item
|
||||||
|
server.delete('/supplies/:id', deleteSupplyItem);
|
||||||
|
|
||||||
|
// Mark item as ordered
|
||||||
|
server.post('/supplies/:id/order', markAsOrdered);
|
||||||
|
|
||||||
|
// Adjust quantity
|
||||||
|
server.post('/supplies/:id/adjust', adjustQuantity);
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import { roomRoutes } from './routes/rooms.routes';
|
||||||
import { batchRoutes } from './routes/batches.routes';
|
import { batchRoutes } from './routes/batches.routes';
|
||||||
import { timeclockRoutes } from './routes/timeclock.routes';
|
import { timeclockRoutes } from './routes/timeclock.routes';
|
||||||
import { walkthroughRoutes } from './routes/walkthrough.routes';
|
import { walkthroughRoutes } from './routes/walkthrough.routes';
|
||||||
|
import { suppliesRoutes } from './routes/supplies.routes';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
|
@ -26,6 +27,7 @@ server.register(roomRoutes, { prefix: '/api/rooms' });
|
||||||
server.register(batchRoutes, { prefix: '/api/batches' });
|
server.register(batchRoutes, { prefix: '/api/batches' });
|
||||||
server.register(timeclockRoutes, { prefix: '/api/timeclock' });
|
server.register(timeclockRoutes, { prefix: '/api/timeclock' });
|
||||||
server.register(walkthroughRoutes, { prefix: '/api/walkthroughs' });
|
server.register(walkthroughRoutes, { prefix: '/api/walkthroughs' });
|
||||||
|
server.register(suppliesRoutes, { prefix: '/api' });
|
||||||
|
|
||||||
server.get('/api/healthz', async (request, reply) => {
|
server.get('/api/healthz', async (request, reply) => {
|
||||||
return { status: 'ok', timestamp: new Date().toISOString() };
|
return { status: 'ok', timestamp: new Date().toISOString() };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue