feat: implement flexible feeding intervals and fix layout alignment
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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}",
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
@@ -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[]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user