feat(sprint-6): Phase 5 — Full grow calendar (sensors, photos, feeding, harvest traceability)
- 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:
@@ -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] })
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user