076fd6f9b3
- 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
139 lines
4.8 KiB
TypeScript
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>
|
|
)
|
|
}
|