From 22574359bad6e22e799c0564f6046a792a2066d8 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Tue, 9 Dec 2025 15:11:41 -0800 Subject: [PATCH] feat: Shopping List Backend API Complete MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📦 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 --- .../src/controllers/supplies.controller.ts | 193 ++++++++++++++++++ backend/src/routes/supplies.routes.ts | 37 ++++ backend/src/server.ts | 2 + 3 files changed, 232 insertions(+) create mode 100644 backend/src/controllers/supplies.controller.ts create mode 100644 backend/src/routes/supplies.routes.ts diff --git a/backend/src/controllers/supplies.controller.ts b/backend/src/controllers/supplies.controller.ts new file mode 100644 index 0000000..b70eade --- /dev/null +++ b/backend/src/controllers/supplies.controller.ts @@ -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 }); + } +} diff --git a/backend/src/routes/supplies.routes.ts b/backend/src/routes/supplies.routes.ts new file mode 100644 index 0000000..87d1322 --- /dev/null +++ b/backend/src/routes/supplies.routes.ts @@ -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); +} diff --git a/backend/src/server.ts b/backend/src/server.ts index 85f1f4e..2cd7cf2 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -7,6 +7,7 @@ import { roomRoutes } from './routes/rooms.routes'; import { batchRoutes } from './routes/batches.routes'; import { timeclockRoutes } from './routes/timeclock.routes'; import { walkthroughRoutes } from './routes/walkthrough.routes'; +import { suppliesRoutes } from './routes/supplies.routes'; dotenv.config(); @@ -26,6 +27,7 @@ server.register(roomRoutes, { prefix: '/api/rooms' }); server.register(batchRoutes, { prefix: '/api/batches' }); server.register(timeclockRoutes, { prefix: '/api/timeclock' }); server.register(walkthroughRoutes, { prefix: '/api/walkthroughs' }); +server.register(suppliesRoutes, { prefix: '/api' }); server.get('/api/healthz', async (request, reply) => { return { status: 'ok', timestamp: new Date().toISOString() };