From 97d8f12fc07c1aa3de013deb0dd0b5a65bee4ecc Mon Sep 17 00:00:00 2001 From: elpatron Date: Tue, 13 Jan 2026 11:41:21 +0100 Subject: [PATCH] feat(job-completion): add message and photo upload to job completion --- README.md | 6 +- .../[planId]/_components/plan-dashboard.tsx | 122 +++++++++++++++++- app/actions/booking.ts | 19 ++- app/actions/upload-image.ts | 50 +++++++ dictionaries/de.json | 9 +- dictionaries/en.json | 9 +- docker-compose.yml | 1 + lib/notifications.ts | 43 ++++-- package-lock.json | 21 +++ package.json | 4 +- .../migration.sql | 3 + prisma/schema.prisma | 3 + .../4d34a526-456e-4f09-816d-211dc1d81b58.jpeg | Bin 0 -> 132814 bytes .../5f6138f9-3fee-4426-ad9d-e5d8a7ecd1b6.jpeg | Bin 0 -> 132814 bytes 14 files changed, 265 insertions(+), 25 deletions(-) create mode 100644 app/actions/upload-image.ts create mode 100644 prisma/migrations/20260113102925_add_completion_details/migration.sql create mode 100644 public/uploads/cmkcggt3h0000m22q5b4eefey/4d34a526-456e-4f09-816d-211dc1d81b58.jpeg create mode 100644 public/uploads/cmkcgiynz0001m22q5jq74ma3/5f6138f9-3fee-4426-ad9d-e5d8a7ecd1b6.jpeg diff --git a/README.md b/README.md index e363e67..a56276b 100644 --- a/README.md +++ b/README.md @@ -76,21 +76,25 @@ Mit **Docker CLI**: # Image bauen docker build -t cat-sitting-planner . +# Container starten # Container starten docker run -d \ --name cat-sitting-planner \ -p 3000:3000 \ -v /pfad/zum/host/data:/app/data \ + -v /pfad/zum/host/uploads:/app/public/uploads \ --restart always \ cat-sitting-planner ``` ### 2. Datenpersistenz (Wichtig) Die Daten liegen in `/app/data/dev.db`. -Mappe diesen Ordner unbedingt auf ein lokales Volume: +Bilder werden in `/app/public/uploads` gespeichert. +Mappe diese Ordner unbedingt auf lokale Volumes: ```yaml volumes: - /pfad/zum/host/data:/app/data + - /pfad/zum/host/uploads:/app/public/uploads ``` --- diff --git a/app/[lang]/dashboard/[planId]/_components/plan-dashboard.tsx b/app/[lang]/dashboard/[planId]/_components/plan-dashboard.tsx index 8506eed..29f100e 100644 --- a/app/[lang]/dashboard/[planId]/_components/plan-dashboard.tsx +++ b/app/[lang]/dashboard/[planId]/_components/plan-dashboard.tsx @@ -16,6 +16,10 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog" +import { Textarea } from "@/components/ui/textarea" +import { uploadImage } from "@/app/actions/upload-image" +import { Camera, Upload } from "lucide-react" + import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" @@ -61,12 +65,56 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP const [cancelReason, setCancelReason] = useState("") const [isSubmitting, setIsSubmitting] = useState(false) - const handleComplete = async (bookingId: number) => { + // Completion Dialog State + const [isCompleteDialogOpen, setIsCompleteDialogOpen] = useState(false) + const [bookingToComplete, setBookingToComplete] = useState(null) + const [completionMessage, setCompletionMessage] = useState("") + const [completionImage, setCompletionImage] = useState(null) + const [imagePreview, setImagePreview] = useState(null) + + const handleCompleteClick = (bookingId: number) => { + setBookingToComplete(bookingId) + setCompletionMessage("") + setCompletionImage(null) + setImagePreview(null) + setIsCompleteDialogOpen(true) + } + + const handleImageChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (file) { + setCompletionImage(file) + const reader = new FileReader() + reader.onloadend = () => { + setImagePreview(reader.result as string) + } + reader.readAsDataURL(file) + } + } + + const handleConfirmComplete = async () => { + if (!bookingToComplete) return + + setIsSubmitting(true) try { - await completeBooking(bookingId, plan.id, lang) - toast.success(dict.bookedSuccess) // reuse for now or add new toast + let imageUrl: string | undefined = undefined + + if (completionImage) { + const formData = new FormData() + formData.append("file", completionImage) + formData.append("planId", plan.id) + imageUrl = await uploadImage(formData) + } + + await completeBooking(bookingToComplete, plan.id, lang, completionMessage, imageUrl) + toast.success(dict.jobDone) + setIsCompleteDialogOpen(false) + setBookingToComplete(null) } catch (error) { + console.error(error) toast.error(dict.bookError) + } finally { + setIsSubmitting(false) } } @@ -238,7 +286,7 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP 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)} + onClick={() => handleCompleteClick(booking.id)} > {dict.markDone} @@ -352,6 +400,72 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP + + {/* Completion Dialog */} + + + + {dict.completeTitle} + + {dict.completeDesc} + + + +
+
+ +