2025-12-20 23:54:34 +01:00
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
|
|
import { useEffect, useState, useCallback } from "react";
|
|
|
|
|
|
import { Permission } from "../../auth-context";
|
|
|
|
|
|
import { api } from "../../api";
|
|
|
|
|
|
import { Header } from "../../components/Header";
|
|
|
|
|
|
import { useRequireAuth } from "../../hooks/useRequireAuth";
|
|
|
|
|
|
import { components } from "../../generated/api";
|
|
|
|
|
|
import constants from "../../../../shared/constants.json";
|
2025-12-21 21:59:26 +01:00
|
|
|
|
import {
|
|
|
|
|
|
formatDate,
|
|
|
|
|
|
formatDisplayDate,
|
|
|
|
|
|
getDateRange,
|
|
|
|
|
|
formatTimeString,
|
|
|
|
|
|
isWeekend,
|
|
|
|
|
|
} from "../../utils/date";
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
import {
|
|
|
|
|
|
layoutStyles,
|
|
|
|
|
|
typographyStyles,
|
|
|
|
|
|
bannerStyles,
|
|
|
|
|
|
buttonStyles,
|
|
|
|
|
|
modalStyles,
|
|
|
|
|
|
} from "../../styles/shared";
|
2025-12-20 23:54:34 +01:00
|
|
|
|
|
2025-12-22 21:42:42 +01:00
|
|
|
|
const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays } = constants.exchange;
|
2025-12-20 23:54:34 +01:00
|
|
|
|
|
2025-12-21 21:58:41 +01:00
|
|
|
|
type _AvailabilityDay = components["schemas"]["AvailabilityDay"];
|
2025-12-20 23:54:34 +01:00
|
|
|
|
type AvailabilityResponse = components["schemas"]["AvailabilityResponse"];
|
|
|
|
|
|
type TimeSlot = components["schemas"]["TimeSlot"];
|
|
|
|
|
|
|
|
|
|
|
|
// Generate time options for dropdowns (15-min intervals)
|
2025-12-21 17:53:17 +01:00
|
|
|
|
// Moved outside component since slotDurationMinutes is a constant
|
|
|
|
|
|
function generateTimeOptions(slotDurationMinutes: number): string[] {
|
2025-12-20 23:54:34 +01:00
|
|
|
|
const options: string[] = [];
|
|
|
|
|
|
for (let h = 0; h < 24; h++) {
|
|
|
|
|
|
for (let m = 0; m < 60; m += slotDurationMinutes) {
|
|
|
|
|
|
const hh = h.toString().padStart(2, "0");
|
|
|
|
|
|
const mm = m.toString().padStart(2, "0");
|
|
|
|
|
|
options.push(`${hh}:${mm}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return options;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-21 17:53:17 +01:00
|
|
|
|
const TIME_OPTIONS = generateTimeOptions(slotDurationMinutes);
|
2025-12-20 23:54:34 +01:00
|
|
|
|
|
|
|
|
|
|
interface EditSlot {
|
|
|
|
|
|
start_time: string;
|
|
|
|
|
|
end_time: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function AdminAvailabilityPage() {
|
|
|
|
|
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
|
|
|
|
|
requiredPermission: Permission.MANAGE_AVAILABILITY,
|
|
|
|
|
|
fallbackRedirect: "/",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const [availability, setAvailability] = useState<Map<string, TimeSlot[]>>(new Map());
|
|
|
|
|
|
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
|
|
|
|
|
|
const [editSlots, setEditSlots] = useState<EditSlot[]>([]);
|
|
|
|
|
|
const [isSaving, setIsSaving] = useState(false);
|
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
const [copySource, setCopySource] = useState<string | null>(null);
|
|
|
|
|
|
const [copyTargets, setCopyTargets] = useState<Set<string>>(new Set());
|
|
|
|
|
|
const [isCopying, setIsCopying] = useState(false);
|
|
|
|
|
|
|
2025-12-21 17:54:49 +01:00
|
|
|
|
const dates = getDateRange(minAdvanceDays, maxAdvanceDays);
|
2025-12-20 23:54:34 +01:00
|
|
|
|
|
|
|
|
|
|
const fetchAvailability = useCallback(async () => {
|
2025-12-21 17:54:49 +01:00
|
|
|
|
const dateRange = getDateRange(minAdvanceDays, maxAdvanceDays);
|
2025-12-21 17:32:55 +01:00
|
|
|
|
if (!dateRange.length) return;
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
try {
|
2025-12-21 17:32:55 +01:00
|
|
|
|
const fromDate = formatDate(dateRange[0]);
|
|
|
|
|
|
const toDate = formatDate(dateRange[dateRange.length - 1]);
|
2025-12-20 23:54:34 +01:00
|
|
|
|
const data = await api.get<AvailabilityResponse>(
|
|
|
|
|
|
`/api/admin/availability?from=${fromDate}&to=${toDate}`
|
|
|
|
|
|
);
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
const map = new Map<string, TimeSlot[]>();
|
|
|
|
|
|
for (const day of data.days) {
|
|
|
|
|
|
map.set(day.date, day.slots);
|
|
|
|
|
|
}
|
|
|
|
|
|
setAvailability(map);
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
console.error("Failed to fetch availability:", err);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (user && isAuthorized) {
|
|
|
|
|
|
fetchAvailability();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, [user, isAuthorized, fetchAvailability]);
|
|
|
|
|
|
|
|
|
|
|
|
const openEditModal = (date: Date) => {
|
|
|
|
|
|
const dateStr = formatDate(date);
|
|
|
|
|
|
const existingSlots = availability.get(dateStr) || [];
|
|
|
|
|
|
setEditSlots(
|
|
|
|
|
|
existingSlots.length > 0
|
|
|
|
|
|
? existingSlots.map((s) => ({
|
2025-12-21 17:56:20 +01:00
|
|
|
|
start_time: formatTimeString(s.start_time),
|
|
|
|
|
|
end_time: formatTimeString(s.end_time),
|
2025-12-20 23:54:34 +01:00
|
|
|
|
}))
|
|
|
|
|
|
: [{ start_time: "09:00", end_time: "17:00" }]
|
|
|
|
|
|
);
|
|
|
|
|
|
setSelectedDate(date);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const closeModal = () => {
|
|
|
|
|
|
setSelectedDate(null);
|
|
|
|
|
|
setEditSlots([]);
|
|
|
|
|
|
setError(null);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const addSlot = () => {
|
|
|
|
|
|
setEditSlots([...editSlots, { start_time: "09:00", end_time: "17:00" }]);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const removeSlot = (index: number) => {
|
|
|
|
|
|
setEditSlots(editSlots.filter((_, i) => i !== index));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const updateSlot = (index: number, field: "start_time" | "end_time", value: string) => {
|
|
|
|
|
|
const updated = [...editSlots];
|
|
|
|
|
|
updated[index][field] = value;
|
|
|
|
|
|
setEditSlots(updated);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const saveAvailability = async () => {
|
|
|
|
|
|
if (!selectedDate) return;
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
setIsSaving(true);
|
|
|
|
|
|
setError(null);
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
try {
|
|
|
|
|
|
const slots = editSlots.map((s) => ({
|
|
|
|
|
|
start_time: s.start_time + ":00",
|
|
|
|
|
|
end_time: s.end_time + ":00",
|
|
|
|
|
|
}));
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
await api.put("/api/admin/availability", {
|
|
|
|
|
|
date: formatDate(selectedDate),
|
|
|
|
|
|
slots,
|
|
|
|
|
|
});
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
await fetchAvailability();
|
|
|
|
|
|
closeModal();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(err instanceof Error ? err.message : "Failed to save");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const clearAvailability = async () => {
|
|
|
|
|
|
if (!selectedDate) return;
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
setIsSaving(true);
|
|
|
|
|
|
setError(null);
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
try {
|
|
|
|
|
|
await api.put("/api/admin/availability", {
|
|
|
|
|
|
date: formatDate(selectedDate),
|
|
|
|
|
|
slots: [],
|
|
|
|
|
|
});
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
await fetchAvailability();
|
|
|
|
|
|
closeModal();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(err instanceof Error ? err.message : "Failed to clear");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsSaving(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const startCopyMode = (dateStr: string) => {
|
|
|
|
|
|
setCopySource(dateStr);
|
|
|
|
|
|
setCopyTargets(new Set());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const cancelCopyMode = () => {
|
|
|
|
|
|
setCopySource(null);
|
|
|
|
|
|
setCopyTargets(new Set());
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const toggleCopyTarget = (dateStr: string) => {
|
|
|
|
|
|
if (dateStr === copySource) return;
|
|
|
|
|
|
const newTargets = new Set(copyTargets);
|
|
|
|
|
|
if (newTargets.has(dateStr)) {
|
|
|
|
|
|
newTargets.delete(dateStr);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
newTargets.add(dateStr);
|
|
|
|
|
|
}
|
|
|
|
|
|
setCopyTargets(newTargets);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const executeCopy = async () => {
|
|
|
|
|
|
if (!copySource || copyTargets.size === 0) return;
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
setIsCopying(true);
|
|
|
|
|
|
setError(null);
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
try {
|
|
|
|
|
|
await api.post("/api/admin/availability/copy", {
|
|
|
|
|
|
source_date: copySource,
|
|
|
|
|
|
target_dates: Array.from(copyTargets),
|
|
|
|
|
|
});
|
2025-12-21 21:59:26 +01:00
|
|
|
|
|
2025-12-20 23:54:34 +01:00
|
|
|
|
await fetchAvailability();
|
|
|
|
|
|
cancelCopyMode();
|
|
|
|
|
|
} catch (err) {
|
|
|
|
|
|
setError(err instanceof Error ? err.message : "Failed to copy");
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
setIsCopying(false);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const formatSlotTime = (slot: TimeSlot): string => {
|
2025-12-21 17:56:20 +01:00
|
|
|
|
return `${formatTimeString(slot.start_time)} - ${formatTimeString(slot.end_time)}`;
|
2025-12-20 23:54:34 +01:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
|
return (
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<main style={layoutStyles.main}>
|
|
|
|
|
|
<div style={layoutStyles.loader}>Loading...</div>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
</main>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!user || !isAuthorized) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<main style={layoutStyles.main}>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
<Header currentPage="admin-availability" />
|
|
|
|
|
|
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<div style={layoutStyles.contentScrollable}>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
<div style={styles.pageContainer}>
|
|
|
|
|
|
<div style={styles.headerRow}>
|
|
|
|
|
|
<div>
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<h1 style={typographyStyles.pageTitle}>Availability</h1>
|
|
|
|
|
|
<p style={typographyStyles.pageSubtitle}>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
Configure your available time slots for the next {maxAdvanceDays} days
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{copySource && (
|
|
|
|
|
|
<div style={styles.copyActions}>
|
2025-12-21 21:59:26 +01:00
|
|
|
|
<span style={styles.copyHint}>Select days to copy to, then click Copy</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={executeCopy}
|
|
|
|
|
|
disabled={copyTargets.size === 0 || isCopying}
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
style={buttonStyles.accentButton}
|
2025-12-21 21:59:26 +01:00
|
|
|
|
>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
{isCopying ? "Copying..." : `Copy to ${copyTargets.size} day(s)`}
|
|
|
|
|
|
</button>
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<button onClick={cancelCopyMode} style={buttonStyles.secondaryButton}>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
Cancel
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
{error && !selectedDate && <div style={bannerStyles.errorBanner}>{error}</div>}
|
2025-12-20 23:54:34 +01:00
|
|
|
|
|
|
|
|
|
|
<div style={styles.calendar}>
|
|
|
|
|
|
{dates.map((date) => {
|
|
|
|
|
|
const dateStr = formatDate(date);
|
|
|
|
|
|
const slots = availability.get(dateStr) || [];
|
|
|
|
|
|
const hasSlots = slots.length > 0;
|
|
|
|
|
|
const isSource = copySource === dateStr;
|
|
|
|
|
|
const isTarget = copyTargets.has(dateStr);
|
2025-12-21 17:57:06 +01:00
|
|
|
|
const isWeekendDate = isWeekend(date);
|
2025-12-20 23:54:34 +01:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={dateStr}
|
2025-12-21 01:13:10 +01:00
|
|
|
|
data-testid={`day-card-${dateStr}`}
|
2025-12-20 23:54:34 +01:00
|
|
|
|
style={{
|
|
|
|
|
|
...styles.dayCard,
|
|
|
|
|
|
...(hasSlots ? styles.dayCardActive : {}),
|
|
|
|
|
|
...(isSource ? styles.dayCardSource : {}),
|
|
|
|
|
|
...(isTarget ? styles.dayCardTarget : {}),
|
2025-12-21 17:57:06 +01:00
|
|
|
|
...(isWeekendDate ? styles.dayCardWeekend : {}),
|
2025-12-20 23:54:34 +01:00
|
|
|
|
}}
|
|
|
|
|
|
onClick={() => {
|
|
|
|
|
|
if (copySource) {
|
|
|
|
|
|
toggleCopyTarget(dateStr);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
openEditModal(date);
|
|
|
|
|
|
}
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={styles.dayHeader}>
|
|
|
|
|
|
<span style={styles.dayName}>{formatDisplayDate(date)}</span>
|
|
|
|
|
|
{hasSlots && !copySource && (
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
startCopyMode(dateStr);
|
|
|
|
|
|
}}
|
|
|
|
|
|
style={styles.copyFromButton}
|
|
|
|
|
|
title="Copy to other days"
|
|
|
|
|
|
>
|
2025-12-21 17:55:06 +01:00
|
|
|
|
📋
|
2025-12-20 23:54:34 +01:00
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style={styles.slotList}>
|
|
|
|
|
|
{slots.length === 0 ? (
|
|
|
|
|
|
<span style={styles.noSlots}>No availability</span>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
slots.map((slot, i) => (
|
|
|
|
|
|
<span key={i} style={styles.slotBadge}>
|
|
|
|
|
|
{formatSlotTime(slot)}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Edit Modal */}
|
|
|
|
|
|
{selectedDate && (
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<div style={modalStyles.modalOverlay} onClick={closeModal}>
|
|
|
|
|
|
<div style={modalStyles.modal} onClick={(e) => e.stopPropagation()}>
|
|
|
|
|
|
<h2 style={modalStyles.modalTitle}>
|
|
|
|
|
|
Edit Availability - {formatDisplayDate(selectedDate)}
|
|
|
|
|
|
</h2>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
{error && <div style={modalStyles.modalError}>{error}</div>}
|
2025-12-20 23:54:34 +01:00
|
|
|
|
|
|
|
|
|
|
<div style={styles.slotsEditor}>
|
|
|
|
|
|
{editSlots.map((slot, index) => (
|
|
|
|
|
|
<div key={index} style={styles.slotRow}>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={slot.start_time}
|
|
|
|
|
|
onChange={(e) => updateSlot(index, "start_time", e.target.value)}
|
|
|
|
|
|
style={styles.timeSelect}
|
|
|
|
|
|
>
|
|
|
|
|
|
{TIME_OPTIONS.map((t) => (
|
2025-12-21 21:59:26 +01:00
|
|
|
|
<option key={t} value={t}>
|
|
|
|
|
|
{t}
|
|
|
|
|
|
</option>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<span style={styles.slotDash}>→</span>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={slot.end_time}
|
|
|
|
|
|
onChange={(e) => updateSlot(index, "end_time", e.target.value)}
|
|
|
|
|
|
style={styles.timeSelect}
|
|
|
|
|
|
>
|
|
|
|
|
|
{TIME_OPTIONS.map((t) => (
|
2025-12-21 21:59:26 +01:00
|
|
|
|
<option key={t} value={t}>
|
|
|
|
|
|
{t}
|
|
|
|
|
|
</option>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => removeSlot(index)}
|
|
|
|
|
|
style={styles.removeSlotButton}
|
|
|
|
|
|
title="Remove slot"
|
|
|
|
|
|
>
|
|
|
|
|
|
×
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
<button onClick={addSlot} style={styles.addSlotButton}>
|
|
|
|
|
|
+ Add Time Range
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<div style={modalStyles.modalActions}>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
<button onClick={clearAvailability} disabled={isSaving} style={styles.clearButton}>
|
|
|
|
|
|
Clear All
|
|
|
|
|
|
</button>
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<div style={modalStyles.modalActionsRight}>
|
|
|
|
|
|
<button onClick={closeModal} style={buttonStyles.secondaryButton}>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
Cancel
|
|
|
|
|
|
</button>
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
<button
|
|
|
|
|
|
onClick={saveAvailability}
|
|
|
|
|
|
disabled={isSaving}
|
|
|
|
|
|
style={buttonStyles.accentButton}
|
|
|
|
|
|
>
|
2025-12-20 23:54:34 +01:00
|
|
|
|
{isSaving ? "Saving..." : "Save"}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</main>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
refactor(frontend): consolidate shared styles into centralized style system
- Create comprehensive shared.ts with design tokens and categorized styles:
- layoutStyles: main, loader, content variants
- cardStyles: card, tableCard, cardHeader
- tableStyles: complete table styling
- paginationStyles: pagination controls
- formStyles: inputs, labels, errors
- buttonStyles: primary, secondary, accent, danger variants
- badgeStyles: status badges with color variants
- bannerStyles: error/success banners
- modalStyles: modal overlay and content
- toastStyles: toast notifications
- utilityStyles: divider, emptyState, etc.
- Refactor all page components to use shared styles:
- page.tsx (counter)
- audit/page.tsx
- booking/page.tsx
- appointments/page.tsx
- profile/page.tsx
- invites/page.tsx
- admin/invites/page.tsx
- admin/availability/page.tsx
- Reduce code duplication significantly (each page now has only
truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
2025-12-21 23:45:47 +01:00
|
|
|
|
// Page-specific styles
|
|
|
|
|
|
const styles: Record<string, React.CSSProperties> = {
|
2025-12-20 23:54:34 +01:00
|
|
|
|
pageContainer: {
|
|
|
|
|
|
maxWidth: "1200px",
|
|
|
|
|
|
margin: "0 auto",
|
|
|
|
|
|
},
|
|
|
|
|
|
headerRow: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
|
alignItems: "flex-start",
|
|
|
|
|
|
marginBottom: "2rem",
|
|
|
|
|
|
flexWrap: "wrap",
|
|
|
|
|
|
gap: "1rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
copyActions: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
gap: "1rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
copyHint: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "0.85rem",
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.6)",
|
|
|
|
|
|
},
|
|
|
|
|
|
calendar: {
|
|
|
|
|
|
display: "grid",
|
|
|
|
|
|
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
|
|
|
|
|
|
gap: "1rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
dayCard: {
|
|
|
|
|
|
background: "rgba(255, 255, 255, 0.03)",
|
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
|
|
|
|
borderRadius: "12px",
|
|
|
|
|
|
padding: "1rem",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
transition: "all 0.2s",
|
|
|
|
|
|
},
|
|
|
|
|
|
dayCardActive: {
|
|
|
|
|
|
background: "rgba(99, 102, 241, 0.1)",
|
2025-12-21 18:23:31 +01:00
|
|
|
|
border: "1px solid rgba(99, 102, 241, 0.3)",
|
2025-12-20 23:54:34 +01:00
|
|
|
|
},
|
|
|
|
|
|
dayCardSource: {
|
|
|
|
|
|
background: "rgba(34, 197, 94, 0.15)",
|
2025-12-21 18:23:31 +01:00
|
|
|
|
border: "1px solid rgba(34, 197, 94, 0.5)",
|
2025-12-20 23:54:34 +01:00
|
|
|
|
},
|
|
|
|
|
|
dayCardTarget: {
|
|
|
|
|
|
background: "rgba(99, 102, 241, 0.2)",
|
2025-12-21 18:23:31 +01:00
|
|
|
|
border: "1px solid rgba(99, 102, 241, 0.6)",
|
2025-12-20 23:54:34 +01:00
|
|
|
|
},
|
|
|
|
|
|
dayCardWeekend: {
|
|
|
|
|
|
opacity: 0.7,
|
|
|
|
|
|
},
|
|
|
|
|
|
dayHeader: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
justifyContent: "space-between",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
marginBottom: "0.5rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
dayName: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "0.85rem",
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.9)",
|
|
|
|
|
|
},
|
|
|
|
|
|
copyFromButton: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "1rem",
|
|
|
|
|
|
padding: "0.25rem 0.5rem",
|
|
|
|
|
|
background: "transparent",
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.5)",
|
|
|
|
|
|
border: "none",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
},
|
|
|
|
|
|
slotList: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
|
gap: "0.25rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
noSlots: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "0.75rem",
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.3)",
|
|
|
|
|
|
},
|
|
|
|
|
|
slotBadge: {
|
|
|
|
|
|
fontFamily: "'DM Mono', monospace",
|
|
|
|
|
|
fontSize: "0.7rem",
|
|
|
|
|
|
padding: "0.2rem 0.4rem",
|
|
|
|
|
|
background: "rgba(99, 102, 241, 0.2)",
|
|
|
|
|
|
borderRadius: "4px",
|
|
|
|
|
|
color: "rgba(129, 140, 248, 0.9)",
|
|
|
|
|
|
},
|
|
|
|
|
|
slotsEditor: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
flexDirection: "column",
|
|
|
|
|
|
gap: "0.75rem",
|
|
|
|
|
|
marginBottom: "1.5rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
slotRow: {
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
gap: "0.5rem",
|
|
|
|
|
|
},
|
|
|
|
|
|
timeSelect: {
|
|
|
|
|
|
fontFamily: "'DM Mono', monospace",
|
|
|
|
|
|
fontSize: "0.9rem",
|
|
|
|
|
|
padding: "0.5rem",
|
|
|
|
|
|
background: "rgba(255, 255, 255, 0.05)",
|
|
|
|
|
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
|
|
|
|
borderRadius: "6px",
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
},
|
|
|
|
|
|
slotDash: {
|
|
|
|
|
|
color: "rgba(255, 255, 255, 0.4)",
|
|
|
|
|
|
},
|
|
|
|
|
|
removeSlotButton: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "1.2rem",
|
|
|
|
|
|
width: "32px",
|
|
|
|
|
|
height: "32px",
|
|
|
|
|
|
background: "rgba(239, 68, 68, 0.15)",
|
|
|
|
|
|
color: "rgba(239, 68, 68, 0.9)",
|
|
|
|
|
|
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
|
|
|
|
borderRadius: "6px",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
display: "flex",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
|
},
|
|
|
|
|
|
addSlotButton: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "0.85rem",
|
|
|
|
|
|
padding: "0.5rem",
|
|
|
|
|
|
background: "transparent",
|
|
|
|
|
|
color: "rgba(99, 102, 241, 0.9)",
|
|
|
|
|
|
border: "1px dashed rgba(99, 102, 241, 0.4)",
|
|
|
|
|
|
borderRadius: "6px",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
},
|
|
|
|
|
|
clearButton: {
|
|
|
|
|
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
|
|
|
|
fontSize: "0.85rem",
|
|
|
|
|
|
padding: "0.6rem 1rem",
|
|
|
|
|
|
background: "rgba(239, 68, 68, 0.15)",
|
|
|
|
|
|
color: "rgba(239, 68, 68, 0.9)",
|
|
|
|
|
|
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
|
|
|
|
borderRadius: "8px",
|
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|