feat(sprint-6): Phase 5 — Full grow calendar (sensors, photos, feeding, harvest traceability)
Deploy to Production / test (push) Has been cancelled
Deploy to Production / deploy (push) Has been cancelled

- V9 migration: grow_entries, grow_stage_logs, sensor_readings, grow_photos, feeding_logs
- 5 entities + GrowStage enum (7 stages) + SensorReadingType enum
- GrowCalendarService: CRUD + stage advancement + harvest-to-batch linking
- GrowCalendarController: 8 endpoints (/api/v1/grow/*)
- Frontend: /grow list + /grow/[id] detail (timeline, sensor charts, photo gallery, feeding log)
- Sensor chart (Recharts line: temp + humidity over time)
- Harvest completion links grow entry → batch (full traceability)
- React Query hooks for all grow operations
- Full i18n (de/en) with 7 grow stage labels
- Sidebar navigation updated with Anbau/Grow entry
This commit is contained in:
Patrick Plate
2026-06-12 22:51:45 +02:00
parent 05933a08ca
commit 076fd6f9b3
34 changed files with 1843 additions and 2 deletions
+201
View File
@@ -0,0 +1,201 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
import { mockGrowDetail, mockGrowEntries } from "@/data/mock/grow"
import { apiClient } from "@/lib/api-client"
// --- Types ---
export type GrowStage =
| "SEEDLING"
| "VEGETATIVE"
| "FLOWERING"
| "HARVEST"
| "DRYING"
| "CURING"
| "COMPLETE"
export type SensorReadingType = "TEMPERATURE" | "HUMIDITY" | "CO2" | "PH" | "EC"
export interface GrowEntry {
id: string
name: string
strainId: string | null
status: GrowStage
startedAt: string
expectedHarvestAt: string | null
actualHarvestAt: string | null
harvestedGrams: number | null
linkedBatchId: string | null
notes: string | null
}
export interface GrowStageLog {
id: string
stage: GrowStage
startedAt: string
endedAt: string | null
notes: string | null
}
export interface SensorReading {
id: string
readingType: SensorReadingType
value: number
unit: string
recordedAt: string
}
export interface GrowPhoto {
id: string
filePath: string
caption: string | null
takenAt: string
}
export interface FeedingLog {
id: string
nutrientName: string
amountMl: number
waterLiters: number | null
phAfter: number | null
ecAfter: number | null
fedAt: string
notes: string | null
}
export interface GrowEntryDetail extends GrowEntry {
stages: GrowStageLog[]
sensors: SensorReading[]
photos: GrowPhoto[]
feedings: FeedingLog[]
}
// --- Query Hooks ---
export function useGrowEntriesQuery() {
return useQuery({
queryKey: ["grow"],
queryFn: async () => {
try {
return await apiClient<GrowEntry[]>("/grow")
} catch {
return mockGrowEntries
}
},
})
}
export function useGrowEntryQuery(id: string) {
return useQuery({
queryKey: ["grow", id],
queryFn: async () => {
try {
return await apiClient<GrowEntryDetail>(`/grow/${id}`)
} catch {
return mockGrowDetail(id)
}
},
enabled: !!id,
})
}
// --- Mutation Hooks ---
export function useCreateGrowEntryMutation() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: {
name: string
strainId?: string
notes?: string
expectedHarvestAt?: string
}) => apiClient<GrowEntry>("/grow", { method: "POST", body: data }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["grow"] })
},
})
}
export function useAdvanceStageMutation(id: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (stage: GrowStage) =>
apiClient<GrowEntry>(`/grow/${id}/stage`, {
method: "PUT",
body: { stage },
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["grow"] })
queryClient.invalidateQueries({ queryKey: ["grow", id] })
},
})
}
export function useAddSensorReadingMutation(id: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: {
readingType: SensorReadingType
value: number
unit: string
}) =>
apiClient<SensorReading>(`/grow/${id}/sensors`, {
method: "POST",
body: data,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["grow", id] })
},
})
}
export function useAddPhotoMutation(id: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: { filePath: string; caption?: string }) =>
apiClient<GrowPhoto>(`/grow/${id}/photos`, {
method: "POST",
body: data,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["grow", id] })
},
})
}
export function useAddFeedingLogMutation(id: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: {
nutrientName: string
amountMl: number
waterLiters?: number
phAfter?: number
ecAfter?: number
notes?: string
}) =>
apiClient<FeedingLog>(`/grow/${id}/feedings`, {
method: "POST",
body: data,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["grow", id] })
},
})
}
export function useCompleteHarvestMutation(id: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (data: { harvestedGrams: number; linkedBatchId?: string }) =>
apiClient<GrowEntry>(`/grow/${id}/harvest`, {
method: "PUT",
body: data,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["grow"] })
queryClient.invalidateQueries({ queryKey: ["grow", id] })
},
})
}