Dev #3
@@ -39,7 +39,9 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) {
|
||||
|
||||
const [facilityId, setFacilityId] = React.useState(adhocFacilities[0]?.facilityId ?? "")
|
||||
const [startTime, setStartTime] = React.useState(() => toLocalInputValue(new Date()))
|
||||
const [duration, setDuration] = React.useState("60")
|
||||
const [hours, setHours] = React.useState("1")
|
||||
const [minutes, setMinutes] = React.useState("0")
|
||||
const [pricePerHour, setPricePerHour] = React.useState(() => String(adhocFacilities[0]?.pricePerHour ?? 0))
|
||||
const [partySize, setPartySize] = React.useState("")
|
||||
const [guestName, setGuestName] = React.useState("")
|
||||
const [guestPhone, setGuestPhone] = React.useState("")
|
||||
@@ -49,13 +51,19 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) {
|
||||
const { busy: checking, run: runCheck } = useMutation()
|
||||
const { busy: booking, run: runBook } = useMutation()
|
||||
|
||||
const selectedFacility = adhocFacilities.find((f) => f.facilityId === facilityId)
|
||||
const durationMinutes = Number(duration) || 0
|
||||
const estimatedPrice = selectedFacility ? (selectedFacility.pricePerHour * durationMinutes) / 60 : 0
|
||||
const durationMinutes = (Number(hours) || 0) * 60 + (Number(minutes) || 0)
|
||||
const bookingPrice = ((Number(pricePerHour) || 0) * durationMinutes) / 60
|
||||
|
||||
const facilityOptions = adhocFacilities.map((f) => ({ label: f.name, value: f.facilityId }))
|
||||
const canCheck = !!facilityId && !!startTime && durationMinutes >= 1 && !checking
|
||||
const canSubmit = canCheck && !!guestName.trim() && !!guestPhone.trim() && !booking
|
||||
const canSubmit = canCheck && Number(pricePerHour) >= 0 && !!guestName.trim() && !!guestPhone.trim() && !booking
|
||||
|
||||
function selectFacility(id: string) {
|
||||
setFacilityId(id)
|
||||
setAvailability(null)
|
||||
const facility = adhocFacilities.find((f) => f.facilityId === id)
|
||||
setPricePerHour(String(facility?.pricePerHour ?? 0))
|
||||
}
|
||||
|
||||
async function handleCheckAvailability() {
|
||||
if (!canCheck) return
|
||||
@@ -87,6 +95,7 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) {
|
||||
guestName: guestName.trim(),
|
||||
guestPhone: guestPhone.trim(),
|
||||
partySize: partySize ? Number(partySize) : undefined,
|
||||
pricePerHour: Number(pricePerHour),
|
||||
})
|
||||
)
|
||||
if (bookingResult.shareUrl) {
|
||||
@@ -125,14 +134,7 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) {
|
||||
<Label>
|
||||
Facility <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
items={facilityOptions}
|
||||
value={facilityId}
|
||||
onValueChange={(v) => {
|
||||
setFacilityId(v ?? "")
|
||||
setAvailability(null)
|
||||
}}
|
||||
>
|
||||
<Select items={facilityOptions} value={facilityId} onValueChange={(v) => selectFacility(v ?? "")}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select facility" />
|
||||
</SelectTrigger>
|
||||
@@ -146,60 +148,92 @@ function AdhocBookingTab({ facilities }: AdhocBookingTabProps) {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="adhoc-start">
|
||||
Start <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="adhoc-start"
|
||||
type="datetime-local"
|
||||
value={startTime}
|
||||
onChange={(e) => {
|
||||
setStartTime(e.target.value)
|
||||
setAvailability(null)
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="adhoc-duration">
|
||||
Duration (minutes) <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="adhoc-duration"
|
||||
type="number"
|
||||
min={1}
|
||||
value={duration}
|
||||
onChange={(e) => {
|
||||
setDuration(e.target.value)
|
||||
setAvailability(null)
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="adhoc-start">
|
||||
Start <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="adhoc-start"
|
||||
type="datetime-local"
|
||||
value={startTime}
|
||||
onChange={(e) => {
|
||||
setStartTime(e.target.value)
|
||||
setAvailability(null)
|
||||
}}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>
|
||||
Duration <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
aria-label="Hours"
|
||||
value={hours}
|
||||
onChange={(e) => {
|
||||
setHours(e.target.value)
|
||||
setAvailability(null)
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">hours</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
max={59}
|
||||
aria-label="Minutes"
|
||||
value={minutes}
|
||||
onChange={(e) => {
|
||||
setMinutes(e.target.value)
|
||||
setAvailability(null)
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">minutes</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="adhoc-party-size">Party Size</Label>
|
||||
<Label htmlFor="adhoc-price-per-hour">
|
||||
Price / Hour (Rs.) <span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Input
|
||||
id="adhoc-party-size"
|
||||
id="adhoc-price-per-hour"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Optional headcount"
|
||||
value={partySize}
|
||||
onChange={(e) => setPartySize(e.target.value)}
|
||||
min={0}
|
||||
step="0.01"
|
||||
value={pricePerHour}
|
||||
onChange={(e) => setPricePerHour(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label>Estimated Price</Label>
|
||||
<Label>Price for Booking</Label>
|
||||
<div className="flex h-9 items-center rounded-md border bg-muted/40 px-3 text-sm font-medium">
|
||||
Rs. {estimatedPrice.toLocaleString(undefined, { maximumFractionDigits: 2 })}
|
||||
Rs. {bookingPrice.toLocaleString(undefined, { maximumFractionDigits: 2 })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="adhoc-party-size">Party Size</Label>
|
||||
<Input
|
||||
id="adhoc-party-size"
|
||||
type="number"
|
||||
min={1}
|
||||
placeholder="Optional headcount"
|
||||
value={partySize}
|
||||
onChange={(e) => setPartySize(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end gap-3">
|
||||
<Button type="button" variant="outline" onClick={handleCheckAvailability} disabled={!canCheck}>
|
||||
{checking ? <Spinner className="size-4" /> : <SearchIcon />}
|
||||
|
||||
@@ -67,6 +67,8 @@ export interface CreateGuestAdhocBookingInput {
|
||||
guestName: string
|
||||
guestPhone: string
|
||||
partySize?: number
|
||||
/** Overrides the facility's own price per hour for this booking's charge only. */
|
||||
pricePerHour?: number
|
||||
}
|
||||
|
||||
export interface ReassignBookingInput {
|
||||
|
||||
Reference in New Issue
Block a user