159 lines
5.4 KiB
TypeScript
159 lines
5.4 KiB
TypeScript
import { db } from "@/lib/db";
|
|
import { isOverdue, isDueSoon, describeRecurrence } from "@/lib/tasks/schedule";
|
|
import { formatDate } from "@/lib/utils";
|
|
import { AddTaskButton } from "@/components/tasks/add-task-button";
|
|
import { CompleteTaskButton } from "@/components/tasks/complete-task-button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { CalendarDays, Leaf, Home, Wrench, CheckCircle2, AlertCircle, Clock } from "lucide-react";
|
|
import Link from "next/link";
|
|
|
|
export const metadata = { title: "Reminders" };
|
|
|
|
const CATEGORY_ICONS = {
|
|
GARDEN: Leaf,
|
|
HOME: Home,
|
|
EQUIPMENT: Wrench,
|
|
OTHER: Clock,
|
|
};
|
|
|
|
const CATEGORY_LABELS = {
|
|
GARDEN: "Garden",
|
|
HOME: "Home",
|
|
EQUIPMENT: "Equipment",
|
|
OTHER: "Other",
|
|
};
|
|
|
|
export default async function TasksPage() {
|
|
const tasks = await db.task.findMany({
|
|
where: { active: true },
|
|
orderBy: { nextDue: "asc" },
|
|
include: { plant: { select: { id: true, commonName: true, variety: true } } },
|
|
});
|
|
|
|
const overdue = tasks.filter((t) => isOverdue(t.nextDue));
|
|
const upcoming = tasks.filter((t) => !isOverdue(t.nextDue) && isDueSoon(t.nextDue, 60));
|
|
const later = tasks.filter((t) => !isOverdue(t.nextDue) && !isDueSoon(t.nextDue, 60));
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="font-display text-2xl font-semibold">Reminders</h1>
|
|
<p className="text-sm text-muted-foreground mt-0.5">
|
|
{overdue.length > 0
|
|
? `${overdue.length} overdue · ${upcoming.length} coming up`
|
|
: `${upcoming.length} coming up in the next 60 days`}
|
|
</p>
|
|
</div>
|
|
<AddTaskButton />
|
|
</div>
|
|
|
|
{tasks.length === 0 && (
|
|
<div className="text-center py-16 text-muted-foreground">
|
|
<CalendarDays className="h-12 w-12 mx-auto mb-4 opacity-20" />
|
|
<p className="font-medium">No reminders yet</p>
|
|
<p className="text-sm mt-1">Add your first reminder to start tracking what needs doing.</p>
|
|
</div>
|
|
)}
|
|
|
|
{overdue.length > 0 && (
|
|
<Section title="Overdue" icon={AlertCircle} iconClass="text-destructive">
|
|
{overdue.map((task) => (
|
|
<TaskCard key={task.id} task={task} overdue />
|
|
))}
|
|
</Section>
|
|
)}
|
|
|
|
{upcoming.length > 0 && (
|
|
<Section title="Coming up" icon={CalendarDays} iconClass="text-[hsl(var(--leaf))]">
|
|
{upcoming.map((task) => (
|
|
<TaskCard key={task.id} task={task} />
|
|
))}
|
|
</Section>
|
|
)}
|
|
|
|
{later.length > 0 && (
|
|
<Section title="Later" icon={Clock} iconClass="text-muted-foreground">
|
|
{later.map((task) => (
|
|
<TaskCard key={task.id} task={task} />
|
|
))}
|
|
</Section>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Section({ title, icon: Icon, iconClass, children }: {
|
|
title: string;
|
|
icon: React.ElementType;
|
|
iconClass: string;
|
|
children: React.ReactNode;
|
|
}) {
|
|
return (
|
|
<div className="space-y-2">
|
|
<div className="flex items-center gap-2 mb-3">
|
|
<Icon className={`h-4 w-4 ${iconClass}`} />
|
|
<h2 className="font-display text-lg font-semibold">{title}</h2>
|
|
</div>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function TaskCard({ task, overdue = false }: {
|
|
task: Awaited<ReturnType<typeof db.task.findMany>>[0] & {
|
|
plant: { id: string; commonName: string; variety: string | null } | null;
|
|
};
|
|
overdue?: boolean;
|
|
}) {
|
|
const Icon = CATEGORY_ICONS[task.category];
|
|
const daysUntil = Math.ceil((new Date(task.nextDue).getTime() - Date.now()) / 86400000);
|
|
|
|
return (
|
|
<Card className={overdue ? "border-destructive/40" : ""}>
|
|
<CardContent className="py-3 px-4 flex items-start gap-3">
|
|
<Icon className={`h-4 w-4 mt-0.5 shrink-0 ${overdue ? "text-destructive" : "text-muted-foreground"}`} />
|
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-start gap-2 flex-wrap">
|
|
<p className="font-medium text-sm">{task.title}</p>
|
|
<Badge variant="secondary" className="text-xs shrink-0">
|
|
{CATEGORY_LABELS[task.category]}
|
|
</Badge>
|
|
</div>
|
|
|
|
{task.plant && (
|
|
<Link href={`/garden/${task.plant.id}`} className="text-xs text-[hsl(var(--leaf))] hover:underline">
|
|
{task.plant.commonName}{task.plant.variety ? ` · ${task.plant.variety}` : ""}
|
|
</Link>
|
|
)}
|
|
|
|
{task.notes && (
|
|
<p className="text-xs text-muted-foreground mt-0.5">{task.notes}</p>
|
|
)}
|
|
|
|
<p className="text-xs text-muted-foreground mt-1">
|
|
{describeRecurrence(task.recurrence, task.intervalDays, task.month)}
|
|
{task.lastDone && ` · Last done ${formatDate(task.lastDone)}`}
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2 shrink-0">
|
|
<div className="text-right">
|
|
<p className={`text-xs font-medium ${overdue ? "text-destructive" : "text-muted-foreground"}`}>
|
|
{overdue
|
|
? `${Math.abs(daysUntil)}d overdue`
|
|
: daysUntil === 0 ? "Today"
|
|
: daysUntil === 1 ? "Tomorrow"
|
|
: `In ${daysUntil} days`}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground">{formatDate(task.nextDue)}</p>
|
|
</div>
|
|
<CompleteTaskButton taskId={task.id} taskTitle={task.title} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|