feat: add batch detail endpoint and fix drill-down navigation
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:
parent
817abb732d
commit
e7be23cce4
4 changed files with 61 additions and 19 deletions
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
|
@ -243,8 +237,8 @@ export default function BatchDetailPage() {
|
||||||
<div className="flex items-center gap-2 mb-1">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
<h1 className="text-xl font-semibold text-primary">{batch.name}</h1>
|
<h1 className="text-xl font-semibold text-primary">{batch.name}</h1>
|
||||||
<span className={`badge ${batch.stage === 'FLOWERING' ? 'badge-warning' :
|
<span className={`badge ${batch.stage === 'FLOWERING' ? 'badge-warning' :
|
||||||
batch.stage === 'VEGETATIVE' ? 'badge-success' :
|
batch.stage === 'VEGETATIVE' ? 'badge-success' :
|
||||||
'badge'
|
'badge'
|
||||||
}`}>{batch.stage}</span>
|
}`}>{batch.stage}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 text-sm text-secondary">
|
<div className="flex items-center gap-4 text-sm text-secondary">
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue