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:
fullsizemalt 2025-12-09 15:11:41 -08:00
parent d42331075d
commit 22574359ba
3 changed files with 232 additions and 0 deletions

View 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 });
}
}

View 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);
}

View file

@ -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() };