feat: implement flexible feeding intervals and fix layout alignment

This commit is contained in:
2026-01-12 23:06:12 +01:00
parent 87f4e43c9f
commit 3600ba665d
8 changed files with 149 additions and 5 deletions

View File

@@ -3,7 +3,7 @@
import { useState, useEffect } from "react" import { useState, useEffect } from "react"
import { format, eachDayOfInterval, isSameDay } from "date-fns" import { format, eachDayOfInterval, isSameDay } from "date-fns"
import { de, enUS } from "date-fns/locale" import { de, enUS } from "date-fns/locale"
import { CalendarIcon, User, Home, X, Info } from "lucide-react" import { CalendarIcon, User, Home, X, Info, Utensils, Trash2 } from "lucide-react"
import { toast } from "sonner" import { toast } from "sonner"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
@@ -37,6 +37,9 @@ type Plan = {
webhookUrl: string | null webhookUrl: string | null
notifyAll: boolean notifyAll: boolean
bookings: Booking[] bookings: Booking[]
feedingPerDay: number
feedingInterval: number
litterInterval: number
} }
interface PlanDashboardProps { interface PlanDashboardProps {
@@ -255,6 +258,21 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
</DialogContent> </DialogContent>
</Dialog> </Dialog>
)} )}
<div className="mt-3 flex flex-wrap gap-2 pt-2 border-t border-border/50">
{Math.floor((day.getTime() - new Date(plan.startDate).setHours(0, 0, 0, 0)) / (1000 * 60 * 60 * 24)) % plan.feedingInterval === 0 && (
<div className="flex gap-1 items-center" title={`${plan.feedingPerDay}x ${dict.feeding}`}>
{Array.from({ length: plan.feedingPerDay }).map((_, i) => (
<Utensils key={i} className="w-3.5 h-3.5 text-orange-500/70" />
))}
</div>
)}
{Math.floor((day.getTime() - new Date(plan.startDate).setHours(0, 0, 0, 0)) / (1000 * 60 * 60 * 24)) % plan.litterInterval === 0 && (
<div className="flex items-center gap-1 text-xs text-muted-foreground/70" title={dict.litter}>
<Trash2 className="w-3.5 h-3.5 text-blue-500/70" />
</div>
)}
</div>
</div> </div>
) )
})} })}

View File

@@ -4,7 +4,7 @@ import prisma from '@/lib/prisma';
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const body = await req.json(); const body = await req.json();
const { title, startDate, endDate, password, instructions } = body; const { title, startDate, endDate, password, instructions, feedingPerDay, feedingInterval, litterInterval } = body;
if (!title || !startDate || !endDate || !password) { if (!title || !startDate || !endDate || !password) {
return NextResponse.json({ error: 'Missing required fields' }, { status: 400 }); return NextResponse.json({ error: 'Missing required fields' }, { status: 400 });
@@ -17,6 +17,9 @@ export async function POST(req: Request) {
endDate: new Date(endDate), endDate: new Date(endDate),
password, password,
instructions, instructions,
feedingPerDay: feedingPerDay ? parseInt(feedingPerDay) : undefined,
feedingInterval: feedingInterval ? parseInt(feedingInterval) : undefined,
litterInterval: litterInterval ? parseInt(litterInterval) : undefined,
}, },
}); });

View File

