Task #699: الحجز بالنقر على وقت فارغ في تقويم اليوم
- BookingDayGrid (artifacts/tx-os/src/pages/protocol.tsx) now accepts onSlotClick?: (start: Date) => void. Clicking the empty grid background computes the time from click Y (getBoundingClientRect + clientY), snaps down to the nearest 30 minutes, clamps within grid hours (7–22, keeping a full hour inside), and calls onSlotClick with the resulting Date on the selected day. Cursor becomes pointer when handler is set. - Booking blocks now stopPropagation on click so existing behavior (opening the details dialog) is unchanged and never double-triggers. - ProtocolPage passes onSlotClick only when capabilities.canRequest, opening the existing BookingDialog (same form as the "حجز +" button) with the new start prefilled. - BookingDialog: new optional initialStart prop; startsAt = initialStart, endsAt = initialStart + 1h (falls back to prior 9–10 defaults). - Verified: tsc --noEmit clean in artifacts/tx-os; architect review Pass.
This commit is contained in:
@@ -355,6 +355,7 @@ function BookingDayGrid({
|
||||
emptyText,
|
||||
scrollSignal = 0,
|
||||
onSelect,
|
||||
onSlotClick,
|
||||
}: {
|
||||
bookings: Booking[];
|
||||
selectedDay: Date;
|
||||
@@ -364,6 +365,7 @@ function BookingDayGrid({
|
||||
emptyText: string;
|
||||
scrollSignal?: number;
|
||||
onSelect?: (b: Booking) => void;
|
||||
onSlotClick?: (start: Date) => void;
|
||||
}) {
|
||||
const hours: number[] = [];
|
||||
for (let h = GRID_HOUR_START; h <= GRID_HOUR_END; h++) hours.push(h);
|
||||
@@ -438,8 +440,30 @@ function BookingDayGrid({
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
className="relative flex-1 border-s border-slate-200"
|
||||
className={cn(
|
||||
"relative flex-1 border-s border-slate-200",
|
||||
onSlotClick && "cursor-pointer",
|
||||
)}
|
||||
style={{ height: gridHeight }}
|
||||
onClick={(e) => {
|
||||
if (!onSlotClick) return;
|
||||
// Only clicks on the empty grid itself — booking blocks stop
|
||||
// propagation in their own handler.
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
const offsetY = e.clientY - rect.top;
|
||||
const rawMin =
|
||||
GRID_HOUR_START * 60 + (offsetY / GRID_PX_PER_HOUR) * 60;
|
||||
// Snap down to the nearest 30 minutes, keep a full hour inside
|
||||
// the visible grid range.
|
||||
const snapped = Math.floor(rawMin / 30) * 30;
|
||||
const startMin = Math.min(
|
||||
Math.max(snapped, GRID_HOUR_START * 60),
|
||||
GRID_HOUR_END * 60 - 60,
|
||||
);
|
||||
const start = new Date(selectedDay);
|
||||
start.setHours(Math.floor(startMin / 60), startMin % 60, 0, 0);
|
||||
onSlotClick(start);
|
||||
}}
|
||||
>
|
||||
{hours.map((h) => (
|
||||
<div
|
||||
@@ -463,7 +487,10 @@ function BookingDayGrid({
|
||||
<button
|
||||
key={ev.b.id}
|
||||
type="button"
|
||||
onClick={() => onSelect?.(ev.b)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onSelect?.(ev.b);
|
||||
}}
|
||||
className={cn(
|
||||
"absolute overflow-hidden rounded-md border-s-4 px-1.5 py-0.5 text-start text-[11px] leading-tight shadow-sm transition-shadow",
|
||||
"cursor-pointer hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sky-500",
|
||||
@@ -628,6 +655,7 @@ export default function ProtocolPage() {
|
||||
open: boolean;
|
||||
edit?: Booking;
|
||||
day?: Date;
|
||||
start?: Date;
|
||||
}>({ open: false });
|
||||
const [rejectTarget, setRejectTarget] = useState<{
|
||||
kind: "booking" | "issue" | "external";
|
||||
@@ -1504,6 +1532,11 @@ export default function ProtocolPage() {
|
||||
emptyText={t("protocol.bookings.emptyDay")}
|
||||
scrollSignal={todayScrollSignal}
|
||||
onSelect={(b) => setDetailsBooking(b)}
|
||||
onSlotClick={
|
||||
c?.canRequest
|
||||
? (start) => setBookingDialog({ open: true, start })
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1874,6 +1907,7 @@ export default function ProtocolPage() {
|
||||
rooms={rooms.data ?? []}
|
||||
edit={bookingDialog.edit}
|
||||
initialDay={bookingDialog.day}
|
||||
initialStart={bookingDialog.start}
|
||||
nameOf={nameOf}
|
||||
onClose={() => setBookingDialog({ open: false })}
|
||||
onSaved={() => {
|
||||
@@ -2529,6 +2563,7 @@ function BookingDialog({
|
||||
rooms,
|
||||
edit,
|
||||
initialDay,
|
||||
initialStart,
|
||||
nameOf,
|
||||
onClose,
|
||||
onSaved,
|
||||
@@ -2537,6 +2572,7 @@ function BookingDialog({
|
||||
rooms: Room[];
|
||||
edit?: Booking;
|
||||
initialDay?: Date;
|
||||
initialStart?: Date;
|
||||
nameOf: (ar: string, en: string) => string;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
@@ -2554,12 +2590,19 @@ function BookingDialog({
|
||||
edit ? String(edit.roomId) : rooms[0] ? String(rooms[0].id) : "",
|
||||
);
|
||||
const [requesterName, setRequesterName] = useState(edit?.requesterName ?? "");
|
||||
const [startsAt, setStartsAt] = useState(
|
||||
edit ? toLocalInput(edit.startsAt) : dayAt(9),
|
||||
);
|
||||
const [endsAt, setEndsAt] = useState(
|
||||
edit ? toLocalInput(edit.endsAt) : dayAt(10),
|
||||
);
|
||||
const [startsAt, setStartsAt] = useState(() => {
|
||||
if (edit) return toLocalInput(edit.startsAt);
|
||||
if (initialStart) return toLocalInput(initialStart.toISOString());
|
||||
return dayAt(9);
|
||||
});
|
||||
const [endsAt, setEndsAt] = useState(() => {
|
||||
if (edit) return toLocalInput(edit.endsAt);
|
||||
if (initialStart)
|
||||
return toLocalInput(
|
||||
new Date(initialStart.getTime() + 60 * 60 * 1000).toISOString(),
|
||||
);
|
||||
return dayAt(10);
|
||||
});
|
||||
const [notes, setNotes] = useState(edit?.notes ?? "");
|
||||
|
||||
const mut = useMutation({
|
||||
|
||||
Reference in New Issue
Block a user