new-modal #4

Merged
jare2473 merged 18 commits from new-modal into main 2025-09-10 10:01:30 +02:00
9 changed files with 323 additions and 92 deletions
Showing only changes of commit 38cbcfed1b - Show all commits

View File

@ -5,6 +5,7 @@ import { CalendarDate } from '@internationalized/date';
import Layout from './Layout';
import { RoomBooking } from './pages/RoomBooking';
import { NewBooking } from './pages/NewBooking';
import { BookingDetails } from './pages/BookingDetails';
import { BookingSettings } from './pages/BookingSettings';
import { CourseSchedule } from './pages/CourseSchedule';
import { CourseScheduleView } from './pages/CourseScheduleView';
@ -119,6 +120,7 @@ const AppRoutes = () => {
<Route path="/" element={<Layout />}>
<Route index element={<RoomBooking bookings={bookings} showSuccessBanner={showSuccessBanner} lastCreatedBooking={lastCreatedBooking} onDismissBanner={() => setShowSuccessBanner(false)} onBookingUpdate={updateBooking} onBookingDelete={deleteBooking} showDeleteBanner={showDeleteBanner} lastDeletedBooking={lastDeletedBooking} onDismissDeleteBanner={() => setShowDeleteBanner(false)} />} />
<Route path="new-booking" element={<NewBooking addBooking={addBooking} />} />
<Route path="booking-details" element={<BookingDetails addBooking={addBooking} />} />
<Route path="course-schedule" element={<CourseSchedule />} />
<Route path="course-schedule/:courseId" element={<CourseScheduleView />} />
<Route path="booking-settings" element={<BookingSettings />} />

View File

@ -15,6 +15,7 @@ export function BookingModal({
setEndTimeIndex,
className,
onClose,
onNavigateToDetails,
isOpen = true
}) {
const booking = useBookingContext();
@ -24,7 +25,6 @@ export function BookingModal({
const initialEndTimeIndex = booking.selectedBookingLength > 0 ? startTimeIndex + booking.selectedBookingLength :
(hoursAvailable === 1 ? startTimeIndex + 1 : null); // Auto-select 30 min if that's all that's available
const [selectedEndTimeIndex, setSelectedEndTimeIndex] = useState(null);
const [currentStep, setCurrentStep] = useState(1);
const hasInitialized = useRef(false);
// Store the original hours available to prevent it from changing when selections are made
@ -99,16 +99,19 @@ export function BookingModal({
return durationSlots * 0.5; // Each slot is 30 minutes
};
const handleNextStep = () => {
if (currentStep === 1 && hasSelectedEndTime) {
setCurrentStep(2);
}
};
const handleBackStep = () => {
if (currentStep === 2) {
setCurrentStep(1);
const handleNavigateToDetails = () => {
console.log('handleNavigateToDetails called', { hasSelectedEndTime, onNavigateToDetails });
onNavigateToDetails();
/*
if (hasSelectedEndTime) {
// Close modal first, then navigate
onClose && onClose();
setTimeout(() => {
onNavigateToDetails && onNavigateToDetails();
}, 100);
}
*/
};
@ -122,69 +125,46 @@ export function BookingModal({
>
<Dialog style={{overflow: 'visible'}}>
<form>
{currentStep === 1 ? (
<>
<Heading slot="title">Välj sluttid</Heading>
<p>{convertDateObjectToString(booking.selectedDate)}</p>
<div className={styles.timeDisplay}>
<div className={styles.timeRange}>
<div className={styles.startTimeSection}>
<label>Starttid</label>
<div className={styles.startTimeValue}>{getTimeFromIndex(startTimeIndex)}</div>
</div>
<div className={styles.endTimeSection}>
<label>Sluttid</label>
<Dropdown
options={endTimeOptions}
disabledOptions={disabledOptions}
onChange={handleChange}
value={selectedEndTimeIndex || ""}
placeholder={!initialEndTimeIndex ? {
value: "",
label: "Välj sluttid"
} : null}
className={styles.endTimeDropdown}
/>
</div>
<div className={styles.modalContent}>
<Heading slot="title">Välj sluttid</Heading>
<p>{convertDateObjectToString(booking.selectedDate)}</p>
<div className={styles.timeDisplay}>
<div className={styles.timeRange}>
<div className={styles.startTimeSection}>
<label>Starttid</label>
<div className={styles.startTimeValue}>{getTimeFromIndex(startTimeIndex)}</div>
</div>
<div className={styles.endTimeSection}>
<label>Sluttid</label>
<Dropdown
options={endTimeOptions}
disabledOptions={disabledOptions}
onChange={handleChange}
value={selectedEndTimeIndex || ""}
placeholder={!initialEndTimeIndex ? {
value: "",
label: "Välj sluttid"
} : null}
className={styles.endTimeDropdown}
/>
</div>
</div>
</div>
</div>
<div className={styles.modalFooter}>
<Button className={styles.cancelButton} slot="close">
Avbryt
</Button>
<Button
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
onClick={hasSelectedEndTime ? handleNextStep : undefined}
isDisabled={!hasSelectedEndTime}
>
{hasSelectedEndTime ? 'Nästa' : 'Välj sluttid först'}
</Button>
</div>
</>
) : (
<>
<Heading slot="title">Bokningsuppgifter</Heading>
<p>{convertDateObjectToString(booking.selectedDate)} · {getTimeFromIndex(startTimeIndex)} - {getTimeFromIndex(selectedEndTimeIndex)}</p>
<BookingTitleField />
<ParticipantsSelector />
<div className={styles.modalFooter}>
<Button
className={styles.cancelButton}
onClick={handleBackStep}
>
Tillbaka
</Button>
<Button
className={styles.saveButton}
onClick={booking.handleSave}
>
Boka
</Button>
</div>
</>
)}
<div className={styles.modalFooter}>
<Button className={styles.cancelButton} slot="close">
Avbryt
</Button>
<button
type="button"
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
onClick={hasSelectedEndTime ? handleNavigateToDetails : undefined}
disabled={!hasSelectedEndTime}
>
{hasSelectedEndTime ? 'Nästa' : 'Välj sluttid först'}
</button>
</div>
</form>
</Dialog>
</Modal>

View File

@ -311,4 +311,44 @@
flex-direction: column;
gap: 1rem;
margin: 1rem 0;
}
/* Consistent modal sizing */
:global(.react-aria-ModalOverlay .react-aria-Modal.react-aria-Modal) {
height: 550px !important;
width: 400px !important;
max-width: 90vw !important;
min-height: 550px !important;
max-height: 550px !important;
}
:global(.react-aria-ModalOverlay .react-aria-Modal.react-aria-Modal form) {
height: 100% !important;
width: 100% !important;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
}
.modalContent {
height: 450px;
overflow-y: auto;
padding: 1.5rem;
flex-shrink: 0;
}
.modalFooter {
height: fit-content;
width: 100%;
color: var(--color-primary);
display: flex;
align-items: center;
gap: 1rem;
margin: 0;
padding-top: 1rem;
border-top: 1px solid var(--border-light);
flex-shrink: 0;
position: relative;
z-index: 1;
}

View File

@ -122,7 +122,7 @@
border: 1px solid var(--dropdown-border);
border-radius: 0.5rem;
box-shadow: var(--dropdown-shadow);
z-index: 1200;
z-index: 1000;
max-height: 300px;
overflow-y: auto;
margin-top: 0rem;
@ -310,4 +310,5 @@
outline: 2px solid var(--color-primary);
outline-offset: 2px;
border-color: var(--color-primary);
}
}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import TimeCard from './TimeCard';
import { InlineBookingForm } from '../booking/InlineBookingForm';
import { BookingModal } from '../booking/BookingModal';
@ -10,11 +11,26 @@ import { useSettingsContext } from '../../context/SettingsContext';
const SLOT_GROUPING_SIZE = 8;
export function TimeCardContainer() {
const navigate = useNavigate();
const booking = useBookingContext();
const { settings } = useSettingsContext();
// Check if we should use inline form
const useInlineForm = settings.bookingFormType === 'inline';
const handleNavigateToDetails = () => {
console.log('TimeCardContainer handleNavigateToDetails called, navigating to /booking-details');
navigate('/booking-details', {
state: {
selectedDate: booking.selectedDate,
selectedStartIndex: booking.selectedStartIndex,
selectedEndIndex: booking.selectedEndIndex,
assignedRoom: booking.assignedRoom,
title: booking.title,
participants: booking.participants
}
});
};
const slotCount = 24; // 12 hours * 2 slots per hour (8:00 to 20:00)
const slotIndices = Array.from({ length: slotCount }, (_, i) => i);
@ -160,6 +176,7 @@ export function TimeCardContainer() {
setEndTimeIndex={booking.setSelectedEndIndex}
className={modalStyles.modalContainer}
onClose={() => booking.resetTimeSelections()}
onNavigateToDetails={handleNavigateToDetails}
isOpen={true}
/>
)}

View File

@ -15,27 +15,36 @@ export function getTimeFromIndex(timeIndex) {
}
export function convertDateObjectToString( date ) {
const dayIndex = getDayOfWeek(date, "en-US");
const monthIndex = date.month;
// Always use long format for now
const isSmallScreen = false;
if (isSmallScreen) {
const days = ["Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"];
const months = ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"];
try {
const dayIndex = getDayOfWeek(date, "en-US");
const monthIndex = date.month;
const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
// Always use long format for now
const isSmallScreen = false;
return `${dayOfWeek} ${date.day} ${monthName}`;
} else {
const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
const months = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`;
if (isSmallScreen) {
const days = ["Mån", "Tis", "Ons", "Tor", "Fre", "Lör", "Sön"];
const months = ["Jan", "Feb", "Mar", "Apr", "Maj", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dec"];
const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
return `${dayOfWeek} ${date.day} ${monthName}`;
} else {
const days = ["Måndag", "Tisdag", "Onsdag", "Torsdag", "Fredag", "Lördag", "Söndag"];
const months = ["Januari", "Februari", "Mars", "April", "Maj", "Juni", "Juli", "Augusti", "September", "Oktober", "November", "December"];
const dayOfWeek = (dayIndex >= 0 && dayIndex < 7) ? days[dayIndex === 0 ? 6 : dayIndex - 1] : "Ogiltig dag";
const monthName = (monthIndex >= 1 && monthIndex <= 12) ? months[monthIndex - 1] : "Ogiltig månad";
return `${dayOfWeek} ${date.day} ${monthName} ${date.year}`;
}
} catch (error) {
console.error('Error converting date to string:', error);
// Fallback to a simple format if the date conversion fails
if (date && typeof date === 'object' && date.day && date.month && date.year) {
return `${date.day}/${date.month}/${date.year}`;
}
return 'Ogiltigt datum';
}
}

View File

@ -187,6 +187,10 @@ export function useBookingState(addBooking, initialDate = null) {
// Setters
setTitle,
setSelectedEndIndex,
setSelectedDate,
setSelectedStartIndex,
setAssignedRoom,
setParticipants,
// Handlers
handleTimeCardClick,
@ -222,5 +226,9 @@ export function useBookingState(addBooking, initialDate = null) {
handleParticipantChange,
handleRemoveParticipant,
resetTimeSelections,
setSelectedDate,
setSelectedStartIndex,
setAssignedRoom,
setParticipants,
]);
}

View File

@ -0,0 +1,83 @@
import React, { useEffect } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import styles from './BookingDetails.module.css';
import { BookingTitleField } from '../components/forms/BookingTitleField';
import { ParticipantsSelector } from '../components/forms/ParticipantsSelector';
import { useBookingContext } from '../context/BookingContext';
import { BookingProvider } from '../context/BookingContext';
import { useSettingsContext } from '../context/SettingsContext';
import { useBookingState } from '../hooks/useBookingState';
import { convertDateObjectToString, getTimeFromIndex } from '../helpers';
export function BookingDetails({ addBooking }) {
const navigate = useNavigate();
const location = useLocation();
const { getEffectiveToday } = useSettingsContext();
const booking = useBookingState(addBooking, getEffectiveToday());
useEffect(() => {
window.scrollTo(0, 0);
}, []);
// Populate booking state from navigation state if available
useEffect(() => {
const navigationState = location.state;
if (navigationState) {
// Update booking state with navigation data
if (navigationState.selectedDate) booking.setSelectedDate(navigationState.selectedDate);
if (navigationState.selectedStartIndex !== undefined) booking.setSelectedStartIndex(navigationState.selectedStartIndex);
if (navigationState.selectedEndIndex !== undefined) booking.setSelectedEndIndex(navigationState.selectedEndIndex);
if (navigationState.assignedRoom) booking.setAssignedRoom(navigationState.assignedRoom);
if (navigationState.title) booking.setTitle(navigationState.title);
if (navigationState.participants) booking.setParticipants(navigationState.participants);
} else if (!booking.selectedDate || !booking.selectedStartIndex || !booking.selectedEndIndex) {
// Redirect back if no booking data from navigation or state
navigate('/new-booking');
}
}, [location.state, navigate]);
const handleBack = () => {
navigate(-1);
};
const handleSave = () => {
booking.handleSave();
};
return (
<BookingProvider value={booking}>
<div className={styles.pageContainer}>
<div className={styles.header}>
<button
className={styles.backButton}
onClick={handleBack}
>
Tillbaka
</button>
<h2>Bokningsuppgifter</h2>
</div>
<div className={styles.timeInfo}>
<p className={styles.dateTime}>
{convertDateObjectToString(booking.selectedDate)} · {getTimeFromIndex(booking.selectedStartIndex)} - {getTimeFromIndex(booking.selectedEndIndex)}
</p>
</div>
<div className={styles.formContainer}>
<BookingTitleField />
<ParticipantsSelector />
</div>
<div className={styles.footer}>
<button
className={styles.saveButton}
onClick={handleSave}
>
Boka
</button>
</div>
</div>
</BookingProvider>
);
}

View File

@ -0,0 +1,91 @@
.pageContainer {
margin: 0 auto;
padding: 1rem;
min-height: calc(100vh - 4rem); /* Adjust for header/footer height */
display: flex;
flex-direction: column;
width: 100%;
}
.header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 2rem;
}
.backButton {
background: none;
border: none;
font-size: 1rem;
color: var(--color-primary);
cursor: pointer;
padding: 0.5rem;
border-radius: 0.25rem;
transition: background-color 0.2s ease;
}
.backButton:hover {
background-color: var(--bg-secondary);
}
.header h2 {
margin: 0;
font-size: 1.5rem;
color: var(--text-primary);
}
.timeInfo {
background: var(--bg-secondary);
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 2rem;
}
.dateTime {
margin: 0;
font-size: 1.1rem;
font-weight: 500;
color: var(--text-primary);
}
.formContainer {
display: flex;
flex-direction: column;
gap: 2rem;
}
.footer {
margin-top: 2rem;
padding-top: 2rem;
border-top: 1px solid var(--border-light);
width: 100%;
padding: 1rem;
}
.saveButton {
width: 100%;
padding: 1rem;
background-color: var(--color-primary);
color: var(--color-white);
border: none;
border-radius: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: background-color 0.2s ease;
box-sizing: border-box;
}
.saveButton:hover {
background-color: var(--color-primary-dark);
}
.saveButton:active {
transform: translateY(1px);
}
.saveButton:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}