feat: allow sitters to mark jobs as completed with notification
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, Utensils, Trash2 } from "lucide-react"
|
import { CalendarIcon, User, Home, X, Info, Utensils, Trash2, Check } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
|
||||||
import { createBooking, deleteBooking } from "@/app/actions/booking"
|
import { createBooking, deleteBooking, completeBooking } from "@/app/actions/booking"
|
||||||
import { PlanSettings } from "@/components/plan-settings"
|
import { PlanSettings } from "@/components/plan-settings"
|
||||||
|
|
||||||
type Booking = {
|
type Booking = {
|
||||||
@@ -27,6 +27,7 @@ type Booking = {
|
|||||||
date: Date
|
date: Date
|
||||||
sitterName: string | null
|
sitterName: string | null
|
||||||
type: string
|
type: string
|
||||||
|
completedAt?: Date | string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
type Plan = {
|
type Plan = {
|
||||||
@@ -59,6 +60,15 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
|
|||||||
const [cancelReason, setCancelReason] = useState("")
|
const [cancelReason, setCancelReason] = useState("")
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const handleComplete = async (bookingId: number) => {
|
||||||
|
try {
|
||||||
|
await completeBooking(bookingId, plan.id, lang)
|
||||||
|
toast.success(dict.bookedSuccess) // reuse for now or add new toast
|
||||||
|
} catch (error) {
|
||||||
|
toast.error(dict.bookError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const dateLocale = lang === "de" ? de : enUS
|
const dateLocale = lang === "de" ? de : enUS
|
||||||
|
|
||||||
// Load saved name from localStorage
|
// Load saved name from localStorage
|
||||||
@@ -194,6 +204,7 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{booking ? (
|
{booking ? (
|
||||||
|
<div className="space-y-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isOwnerHome ? (
|
{isOwnerHome ? (
|
||||||
<>
|
<>
|
||||||
@@ -207,6 +218,28 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{booking.type === "SITTER" && (
|
||||||
|
<div className="pt-1">
|
||||||
|
{booking.completedAt ? (
|
||||||
|
<div className="flex items-center gap-2 text-green-600 font-bold bg-green-100/50 dark:bg-green-900/30 px-3 py-1.5 rounded-full border border-green-200 dark:border-green-800 w-fit text-sm">
|
||||||
|
<Check className="w-4 h-4 stroke-[3px]" />
|
||||||
|
<span>{dict.jobDone}</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="w-full flex gap-2 items-center border-green-200 hover:bg-green-100/50 text-green-700 dark:border-green-800 dark:hover:bg-green-900/40 font-semibold"
|
||||||
|
onClick={() => handleComplete(booking.id)}
|
||||||
|
>
|
||||||
|
<Check className="w-4 h-4" />
|
||||||
|
{dict.markDone}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Dialog open={isDialogOpen && isSameDay(selectedDate!, day)} onOpenChange={(open: boolean) => {
|
<Dialog open={isDialogOpen && isSameDay(selectedDate!, day)} onOpenChange={(open: boolean) => {
|
||||||
setIsDialogOpen(open)
|
setIsDialogOpen(open)
|
||||||
|
|||||||
@@ -82,3 +82,35 @@ export async function deleteBooking(bookingId: number, planId: string, lang: str
|
|||||||
|
|
||||||
revalidatePath(`/${lang}/dashboard/${planId}`)
|
revalidatePath(`/${lang}/dashboard/${planId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function completeBooking(bookingId: number, planId: string, lang: string = "en") {
|
||||||
|
const dict = await getDictionary(lang as any)
|
||||||
|
|
||||||
|
const booking = await prisma.booking.findUnique({
|
||||||
|
where: { id: bookingId },
|
||||||
|
include: { plan: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!booking) return
|
||||||
|
|
||||||
|
await prisma.booking.update({
|
||||||
|
where: { id: bookingId },
|
||||||
|
data: { completedAt: new Date() }
|
||||||
|
})
|
||||||
|
|
||||||
|
if (booking.plan.webhookUrl) {
|
||||||
|
const host = (await headers()).get("host")
|
||||||
|
const protocol = host?.includes("localhost") ? "http" : "https"
|
||||||
|
const planUrl = `${protocol}://${host}/${lang}/dashboard/${planId}`
|
||||||
|
|
||||||
|
const dateStr = booking.date.toLocaleDateString(lang)
|
||||||
|
const message = dict.notifications.completed
|
||||||
|
.replace("{name}", booking.sitterName || "Someone")
|
||||||
|
.replace("{date}", dateStr)
|
||||||
|
.replace("{url}", planUrl)
|
||||||
|
|
||||||
|
await sendNotification(booking.plan.webhookUrl, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath(`/${lang}/dashboard/${planId}`)
|
||||||
|
}
|
||||||
|
|||||||
@@ -55,7 +55,9 @@
|
|||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"cancelSuccess": "Eintrag entfernt",
|
"cancelSuccess": "Eintrag entfernt",
|
||||||
"cancelError": "Entfernen fehlgeschlagen",
|
"cancelError": "Entfernen fehlgeschlagen",
|
||||||
"noInstructions": "Noch keine spezifischen Instruktionen hinterlegt."
|
"noInstructions": "Noch keine spezifischen Instruktionen hinterlegt.",
|
||||||
|
"markDone": "Job erledigt?",
|
||||||
|
"jobDone": "Job erledigt!"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Passwort eingeben",
|
"title": "Passwort eingeben",
|
||||||
@@ -82,6 +84,7 @@
|
|||||||
"ownerHome": "🏠 BESITZER ZU HAUSE: Markiert für den {date}.\nPlan: {url}",
|
"ownerHome": "🏠 BESITZER ZU HAUSE: Markiert für den {date}.\nPlan: {url}",
|
||||||
"newBooking": "✅ NEUE BUCHUNG: {name} sittet am {date}.\nPlan: {url}",
|
"newBooking": "✅ NEUE BUCHUNG: {name} sittet am {date}.\nPlan: {url}",
|
||||||
"cancellation": "🚨 ABSAGE: {name} hat die Buchung für den {date} gelöscht.{message}\nPlan: {url}",
|
"cancellation": "🚨 ABSAGE: {name} hat die Buchung für den {date} gelöscht.{message}\nPlan: {url}",
|
||||||
"instructionsUpdated": "📝 UPDATE: Die Katzen-Instruktionen wurden geändert.\nPlan: {url}"
|
"instructionsUpdated": "📝 UPDATE: Die Katzen-Instruktionen wurden geändert.\nPlan: {url}",
|
||||||
|
"completed": "✨ ERLEDIGT: {name} hat den Job für den {date} erledigt!\nPlan: {url}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -54,8 +54,10 @@
|
|||||||
"cancelSubmit": "Confirm Cancellation",
|
"cancelSubmit": "Confirm Cancellation",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"cancelSuccess": "Entry removed",
|
"cancelSuccess": "Entry removed",
|
||||||
"cancelError": "Failed to remove entry",
|
"cancelError": "Cancellation failed",
|
||||||
"noInstructions": "No specific instructions provided yet."
|
"noInstructions": "No specific instructions added yet.",
|
||||||
|
"markDone": "Job done?",
|
||||||
|
"jobDone": "Job done!"
|
||||||
},
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Enter Password",
|
"title": "Enter Password",
|
||||||
@@ -81,7 +83,8 @@
|
|||||||
"notifications": {
|
"notifications": {
|
||||||
"ownerHome": "🏠 OWNER HOME: Marked for {date}.\nPlan: {url}",
|
"ownerHome": "🏠 OWNER HOME: Marked for {date}.\nPlan: {url}",
|
||||||
"newBooking": "✅ NEW BOOKING: {name} is sitting on {date}.\nPlan: {url}",
|
"newBooking": "✅ NEW BOOKING: {name} is sitting on {date}.\nPlan: {url}",
|
||||||
"cancellation": "🚨 CANCELLATION: {name} removed their booking for {date}.{message}\nPlan: {url}",
|
"cancellation": "🚨 CANCELLATION: {name} deleted the booking for {date}.{message}\nPlan: {url}",
|
||||||
"instructionsUpdated": "📝 UPDATED: Cat instructions have been modified.\nPlan: {url}"
|
"instructionsUpdated": "📝 UPDATE: Cat instructions were changed.\nPlan: {url}",
|
||||||
|
"completed": "✨ DONE: {name} finished the job for {date}!\nPlan: {url}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Booking" ADD COLUMN "completedAt" DATETIME;
|
||||||
@@ -30,6 +30,7 @@ model Booking {
|
|||||||
date DateTime
|
date DateTime
|
||||||
sitterName String?
|
sitterName String?
|
||||||
type String @default("SITTER") // "SITTER" or "OWNER_HOME"
|
type String @default("SITTER") // "SITTER" or "OWNER_HOME"
|
||||||
|
completedAt DateTime?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
|
|
||||||
@@unique([planId, date, sitterName])
|
@@unique([planId, date, sitterName])
|
||||||
|
|||||||
Reference in New Issue
Block a user