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} + + + +
+
+ +