feat(scheduling,admin,today): week-range queries, file size limits, mobile nav UX
- Expand exceptional application query to Monday of start week - Add 20MB file size limit with visual feedback in ImageUpload - Reorder admin nav links, add burger badge, slide transition + backdrop - Fetch week-range working/available hours, add closing time indicator - Skip lunch protection for days <= 5h via shouldApplyLunchProtection - Extract formatDateISO to shared utils
This commit is contained in:
@@ -13,9 +13,11 @@
|
||||
import {
|
||||
calculateMiddleWindow,
|
||||
extractBookedSlots,
|
||||
findAllLunchGaps
|
||||
findAllLunchGaps,
|
||||
shouldApplyLunchProtection,
|
||||
timeToMinutes
|
||||
} from '$lib/lunchProtection';
|
||||
import { formatDuration } from '$lib/utils/format';
|
||||
import { formatDuration, formatDateISO } from '$lib/utils/format';
|
||||
|
||||
interface Props {
|
||||
openBookingModal: (bookingId: string) => void;
|
||||
@@ -98,9 +100,20 @@
|
||||
let startSelectValue = $derived(`${startHour}:${startMinute}:${startPeriod}`);
|
||||
let endSelectValue = $derived(`${endHour}:${endMinute}:${endPeriod}`);
|
||||
|
||||
const today = $derived.by(() => {
|
||||
const today = $derived(formatDateISO(new SvelteDate()));
|
||||
|
||||
const weekStartStr = $derived.by(() => {
|
||||
const d = new SvelteDate();
|
||||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
const day = d.getDay();
|
||||
const diff = day === 0 ? 6 : day - 1;
|
||||
d.setDate(d.getDate() - diff);
|
||||
return formatDateISO(d);
|
||||
});
|
||||
|
||||
const weekEndStr = $derived.by(() => {
|
||||
const d = new SvelteDate(weekStartStr);
|
||||
d.setDate(d.getDate() + 6);
|
||||
return formatDateISO(d);
|
||||
});
|
||||
|
||||
function to24h(hour: string, minute: string, period: 'AM' | 'PM'): string {
|
||||
@@ -110,11 +123,6 @@
|
||||
return `${String(h).padStart(2, '0')}:${minute}`;
|
||||
}
|
||||
|
||||
function timeToMinutes(time: string): number {
|
||||
const [h, m] = time.split(':').map(Number);
|
||||
return h * 60 + m;
|
||||
}
|
||||
|
||||
function minutesTo12h(totalMin: number): { hour: string; minute: string; period: 'AM' | 'PM' } {
|
||||
let h = Math.floor(totalMin / 60);
|
||||
const m = totalMin % 60;
|
||||
@@ -179,6 +187,9 @@
|
||||
if (!workingHours || !availableHours || !workingHours.isOpen) return null;
|
||||
const wh = workingHours;
|
||||
const ah = availableHours;
|
||||
|
||||
if (!shouldApplyLunchProtection(wh.startTime, wh.endTime)) return null;
|
||||
|
||||
const { windowStart, windowEnd } = calculateMiddleWindow(wh.startTime, wh.endTime);
|
||||
const existingBookings = extractBookedSlots(wh.startTime, wh.endTime, ah.slots);
|
||||
const gaps = findAllLunchGaps(windowStart, windowEnd, existingBookings);
|
||||
@@ -246,7 +257,14 @@
|
||||
data: { duration: number; startLabel: string; endLabel: string };
|
||||
};
|
||||
|
||||
type TimelineItem = AppointmentTimelineItem | BlockerTimelineItem | LunchTimelineItem;
|
||||
type ClosingTimeTimelineItem = {
|
||||
id: string;
|
||||
startMinutes: number;
|
||||
type: 'closing';
|
||||
data: { label: string };
|
||||
};
|
||||
|
||||
type TimelineItem = AppointmentTimelineItem | BlockerTimelineItem | LunchTimelineItem | ClosingTimeTimelineItem;
|
||||
|
||||
let timeline = $derived.by(() => {
|
||||
const items: TimelineItem[] = [];
|
||||
@@ -289,6 +307,17 @@
|
||||
});
|
||||
}
|
||||
|
||||
if (workingHours?.isOpen && workingHours.endTime) {
|
||||
const closeMin = timeToMinutes(workingHours.endTime);
|
||||
const t = minutesTo12h(closeMin);
|
||||
items.push({
|
||||
id: 'closing',
|
||||
startMinutes: closeMin,
|
||||
type: 'closing',
|
||||
data: { label: `${t.hour}:${t.minute} ${t.period}` }
|
||||
});
|
||||
}
|
||||
|
||||
return items.sort((a, b) => a.startMinutes - b.startMinutes);
|
||||
});
|
||||
|
||||
@@ -346,7 +375,7 @@
|
||||
}
|
||||
),
|
||||
fetch(
|
||||
`/api/scheduling/working-hours?start=${encodeURIComponent(dateParam)}&end=${encodeURIComponent(dateParam)}`,
|
||||
`/api/scheduling/working-hours?start=${encodeURIComponent(weekStartStr)}&end=${encodeURIComponent(weekEndStr)}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -355,7 +384,7 @@
|
||||
}
|
||||
),
|
||||
fetch(
|
||||
`/api/scheduling/available-hours?start=${encodeURIComponent(dateParam)}&end=${encodeURIComponent(dateParam)}`,
|
||||
`/api/scheduling/available-hours?start=${encodeURIComponent(weekStartStr)}&end=${encodeURIComponent(weekEndStr)}`,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -834,6 +863,16 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{:else if item.type === 'closing'}
|
||||
<div class="flex items-center gap-3 border-t border-gray-100 px-1 pt-3">
|
||||
<div
|
||||
class="text-xs font-medium text-gray-400"
|
||||
>
|
||||
{item.data.label}
|
||||
</div>
|
||||
<div class="h-px flex-1 bg-gray-100"></div>
|
||||
<span class="text-[11px] text-gray-400">Close</span>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user