feat(job-completion): add message and photo upload to job completion
This commit is contained in:
@@ -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<number | null>(null)
|
||||
const [completionMessage, setCompletionMessage] = useState("")
|
||||
const [completionImage, setCompletionImage] = useState<File | null>(null)
|
||||
const [imagePreview, setImagePreview] = useState<string | null>(null)
|
||||
|
||||
const handleCompleteClick = (bookingId: number) => {
|
||||
setBookingToComplete(bookingId)
|
||||
setCompletionMessage("")
|
||||
setCompletionImage(null)
|
||||
setImagePreview(null)
|
||||
setIsCompleteDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
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)}
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
{dict.markDone}
|
||||
@@ -352,6 +400,72 @@ export function PlanDashboard({ plan, dict, settingsDict, lang }: PlanDashboardP
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Completion Dialog */}
|
||||
<Dialog open={isCompleteDialogOpen} onOpenChange={setIsCompleteDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{dict.completeTitle}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{dict.completeDesc}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="message">{dict.completeMessage}</Label>
|
||||
<Textarea
|
||||
id="message"
|
||||
value={completionMessage}
|
||||
onChange={(e) => setCompletionMessage(e.target.value)}
|
||||
placeholder={dict.completeMessagePlaceholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="photo">{dict.completePhoto}</Label>
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" className="w-12 h-12" onClick={() => document.getElementById('photo-upload')?.click()}>
|
||||
<Camera className="w-6 h-6" />
|
||||
</Button>
|
||||
<Input
|
||||
id="photo-upload"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
capture="environment"
|
||||
className="hidden"
|
||||
onChange={handleImageChange}
|
||||
/>
|
||||
{imagePreview && (
|
||||
<div className="relative w-20 h-20 rounded-md overflow-hidden border">
|
||||
<img src={imagePreview} alt="Preview" className="w-full h-full object-cover" />
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-0 right-0 h-5 w-5 bg-background/50 hover:bg-background"
|
||||
onClick={() => {
|
||||
setCompletionImage(null)
|
||||
setImagePreview(null)
|
||||
}}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setIsCompleteDialogOpen(false)} disabled={isSubmitting}>
|
||||
{dict.cancel}
|
||||
</Button>
|
||||
<Button onClick={handleConfirmComplete} disabled={isSubmitting}>
|
||||
{isSubmitting ? dict.completing : dict.completeSubmit}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user