eriks-booking-variant #6
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
44
my-app/src/components/ui/Chip.jsx
Normal file
44
my-app/src/components/ui/Chip.jsx
Normal 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>
|
||||
);
|
||||
}
|
||||
88
my-app/src/components/ui/Chip.module.css
Normal file
88
my-app/src/components/ui/Chip.module.css
Normal 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;
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -29,3 +29,9 @@
|
||||
font-size: 0.8rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.clean {
|
||||
border: none;
|
||||
background: transparent;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user