feat: add batch detail endpoint and fix drill-down navigation
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

Backend:
- Add getBatchById controller with touch points and IPM schedule
- Add GET /batches/:id route

Frontend:
- Update Batch interface to include touchPoints
- BatchDetailPage now uses real touch points from API
- Better error handling on batch load failure
This commit is contained in:
fullsizemalt 2025-12-12 19:04:16 -08:00
parent 817abb732d
commit e7be23cce4
4 changed files with 61 additions and 19 deletions

View file

@ -17,6 +17,33 @@ export const getBatches = async (request: FastifyRequest, reply: FastifyReply) =
return batches; return batches;
}; };
export const getBatchById = async (request: FastifyRequest, reply: FastifyReply) => {
const { id } = request.params as { id: string };
const batch = await request.server.prisma.batch.findUnique({
where: { id },
include: {
room: true,
ipmSchedule: true,
touchPoints: {
orderBy: { createdAt: 'desc' },
take: 10,
include: {
user: {
select: { id: true, name: true }
}
}
}
}
});
if (!batch) {
return reply.status(404).send({ message: 'Batch not found' });
}
return batch;
};
export const createBatch = async (request: FastifyRequest, reply: FastifyReply) => { export const createBatch = async (request: FastifyRequest, reply: FastifyReply) => {
const { const {

View file

@ -1,5 +1,5 @@
import { FastifyInstance } from 'fastify'; import { FastifyInstance } from 'fastify';
import { getBatches, createBatch, updateBatch } from '../controllers/batches.controller'; import { getBatches, getBatchById, createBatch, updateBatch } from '../controllers/batches.controller';
export async function batchRoutes(server: FastifyInstance) { export async function batchRoutes(server: FastifyInstance) {
server.addHook('onRequest', async (request) => { server.addHook('onRequest', async (request) => {
@ -11,6 +11,7 @@ export async function batchRoutes(server: FastifyInstance) {
}); });
server.get('/', getBatches); server.get('/', getBatches);
server.get('/:id', getBatchById);
server.post('/', createBatch); server.post('/', createBatch);
server.put('/:id', updateBatch); server.put('/:id', updateBatch);
} }

View file

@ -21,6 +21,16 @@ export interface Batch {
intervalDays: number; intervalDays: number;
product?: string; product?: string;
}; };
touchPoints?: {
id: string;
type: string;
notes?: string;
createdAt: string;
user?: {
id: string;
name: string;
};
}[];
} }
export const batchesApi = { export const batchesApi = {

View file

@ -181,7 +181,7 @@ export default function BatchDetailPage() {
const [batch, setBatch] = useState<Batch | null>(null); const [batch, setBatch] = useState<Batch | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
// Mock sensor data // Mock sensor data (will be replaced with real sensor API)
const [sensorData] = useState(() => ({ const [sensorData] = useState(() => ({
temperature: generateMockData(14, 72, 78), temperature: generateMockData(14, 72, 78),
humidity: generateMockData(14, 50, 65), humidity: generateMockData(14, 50, 65),
@ -190,20 +190,14 @@ export default function BatchDetailPage() {
lightPPFD: generateMockData(14, 600, 900), lightPPFD: generateMockData(14, 600, 900),
})); }));
// Mock touch points
const [touchPoints] = useState(() => [
{ type: 'INSPECT', date: new Date().toISOString(), user: 'Alex', notes: 'Looking healthy' },
{ type: 'WATER', date: new Date(Date.now() - 86400000).toISOString(), user: 'Jordan' },
{ type: 'FEED', date: new Date(Date.now() - 172800000).toISOString(), user: 'Alex', notes: 'Week 3 flower nutrients' },
{ type: 'PHOTO', date: new Date(Date.now() - 259200000).toISOString(), user: 'Sam', notes: 'Progress photo' },
{ type: 'INSPECT', date: new Date(Date.now() - 345600000).toISOString(), user: 'Jordan', notes: 'No issues' },
]);
useEffect(() => { useEffect(() => {
if (id) { if (id) {
batchesApi.getById(id) batchesApi.getById(id)
.then(setBatch) .then(setBatch)
.catch(() => navigate('/batches')) .catch((err) => {
console.error('Failed to load batch:', err);
navigate('/batches');
})
.finally(() => setLoading(false)); .finally(() => setLoading(false));
} }
}, [id, navigate]); }, [id, navigate]);
@ -324,12 +318,22 @@ export default function BatchDetailPage() {
<div className="card"> <div className="card">
<div className="p-4 border-b border-subtle flex items-center justify-between"> <div className="p-4 border-b border-subtle flex items-center justify-between">
<h3 className="text-sm font-medium text-primary">Recent Activity</h3> <h3 className="text-sm font-medium text-primary">Recent Activity</h3>
<span className="text-xs text-tertiary">{touchPoints.length} entries</span> <span className="text-xs text-tertiary">{batch.touchPoints?.length || 0} entries</span>
</div> </div>
<div className="p-4 divide-y divide-subtle"> <div className="p-4 divide-y divide-subtle">
{touchPoints.map((tp, i) => ( {batch.touchPoints && batch.touchPoints.length > 0 ? (
<TouchPoint key={i} {...tp} /> batch.touchPoints.map((tp) => (
))} <TouchPoint
key={tp.id}
type={tp.type}
date={tp.createdAt}
user={tp.user?.name || 'Unknown'}
notes={tp.notes}
/>
))
) : (
<p className="text-sm text-tertiary text-center py-4">No activity yet</p>
)}
</div> </div>
</div> </div>
@ -344,7 +348,7 @@ export default function BatchDetailPage() {
<div className="text-xs text-tertiary">Days</div> <div className="text-xs text-tertiary">Days</div>
</div> </div>
<div> <div>
<div className="text-2xl font-semibold text-primary">{touchPoints.length}</div> <div className="text-2xl font-semibold text-primary">{batch.touchPoints?.length || 0}</div>
<div className="text-xs text-tertiary">Touch Points</div> <div className="text-xs text-tertiary">Touch Points</div>
</div> </div>
<div> <div>