Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/ui/hooks/useBooking.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,33 @@ describe('useBooking', () => {
expect(result.current.missingFields).toContain('project')
})

it('snaps a non-quarter start to the nearest quarter and keeps the duration', () => {
// A concept block at 16:11–16:41 has times no <select> option matches, so the
// dropdowns fell back to their first entry (07:00). Snap to 16:15 + 30m → 16:45.
const { result } = renderHook(() => useBooking({ startTime: '16:11', endTime: '16:41' }))
expect(result.current.startTime).toBe('16:15')
expect(result.current.endTime).toBe('16:45')
})

it('rounds a non-quarter duration up to a whole quarter so TOT lands on an option', () => {
const { result } = renderHook(() => useBooking({ startTime: '10:02', endTime: '10:39' }))
// 10:02 → 10:00; 37m → 30m → 10:30
expect(result.current.startTime).toBe('10:00')
expect(result.current.endTime).toBe('10:30')
})

it('leaves already-quarter-aligned times untouched', () => {
const { result } = renderHook(() => useBooking({ startTime: '09:00', endTime: '11:00' }))
expect(result.current.startTime).toBe('09:00')
expect(result.current.endTime).toBe('11:00')
})

it('keeps the default times when no start is provided', () => {
const { result } = renderHook(() => useBooking())
expect(result.current.startTime).toBe('09:00')
expect(result.current.endTime).toBe('09:30')
})

it('loads services when a project is selected', async () => {
const { result } = renderHook(() => useBooking())
await act(async () => {
Expand Down
38 changes: 36 additions & 2 deletions src/ui/hooks/useBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ const SIMPLICATE_BASE_URL = import.meta.env.VITE_SIMPLICATE_BASE_URL as string

export type BookingStatus = 'idle' | 'loading' | 'success' | 'error'

const QUARTER_MIN = 15

function timeToMinutes(time: string): number {
const [h, m] = time.split(':').map(Number)
return h! * 60 + m!
}

function minutesToTime(min: number): string {
const h = Math.floor(min / 60).toString().padStart(2, '0')
const m = (min % 60).toString().padStart(2, '0')
return `${h}:${m}`
}

/**
* The VAN/TOT dropdowns only offer quarter-hour options (see TimeSelect), so a
* raw block time like 16:11 matches no <option> and the <select> silently falls
* back to its first entry — which is why concept blocks showed up as 07:00–07:00.
* Snap the start to the nearest quarter and carry the original duration (also
* quarter-rounded, min 15m) so both dropdowns land on a real option and the
* booked range still reflects the block. With no start, fall back to 09:00–09:30.
*/
function snapInitialTimes(start?: string, end?: string): { startTime: string; endTime: string } {
if (!start) return { startTime: '09:00', endTime: '09:30' }
const startMin = timeToMinutes(start)
const snappedStart = Math.round(startMin / QUARTER_MIN) * QUARTER_MIN
const rawDuration = end ? timeToMinutes(end) - startMin : 30
const duration = Math.max(QUARTER_MIN, Math.round(rawDuration / QUARTER_MIN) * QUARTER_MIN)
return {
startTime: minutesToTime(snappedStart),
endTime: minutesToTime(snappedStart + duration),
}
}

function sortProjects(projects: SimplicateProject[], starredIds: ReadonlySet<string>): { sorted: SimplicateProject[]; lastStarredId: string | undefined } {
const starred = projects
.filter(p => starredIds.has(p.id))
Expand All @@ -31,8 +64,9 @@ export function useBooking(initial: Partial<HourEntry> = {}) {
const [serviceId, setServiceId] = useState(initial.projectServiceId ?? '')
const [hourTypeId, setHourTypeId] = useState(initial.hourTypeId ?? '')
const [note, setNote] = useState(initial.note ?? '')
const [startTime, setStartTime] = useState(initial.startTime ?? '09:00')
const [endTime, setEndTime] = useState(initial.endTime ?? '09:30')
const snapped = snapInitialTimes(initial.startTime, initial.endTime)
const [startTime, setStartTime] = useState(snapped.startTime)
const [endTime, setEndTime] = useState(snapped.endTime)
const [date, setDate] = useState(initial.startDate ?? new Date().toISOString().split('T')[0]!)
const [services, setServices] = useState<{ id: string; name: string; hourTypeIds: string[] }[]>([])
const [status, setStatus] = useState<BookingStatus>('idle')
Expand Down
Loading