@@ -43,21 +43,33 @@ export function CreatePlanForm({ dict, lang }: CreatePlanFormProps) {
password: z.string().min(4, { password: z.string().min(4, {
message: dict.passwordError, message: dict.passwordError,
}), }),
feedingPerDay: z.coerce.number().min(1).max(10),
feedingInterval: z.coerce.number().min(1).max(30),
litterInterval: z.coerce.number().min(1).max(30),
instructions: z.string().optional(), instructions: z.string().optional(),
}) })
const form = useForm<z.infer<typeof formSchema>>({ type FormValues = z.infer<typeof formSchema>
resolver: zodResolver(formSchema),
const form = useForm<FormValues>({
resolver: zodResolver(formSchema) as any,
defaultValues: { defaultValues: {
title: "", title: "",
password: "", password: "",
instructions: "", instructions: "",
feedingPerDay: 2,
feedingInterval: 1,
litterInterval: 2,
dateRange: {
from: undefined,
to: undefined,
}
}, },
}) })
const dateLocale = lang === "de" ? de : enUS const dateLocale = lang === "de" ? de : enUS
async function onSubmit(values: z.infer<typeof formSchema>) { async function onSubmit(values: FormValues) {
try { try {
const response = await fetch("/api/plan", { const response = await fetch("/api/plan", {
method: "POST", method: "POST",
@@ -68,6 +80,9 @@ export function CreatePlanForm({ dict, lang }: CreatePlanFormProps) {
endDate: values.dateRange.to, endDate: values.dateRange.to,
password: values.password, password: values.password,
instructions: values.instructions, instructions: values.instructions,
feedingPerDay: values.feedingPerDay,
feedingInterval: values.feedingInterval,
litterInterval: values.litterInterval,
}), }),
}) })
@@ -153,6 +168,52 @@ export function CreatePlanForm({ dict, lang }: CreatePlanFormProps) {
</FormItem> </FormItem>
)} )}
/> />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<FormField
control={form.control}
name="feedingInterval"
render={({ field }) => (
<FormItem>
<FormLabel className="min-h-[3.5rem] flex items-end pb-2">{dict.feedingInterval}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription className="min-h-[4rem]">{dict.feedingIntervalDesc}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="feedingPerDay"
render={({ field }) => (
<FormItem>
<FormLabel className="min-h-[3.5rem] flex items-end pb-2">{dict.feedingPerDay}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription className="min-h-[4rem]">{dict.feedingDesc}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="litterInterval"
render={({ field }) => (
<FormItem>
<FormLabel className="min-h-[3.5rem] flex items-end pb-2">{dict.litterInterval}</FormLabel>
<FormControl>
<Input type="number" {...field} />
</FormControl>
<FormDescription className="min-h-[4rem]">{dict.litterDesc}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="password" name="password"

View File

@@ -15,6 +15,12 @@
"groupPassword": "Gruppen-Passwort", "groupPassword": "Gruppen-Passwort",
"passwordPlaceholder": "geheim-miau", "passwordPlaceholder": "geheim-miau",
"passwordDesc": "Teile dieses Passwort mit deinen Katzen-Sittern.", "passwordDesc": "Teile dieses Passwort mit deinen Katzen-Sittern.",
"feedingPerDay": "Wie oft am Fütterungstag füttern?",
"feedingDesc": "Anzahl der Mahlzeiten pro Tag.",
"feedingInterval": "Alle wieviel Tage füttern?",
"feedingIntervalDesc": "Intervall in Tagen (z.B. 1 für täglich, 3 für Futterautomat).",
"litterInterval": "Alle wieviel Tage das Klo reinigen?",
"litterDesc": "Intervall in Tagen.",
"submit": "Plan erstellen", "submit": "Plan erstellen",
"success": "Plan erfolgreich erstellt!", "success": "Plan erfolgreich erstellt!",
"error": "Etwas ist schiefgelaufen. Bitte versuche es erneut.", "error": "Etwas ist schiefgelaufen. Bitte versuche es erneut.",
@@ -27,6 +33,8 @@
"instructionsTitle": "Katzenpflege-Anleitungen", "instructionsTitle": "Katzenpflege-Anleitungen",
"export": "Exportieren", "export": "Exportieren",
"settings": "Einstellungen", "settings": "Einstellungen",
"feeding": "Füttern",
"litter": "Klo reinigen",
"ownerHome": "Besitzer zu Hause", "ownerHome": "Besitzer zu Hause",
"illDoIt": "Ich mache das!", "illDoIt": "Ich mache das!",
"bookTitle": "Buchung für den {date}", "bookTitle": "Buchung für den {date}",

View File

@@ -15,6 +15,12 @@
"groupPassword": "Group Password", "groupPassword": "Group Password",
"passwordPlaceholder": "secret-meow", "passwordPlaceholder": "secret-meow",
"passwordDesc": "Share this password with your cat sitters.", "passwordDesc": "Share this password with your cat sitters.",
"feedingPerDay": "Feeding times per feeding day",
"feedingDesc": "Number of meals per day.",
"feedingInterval": "Feeding interval (days)",
"feedingIntervalDesc": "Every X days (e.g., 1 for daily, 3 for auto-feeder).",
"litterInterval": "Litter cleaning interval (days)",
"litterDesc": "Every X days.",
"submit": "Create Plan", "submit": "Create Plan",
"success": "Plan created successfully!", "success": "Plan created successfully!",
"error": "Something went wrong. Please try again.", "error": "Something went wrong. Please try again.",
@@ -27,6 +33,8 @@
"instructionsTitle": "Cat Care Instructions", "instructionsTitle": "Cat Care Instructions",
"export": "Export", "export": "Export",
"settings": "Settings", "settings": "Settings",
"feeding": "Feeding",
"litter": "Clean litter",
"ownerHome": "Owner Home", "ownerHome": "Owner Home",
"illDoIt": "I'll do it!", "illDoIt": "I'll do it!",
"bookTitle": "Book {date}", "bookTitle": "Book {date}",

View File

@@ -0,0 +1,21 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Plan" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"password" TEXT NOT NULL,
"startDate" DATETIME NOT NULL,
"endDate" DATETIME NOT NULL,
"instructions" TEXT,
"webhookUrl" TEXT,
"notifyAll" BOOLEAN NOT NULL DEFAULT true,
"feedingPerDay" INTEGER NOT NULL DEFAULT 2,
"litterInterval" INTEGER NOT NULL DEFAULT 2,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_Plan" ("createdAt", "endDate", "id", "instructions", "notifyAll", "password", "startDate", "title", "webhookUrl") SELECT "createdAt", "endDate", "id", "instructions", "notifyAll", "password", "startDate", "title", "webhookUrl" FROM "Plan";
DROP TABLE "Plan";
ALTER TABLE "new_Plan" RENAME TO "Plan";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -0,0 +1,22 @@
-- RedefineTables
PRAGMA defer_foreign_keys=ON;
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_Plan" (
"id" TEXT NOT NULL PRIMARY KEY,
"title" TEXT NOT NULL,
"password" TEXT NOT NULL,
"startDate" DATETIME NOT NULL,
"endDate" DATETIME NOT NULL,
"instructions" TEXT,
"webhookUrl" TEXT,
"notifyAll" BOOLEAN NOT NULL DEFAULT true,
"feedingPerDay" INTEGER NOT NULL DEFAULT 2,
"feedingInterval" INTEGER NOT NULL DEFAULT 1,
"litterInterval" INTEGER NOT NULL DEFAULT 2,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO "new_Plan" ("createdAt", "endDate", "feedingPerDay", "id", "instructions", "litterInterval", "notifyAll", "password", "startDate", "title", "webhookUrl") SELECT "createdAt", "endDate", "feedingPerDay", "id", "instructions", "litterInterval", "notifyAll", "password", "startDate", "title", "webhookUrl" FROM "Plan";
DROP TABLE "Plan";
ALTER TABLE "new_Plan" RENAME TO "Plan";
PRAGMA foreign_keys=ON;
PRAGMA defer_foreign_keys=OFF;

View File

@@ -16,6 +16,9 @@ model Plan {
instructions String? instructions String?
webhookUrl String? webhookUrl String?
notifyAll Boolean @default(true) notifyAll Boolean @default(true)
feedingPerDay Int @default(2)
feedingInterval Int @default(1)
litterInterval Int @default(2)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
bookings Booking[] bookings Booking[]
} }