Files
cannamanage/cannamanage-frontend/src/app/(dashboard-layout)/grow/page.tsx
T
Patrick Plate 076fd6f9b3
Deploy to Production / test (push) Has been cancelled
Deploy to Production / deploy (push) Has been cancelled
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
2026-06-12 22:51:45 +02:00

139 lines
4.8 KiB
TypeScript

"use client"
import Link from "next/link"
import { useGrowEntriesQuery } from "@/services/grow"
import { formatDistanceToNow } from "date-fns"
import { de } from "date-fns/locale"
import { useTranslations } from "next-intl"
import { Leaf, Plus, Sprout } from "lucide-react"
import type { GrowStage } from "@/services/grow"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { TableSkeleton } from "@/components/ui/data-skeleton"
const STAGE_COLORS: Record<GrowStage, string> = {
SEEDLING: "bg-lime-500/20 text-lime-700 dark:text-lime-400",
VEGETATIVE: "bg-green-500/20 text-green-700 dark:text-green-400",
FLOWERING: "bg-purple-500/20 text-purple-700 dark:text-purple-400",
HARVEST: "bg-amber-500/20 text-amber-700 dark:text-amber-400",
DRYING: "bg-orange-500/20 text-orange-700 dark:text-orange-400",
CURING: "bg-yellow-500/20 text-yellow-700 dark:text-yellow-400",
COMPLETE: "bg-emerald-500/20 text-emerald-700 dark:text-emerald-400",
}
const STAGES_ORDER: GrowStage[] = [
"SEEDLING",
"VEGETATIVE",
"FLOWERING",
"HARVEST",
"DRYING",
"CURING",
"COMPLETE",
]
export default function GrowPage() {
const t = useTranslations("grow")
const { data: entries, isLoading } = useGrowEntriesQuery()
if (isLoading) {
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">{t("title")}</h1>
</div>
<TableSkeleton />
</div>
)
}
const growEntries = entries ?? []
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold">{t("title")}</h1>
<Button asChild>
<Link href="/grow/new">
<Plus className="mr-2 h-4 w-4" />
{t("newGrow")}
</Link>
</Button>
</div>
{growEntries.length === 0 ? (
<Card>
<CardContent className="flex flex-col items-center justify-center py-12">
<Sprout className="mb-4 h-12 w-12 text-muted-foreground" />
<p className="text-muted-foreground">{t("noGrows")}</p>
</CardContent>
</Card>
) : (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
{growEntries.map((entry) => {
const stageIndex = STAGES_ORDER.indexOf(entry.status)
const daysInStage = formatDistanceToNow(new Date(entry.startedAt), {
locale: de,
addSuffix: false,
})
return (
<Link key={entry.id} href={`/grow/${entry.id}`}>
<Card className="transition-shadow hover:shadow-md">
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<CardTitle className="text-base">{entry.name}</CardTitle>
<Badge
className={STAGE_COLORS[entry.status]}
variant="secondary"
>
{t(`stages.${entry.status}`)}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-3">
{/* Stage progress indicator */}
<div className="flex items-center gap-1">
{STAGES_ORDER.map((stage, i) => (
<div
key={stage}
className={`h-2 flex-1 rounded-full ${
i <= stageIndex ? "bg-primary" : "bg-muted"
}`}
/>
))}
</div>
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>
<Leaf className="mr-1 inline h-3.5 w-3.5" />
{t("daysInStage")}: {daysInStage}
</span>
{entry.expectedHarvestAt && (
<span>
{t("expectedHarvest")}:{" "}
{new Date(entry.expectedHarvestAt).toLocaleDateString(
"de-DE"
)}
</span>
)}
</div>
{entry.harvestedGrams && (
<div className="text-sm font-medium text-emerald-600 dark:text-emerald-400">
🌿 {entry.harvestedGrams}g {t("harvestGrams")}
</div>
)}
</CardContent>
</Card>
</Link>
)
})}
</div>
)}
</div>
)
}