Schedule Conflict Checks as a Warning, Not a Wall
A booking form that warns about schedule clashes without blocking: read-only endpoint, inclusive overlap query, debounce and abort.
TL;DR
A hall got double-booked because two officers approved overlapping applications without checking each other, and the old form couldn't flag taken dates. The shipped fix pairs a validated, read-only endpoint that flags overlapping schedules with a debounced, race-safe warning box on the form. The warning never blocks submission; the officer still decides.
Two rental applications for the same hall, overlapping dates, and both passed because two different officers approved them back to back without checking each other. The old form had no way to say that the chosen date range was already taken. The clash only surfaced on the dashboard, after both files were in.
The first idea: block the submit button when a conflict is detected. That was rejected. Facility availability is advisory; exceptions, rescheduling, and the final call stay with the officer. What got shipped is the opposite: a read-only endpoint that flags overlapping schedules, then a warning box on the form that never blocks anything.
Parameter Validation on the Server
The availability endpoint takes three parameters. The facility identifier must parse as a positive integer. Both date parameters must pass time.Parse with the "2006-01-02" layout: Go layouts are written against the reference timestamp 01/02 03:04:05PM '06 -0700, so 2006-01-02 is the canonical way to write a YYYY-MM-DD date [1]. When the start date exceeds the end date, the server answers 400 INVALID_INPUT before touching the database.
id, err := strconv.ParseInt(rawID, 10, 64)
if err != nil || id <= 0 {
return http.StatusBadRequest // INVALID_INPUT
}
_, err = time.Parse("2006-01-02", rawStart)
if err != nil {
return http.StatusBadRequest // format wajib YYYY-MM-DD
}
The endpoint carries no rate limit. The reason is simple and written into its route: the surface matches the public form but the endpoint is strictly read-only, so repeated availability checks from the client are not treated as abuse.
The Inclusive Overlap Query
Two date ranges overlap under two comparisons, not four. An existing rental counts as a clash when its start date does not fall after the requested end and its end date does not precede the requested start:
WHERE rental_start_date <= ?
AND rental_end_date >= ?
AND status <> 'ditolak'
Both comparisons are inclusive at both ends, so a 12-14 booking is still detected by a 10-15 request. Rejected applications drop out through the status filter, and rows without rental dates fall away on their own because NULL comparisons fail. Results sort ascending by start date so the list reads chronologically.
One driver detail is easy to miss. When the database connection uses the parseTime option, the MySQL driver scans DATE columns as time objects and the JSON serialization changes shape with them. The SELECT clause uses DATE_FORMAT with a YYYY-MM-DD pattern so the column scans back as a string; the function re-renders the date value per the format string and returns NULL when its argument is NULL [2]. The response contract is deliberately thin: only a start date and an end date, no applicant name or identity. An unknown facility does not get a 404; an empty list is the canonical answer for "no records", so the client never has to tell those two states apart.
A Warning That Never Blocks
On the form side, the check only runs after the user stops changing the selection for 500 milliseconds. Three layers keep the response correct. Debouncing holds requests while the date range is still moving. A generation counter increments whenever the check conditions change, and a response arriving under an old generation number is discarded outright. AbortController closes the gap: its signal goes straight into fetch so an in-flight request can be cancelled from the useEffect cleanup function [3].
Failure is treated as the absence of a warning, not as an obstacle. A 400, a 401, or a network fault leaves the warning box hidden and the form fully submittable. The box itself is amber-stamped with a role="status" attribute so screen readers announce its content without stealing focus. The submit button is never locked by this state.
The design decision fits in one sentence: the check informs, the human decides. The result is directly verifiable. Two overlapping applications now list their clashing periods before the submit button is touched; an application without conflicts never sees the box; and with the API switched off, the form still completes its job.
Sources
[1] golang/go, src/time/format.go, accessed 13 October 2026.
[2] MySQL 8.4 Reference Manual, Date and Time Functions (Wayback snapshot), accessed 13 October 2026.
[3] MDN Web Docs, AbortController, accessed 13 October 2026.