eriks-booking-variant #6

Merged
jare2473 merged 13 commits from eriks-booking-variant into main 2025-09-22 11:16:13 +02:00
10 changed files with 274 additions and 24 deletions
Showing only changes of commit c5820294f9 - Show all commits

View File

@@ -1,15 +1,33 @@
import React, { useState, useEffect, useRef } from 'react';
import { Button } from 'react-aria-components';
import { useNavigate } from 'react-router-dom';
import { convertDateObjectToString, getTimeFromIndex } from '../../helpers';
import { convertDateObjectToString, getTimeFromIndex, formatBookingDate } from '../../helpers';
import Dropdown from '../ui/Dropdown';
import { Chip } from '../ui/Chip';
import { BookingTitleField } from '../forms/BookingTitleField';
import { ParticipantsSelector } from '../forms/ParticipantsSelector';
import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
import { generateId } from '../../utils/bookingUtils';
import { USER } from '../../constants/bookingConstants';
import styles from './InlineModalBookingForm.module.css';
import extendedStyles from './InlineModalExtendedBookingForm.module.css';
// Helper function to get room category
function getRoomCategory(roomName) {
// Extract room number from room name (e.g., "G5:7" -> 7)
const roomNumber = parseInt(roomName.split(':')[1]);
// Assign categories based on room number ranges
if (roomNumber >= 1 && roomNumber <= 4) return 'green';
if (roomNumber >= 5 && roomNumber <= 8) return 'red';
if (roomNumber >= 9 && roomNumber <= 12) return 'blue';
if (roomNumber >= 13 && roomNumber <= 15) return 'yellow';
// Default fallback
return 'green';
}
export function InlineModalExtendedBookingForm({
startTimeIndex,
hoursAvailable,
@@ -17,6 +35,7 @@ export function InlineModalExtendedBookingForm({
setEndTimeIndex,
onClose,
onNavigateToDetails,
addBooking,
arrowPointsLeft = true
}) {
const navigate = useNavigate();
@@ -27,6 +46,7 @@ export function InlineModalExtendedBookingForm({
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 [showConfirmation, setShowConfirmation] = useState(false);
const hasInitialized = useRef(false);
// Store the original hours available to prevent it from changing when selections are made
@@ -43,6 +63,8 @@ export function InlineModalExtendedBookingForm({
booking.setSelectedEndIndex(initialEndTimeIndex);
hasInitialized.current = true;
}
console.log("Booking:", booking);
}, [initialEndTimeIndex, setEndTimeIndex, booking]);
// Generate end time options based on available hours
@@ -93,22 +115,101 @@ export function InlineModalExtendedBookingForm({
// Check if user has selected an end time (including pre-selected)
const hasSelectedEndTime = selectedEndTimeIndex !== null;
const handleNavigateToConfirmation = () => {
if (hasSelectedEndTime) {
// Navigate directly to booking confirmation instead of details
navigate('/booking-confirmation', {
state: {
selectedDate: booking.selectedDate,
selectedStartIndex: booking.selectedStartIndex,
selectedEndIndex: booking.selectedEndIndex,
assignedRoom: booking.assignedRoom,
title: booking.title,
participants: booking.participants
}
const handleSave = () => {
if (hasSelectedEndTime && addBooking) {
console.log('Booking context state:', {
title: booking.title,
participants: booking.participants,
selectedRoom: booking.selectedRoom,
assignedRoom: booking.assignedRoom
});
// Create a booking object with the same logic as in useBookingState
const roomToBook = booking.selectedRoom !== "allRooms" ? booking.selectedRoom : booking.assignedRoom;
// Include the current user as a participant if not already added
const allParticipants = booking.participants.find(p => p.id === USER.id)
? booking.participants
: [USER, ...booking.participants];
const finalTitle = booking.title !== "" ? booking.title : getDefaultBookingTitle();
const newBooking = {
id: generateId(),
date: booking.selectedDate,
startTime: booking.selectedStartIndex,
endTime: booking.selectedEndIndex,
room: roomToBook,
roomCategory: getRoomCategory(roomToBook),
title: finalTitle,
participants: allParticipants
};
console.log('Creating booking:', newBooking);
console.log('Final title used:', finalTitle);
// Save the booking using the passed addBooking function
addBooking(newBooking);
// Show confirmation page within the modal instead of navigating
setShowConfirmation(true);
}
};
// Show confirmation page if user pressed save
if (showConfirmation) {
return (
<div className={`${styles.inlineForm} ${arrowPointsLeft ? styles.arrowLeft : styles.arrowRight}`}>
<div className={styles.formHeader}>
<div style={{ textAlign: 'center', padding: '1rem' }}>
<h3 style={{ margin: '0 0 0.5rem 0', color: '#28a745' }}>Bokning sparad!</h3>
<p style={{ margin: '0', fontSize: '0.9rem', color: '#666' }}>
{booking.title ? `${booking.title}` : `${getDefaultBookingTitle()}`}
</p>
{(() => {
// Include the current user as a participant if not already added
const allParticipants = booking.participants.find(p => p.id === USER.id)
? booking.participants
: [USER, ...booking.participants];
const startTime = getTimeFromIndex(booking.selectedStartIndex);
const endTime = getTimeFromIndex(booking.selectedEndIndex);
const dateStr = formatBookingDate(booking.selectedDate);
const roomName = booking.selectedRoom !== "allRooms" ? booking.selectedRoom : booking.assignedRoom;
return (
<div style={{ marginTop: '0.5rem' }}>
<p style={{ margin: '0', fontSize: '0.85rem', color: '#888' }}>
{dateStr} {startTime} - {endTime}
</p>
<div style={{ margin: '0.5rem 0 0 0', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '0.5rem' }}>
<span style={{ fontSize: '0.85rem', color: '#888' }}>Rum:</span>
<Chip variant="room" size="medium" href={`#room-${roomName}`}>
{roomName}
</Chip>
</div>
{allParticipants.length > 0 && (
<p style={{ margin: '0.25rem 0 0 0', fontSize: '0.85rem', color: '#888' }}>
Deltagare: {allParticipants.map(p => p.name).join(', ')}
</p>
)}
</div>
);
})()}
</div>
</div>
<hr className={extendedStyles.divider} />
<div className={styles.formActions}>
<Button className={styles.saveButton} onPress={onClose}>
Stäng
</Button>
</div>
</div>
);
}
return (
<div className={`${styles.inlineForm} ${arrowPointsLeft ? styles.arrowLeft : styles.arrowRight}`}>
{/* Header */}
@@ -151,7 +252,7 @@ export function InlineModalExtendedBookingForm({
</Button>
<Button
className={`${styles.saveButton} ${!hasSelectedEndTime ? styles.disabledButton : ''}`}
onPress={hasSelectedEndTime ? handleNavigateToConfirmation : undefined}
onPress={hasSelectedEndTime ? handleSave : undefined}
isDisabled={!hasSelectedEndTime}
>
{hasSelectedEndTime ? 'Boka' : 'Välj sluttid först'}

View File

@@ -4,7 +4,7 @@ import { BOOKING_LENGTHS } from '../../constants/bookingConstants';
import { useBookingContext } from '../../context/BookingContext';
import styles from './BookingLengthField.module.css';
export function BookingLengthField() {
export function BookingLengthField({ clean = false }) {
const booking = useBookingContext();
return (
@@ -19,6 +19,7 @@ export function BookingLengthField() {
value: 0
}}
disabledOptions={booking.disabledOptions}
clean={clean}
/>
</div>
);

View File

@@ -4,7 +4,7 @@ import { useBookingContext } from '../../context/BookingContext';
import { useSettingsContext } from '../../context/SettingsContext';
import styles from './RoomSelectionField.module.css';
export function RoomSelectionField() {
export function RoomSelectionField({ clean = false }) {
const booking = useBookingContext();
const { settings } = useSettingsContext();
@@ -27,6 +27,7 @@ export function RoomSelectionField() {
label: "Alla rum",
value: "allRooms"
}}
clean={clean}
/>
</div>
);

View File

@@ -0,0 +1,44 @@
import React from 'react';
import styles from './Chip.module.css';
export function Chip({
children,
onClick,
href,
variant = 'default',
size = 'medium',
className = '',
...props
}) {
const baseClasses = `${styles.chip} ${styles[variant]} ${styles[size]} ${className}`;
if (href) {
return (
<a
href={href}
className={`${baseClasses} ${styles.link}`}
{...props}
>
{children}
</a>
);
}
if (onClick) {
return (
<button
onClick={onClick}
className={`${baseClasses} ${styles.button}`}
{...props}
>
{children}
</button>
);
}
return (
<span className={baseClasses} {...props}>
{children}
</span>
);
}

View File

@@ -0,0 +1,88 @@
.chip {
display: inline-flex;
align-items: center;
border-radius: 12px;
font-weight: 500;
text-decoration: none;
border: none;
cursor: default;
transition: all 0.2s ease;
white-space: nowrap;
width: fit-content;
}
/* Variants */
.default {
background-color: #f3f4f6;
color: #374151;
}
.primary {
background-color: #3b82f6;
color: white;
}
.success {
background-color: #10b981;
color: white;
}
.warning {
background-color: #f59e0b;
color: white;
}
.error {
background-color: #ef4444;
color: white;
}
.room {
background-color: #e0e7ff;
color: #3730a3;
border: 1px solid #c7d2fe;
}
/* Sizes */
.small {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
.medium {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
.large {
padding: 0.5rem 1rem;
font-size: 1rem;
}
/* Interactive states */
.link,
.button {
cursor: pointer;
}
.link:hover,
.button:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.link:active,
.button:active {
transform: translateY(0);
}
.room.link:hover,
.room.button:hover {
background-color: #c7d2fe;
border-color: #a5b4fc;
}
/* Remove button styling */
.button {
font-family: inherit;
}

View File

@@ -3,13 +3,13 @@ import styles from "./Dropdown.module.css";
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faChevronDown } from '@fortawesome/free-solid-svg-icons'
const Dropdown = ({ options, value, onChange, placeholder = {value: "", label: "Select an option"}, disabledOptions = false, className }) => {
const Dropdown = ({ options, value, onChange, placeholder = {value: "", label: "Select an option"}, disabledOptions = false, className, clean = false }) => {
return (
<div className={`${styles.dropdownWrapper} ${className || ''}`}>
<select
value={value}
onChange={onChange}
className={styles.select}
className={`${styles.select} ${clean ? styles.clean : ''}`}
>
{placeholder && (
<option value={placeholder.value} disabled={false} hidden={false}>

View File

@@ -29,3 +29,9 @@
font-size: 0.8rem;
z-index: 1;
}
.clean {
border: none;
background: transparent;
padding-left: 0;
}

View File

@@ -13,7 +13,7 @@ import { useSettingsContext } from '../../context/SettingsContext';
const SLOT_GROUPING_SIZE = 8;
export function TimeCardContainer() {
export function TimeCardContainer({ addBooking }) {
const navigate = useNavigate();
const booking = useBookingContext();
const { settings } = useSettingsContext();
@@ -188,6 +188,7 @@ export function TimeCardContainer() {
setEndTimeIndex={booking.setSelectedEndIndex}
onClose={() => booking.resetTimeSelections()}
onNavigateToDetails={handleNavigateToDetails}
addBooking={addBooking}
arrowPointsLeft={isLeftCard}
/>
);

View File

@@ -68,9 +68,6 @@ export function NewBooking({ addBooking }) {
{/* Filter Section */}
<div className={styles.headerAndFilter}>
<h3 className={styles.elementHeading} style={{ padding: "0 0.5rem" }}>
Välj starttid
</h3>
<div className={styles.filtersSection}>
{settings.showFiltersAlways ? (
/* Always-visible filters */
@@ -115,10 +112,13 @@ export function NewBooking({ addBooking }) {
</>
)}
</div>
<h3 className={styles.elementHeading} style={{ padding: "0 0.5rem" }}>
Välj starttid
</h3>
</div>
<div>
<TimeCardContainer />
<TimeCardContainer addBooking={addBooking} />
</div>
</div>
</main>

View File

@@ -40,7 +40,6 @@
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 2rem
}
@@ -236,6 +235,15 @@
animation: slideDown 0.2s ease-out;
}
.filtersContentClean {
width: fit-content;
max-width: 600px;
padding: 1rem;
background: transparent;
border: none;
border-radius: 0;
}
.filtersRow {
display: flex;
gap: 1rem;