mirror of
https://github.com/xlorepdarkhelm/numinar-coding-project.git
synced 2026-09-09 08:19:48 -04:00
Cleaning up the frontend style.
Signed-off-by: Cliff Hill <xlorep@darkhelm.org>
This commit is contained in:
@@ -132,6 +132,10 @@ class Booking(Base):
|
||||
"""
|
||||
return self._invitees
|
||||
|
||||
def reset_invitee_cache(self) -> None:
|
||||
"""Reset the cached list of invitee email addresses."""
|
||||
self._invitee_emails = None
|
||||
|
||||
|
||||
class Invitee(Base):
|
||||
"""Model representing an invitee to a booking in the application.
|
||||
|
||||
@@ -62,6 +62,7 @@ class BookingUpdate(BaseModel):
|
||||
start_time: datetime | None = None
|
||||
end_time: datetime | None = None
|
||||
title: str | None = None
|
||||
invitees: list[str] | None = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@@ -415,6 +415,7 @@ async def update_booking(
|
||||
logger.debug(
|
||||
f"Entering update_booking with booking_id: {booking_id}, params: {kwargs}"
|
||||
)
|
||||
|
||||
try:
|
||||
# Fetch current booking to compare and validate changes
|
||||
current = await get_booking(session, booking_id)
|
||||
@@ -435,6 +436,9 @@ async def update_booking(
|
||||
|
||||
# Remove old invitees
|
||||
await _remove_old_invitees(session, current, new_invitees)
|
||||
await session.commit() # Ensure deletions are persisted
|
||||
await session.refresh(current) # Refresh relationship from DB
|
||||
current.reset_invitee_cache()
|
||||
|
||||
# Run validations
|
||||
logger.debug(
|
||||
@@ -468,6 +472,16 @@ async def update_booking(
|
||||
new_invitees,
|
||||
{i.user_email for i in updated.get_invitee_objects()},
|
||||
)
|
||||
await session.commit()
|
||||
|
||||
# Always refresh and reset cache after adding invitees
|
||||
updated = await get_booking(session, booking_id)
|
||||
await session.refresh(updated)
|
||||
updated.reset_invitee_cache()
|
||||
|
||||
# Force reload of invitee relationship and cache to ensure up-to-date list
|
||||
# This ensures that updated.invitees returns the correct, current list
|
||||
_ = [i.user_email for i in updated.get_invitee_objects()]
|
||||
|
||||
logger.info(f"Successfully updated booking with id: {booking_id}")
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
"eslint-plugin-jest": "29.0.1",
|
||||
"eslint-plugin-jsdoc": "60.1.1",
|
||||
"eslint-plugin-react": "7.37.5",
|
||||
"eslint-plugin-react-hooks": "^6.1.1",
|
||||
"jest": "30.1.1",
|
||||
"jest-environment-jsdom": "30.1.1",
|
||||
"jsdom": "26.1.0",
|
||||
|
||||
@@ -10,6 +10,8 @@ import { logger } from "../utils/logger";
|
||||
|
||||
// External imports
|
||||
import React, { useState, useMemo, useCallback } from "react";
|
||||
// For type usage in JSX event handlers
|
||||
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
@@ -57,7 +59,7 @@ interface BookingFormProps {
|
||||
* Called when booking is successful or deleted.
|
||||
* If booking is provided, navigate to confirmation. If not, just close the form.
|
||||
*/
|
||||
onBookingSuccess?: (_booking?: Booking) => void;
|
||||
onBookingSuccess?: () => void;
|
||||
}
|
||||
|
||||
const BookingForm: React.FC<BookingFormProps> = ({
|
||||
@@ -298,14 +300,14 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
title,
|
||||
invitees,
|
||||
};
|
||||
// logger.info("[BookingForm] updateBooking payload:", updatePayload);
|
||||
logger.info("[BookingForm] updateBooking payload:", updatePayload);
|
||||
bookingResult = await updateBooking(
|
||||
Number(editBooking.id),
|
||||
updatePayload
|
||||
);
|
||||
}
|
||||
if (onBookingSuccess && bookingResult) {
|
||||
onBookingSuccess(bookingResult);
|
||||
onBookingSuccess();
|
||||
}
|
||||
onClose();
|
||||
} catch (err: unknown) {
|
||||
@@ -352,7 +354,7 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xs" fullWidth>
|
||||
<DialogTitle>
|
||||
{isViewMode
|
||||
? "View Booking"
|
||||
? "View Past Booking"
|
||||
: isEdit
|
||||
? "Update Booking"
|
||||
: "New Booking"}
|
||||
@@ -366,14 +368,46 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
<DialogContent>
|
||||
{/* Room selection */}
|
||||
<Box marginY={2}>
|
||||
<RoomSelect
|
||||
selectedRoomId={room_id}
|
||||
onChange={(id: string | number) =>
|
||||
handleRoomChange({ target: { value: id } } as SelectChangeEvent)
|
||||
}
|
||||
label="Room"
|
||||
minWidth={180}
|
||||
/>
|
||||
{isViewMode ? (
|
||||
<TextField
|
||||
label="Room"
|
||||
value={
|
||||
currentRoom && currentRoom.name ? currentRoom.name : "\u00A0"
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
sx: {
|
||||
color: "rgba(34,34,34,0.8) !important",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
inputProps: {
|
||||
style: {
|
||||
color: "rgba(34,34,34,0.8)",
|
||||
background: "#f0f0f0",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<RoomSelect
|
||||
selectedRoomId={room_id}
|
||||
onChange={(id: string | number) =>
|
||||
handleRoomChange({
|
||||
target: { value: id },
|
||||
} as SelectChangeEvent)
|
||||
}
|
||||
label="Room"
|
||||
minWidth={180}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<Box color="error.main" fontSize={13}>
|
||||
{errors.room_id}
|
||||
</Box>
|
||||
@@ -381,11 +415,30 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
{/* Title */}
|
||||
<TextField
|
||||
label="Title (optional)"
|
||||
value={title}
|
||||
value={isViewMode ? (title ? title : "\u00A0") : title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
InputProps={{ readOnly: isViewMode }}
|
||||
InputProps={
|
||||
isViewMode
|
||||
? {
|
||||
readOnly: true,
|
||||
sx: {
|
||||
color: "rgba(34,34,34,0.8) !important",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
inputProps: {
|
||||
style: {
|
||||
color: "rgba(34,34,34,0.8)",
|
||||
background: "#f0f0f0",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
{/* Start time */}
|
||||
@@ -394,11 +447,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
type="datetime-local"
|
||||
value={(() => {
|
||||
if (!start) {
|
||||
return "";
|
||||
return isViewMode ? "\u00A0" : "";
|
||||
}
|
||||
const d = new Date(start);
|
||||
if (isNaN(d.getTime())) {
|
||||
return "";
|
||||
return isViewMode ? "\u00A0" : "";
|
||||
}
|
||||
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
|
||||
@@ -409,8 +462,27 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
error={!!errors.start}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
required
|
||||
InputProps={{ readOnly: isViewMode }}
|
||||
{...(!isViewMode && { required: true })}
|
||||
InputProps={
|
||||
isViewMode
|
||||
? {
|
||||
readOnly: true,
|
||||
sx: {
|
||||
color: "rgba(34,34,34,0.8) !important",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
inputProps: {
|
||||
style: {
|
||||
color: "rgba(34,34,34,0.8)",
|
||||
background: "#f0f0f0",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<Box color="error.main" fontSize={13} mb={1}>
|
||||
@@ -422,11 +494,11 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
type="datetime-local"
|
||||
value={(() => {
|
||||
if (!end) {
|
||||
return "";
|
||||
return isViewMode ? "\u00A0" : "";
|
||||
}
|
||||
const d = new Date(end);
|
||||
if (isNaN(d.getTime())) {
|
||||
return "";
|
||||
return isViewMode ? "\u00A0" : "";
|
||||
}
|
||||
const pad = (n: number) => n.toString().padStart(2, "0");
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(
|
||||
@@ -437,179 +509,279 @@ const BookingForm: React.FC<BookingFormProps> = ({
|
||||
error={!!errors.end}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
required
|
||||
InputProps={{ readOnly: isViewMode }}
|
||||
{...(!isViewMode && { required: true })}
|
||||
InputProps={
|
||||
isViewMode
|
||||
? {
|
||||
readOnly: true,
|
||||
sx: {
|
||||
color: "rgba(34,34,34,0.8) !important",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
inputProps: {
|
||||
style: {
|
||||
color: "rgba(34,34,34,0.8)",
|
||||
background: "#f0f0f0",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}
|
||||
}
|
||||
disabled={isViewMode}
|
||||
/>
|
||||
<Box color="error.main" fontSize={13} mb={1}>
|
||||
{errors.end}
|
||||
</Box>
|
||||
{/* Invitees */}
|
||||
<FormControl fullWidth margin="normal" error={remainingSlots < 0}>
|
||||
<InputLabel id="invitees-label">Invitees</InputLabel>
|
||||
<Select
|
||||
labelId="invitees-label"
|
||||
multiple
|
||||
value={invitees}
|
||||
onChange={handleInviteeChange}
|
||||
input={<OutlinedInput label="Invitees" readOnly={isViewMode} />}
|
||||
renderValue={(selected) => (
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||
{(selected as string[]).map((value, idx) => {
|
||||
const user = users.find(
|
||||
(u: import("../interfaces").User) => u.email === value
|
||||
);
|
||||
return (
|
||||
<Chip
|
||||
key={value}
|
||||
label={user ? user.name : value}
|
||||
color={errors[`invitee_${idx}`] ? "error" : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<MenuItem
|
||||
disabled
|
||||
sx={{
|
||||
color: remainingSlots < 0 ? "error.main" : "#222",
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
pointerEvents: "none",
|
||||
background: "inherit",
|
||||
}}
|
||||
{isViewMode ? (
|
||||
<TextField
|
||||
label="Invitees"
|
||||
value={
|
||||
invitees.length === 0
|
||||
? "\u00A0"
|
||||
: invitees
|
||||
.map((value) => {
|
||||
const user = users.find((u) => u.email === value);
|
||||
if (user && user.name && user.email) {
|
||||
return `${user.name} <${user.email}>`;
|
||||
} else if (user && user.name) {
|
||||
return user.name;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
})
|
||||
.join(", ")
|
||||
}
|
||||
fullWidth
|
||||
margin="normal"
|
||||
InputProps={{
|
||||
readOnly: true,
|
||||
sx: {
|
||||
color: "rgba(34,34,34,0.8) !important",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
inputProps: {
|
||||
style: {
|
||||
color: "rgba(34,34,34,0.8)",
|
||||
background: "#f0f0f0",
|
||||
opacity: 0.8,
|
||||
WebkitTextFillColor: "rgba(34,34,34,0.8)",
|
||||
},
|
||||
},
|
||||
}}
|
||||
disabled
|
||||
placeholder={invitees.length === 0 ? "No invitees" : undefined}
|
||||
/>
|
||||
) : (
|
||||
<FormControl fullWidth margin="normal" error={remainingSlots < 0}>
|
||||
<InputLabel
|
||||
id="invitees-label"
|
||||
sx={{ backgroundColor: "#fff", opacity: 1, px: 0.5 }}
|
||||
>
|
||||
{remainingSlots >= 0
|
||||
? `${remainingSlots} invitee slot${
|
||||
remainingSlots === 1 ? "" : "s"
|
||||
} left`
|
||||
: `${Math.abs(remainingSlots)} over room capacity`}
|
||||
</MenuItem>
|
||||
{users.map((user: import("../interfaces").User) => {
|
||||
const email = user.email ?? user.name;
|
||||
const isSelected = invitees.includes(email);
|
||||
// User unavailable if booked for another event at this time
|
||||
const unavailable = bookings.some((b: Booking) => {
|
||||
if (isEdit && b.id === editBooking?.id) {
|
||||
return false;
|
||||
Invitees
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="invitees-label"
|
||||
multiple
|
||||
value={invitees}
|
||||
onChange={handleInviteeChange}
|
||||
input={<OutlinedInput label="Invitees" readOnly={isViewMode} />}
|
||||
renderValue={(selected) => (
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5 }}>
|
||||
{(selected as string[]).map((value, idx) => {
|
||||
const user = users.find(
|
||||
(u: import("../interfaces").User) => u.email === value
|
||||
);
|
||||
let label;
|
||||
if (user && user.name && user.email) {
|
||||
label = `${user.name} <${user.email}>`;
|
||||
} else if (user && user.name) {
|
||||
label = user.name;
|
||||
} else {
|
||||
label = value;
|
||||
}
|
||||
return (
|
||||
<Chip
|
||||
key={value}
|
||||
label={label}
|
||||
color={errors[`invitee_${idx}`] ? "error" : undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
)}
|
||||
disabled={isViewMode}
|
||||
>
|
||||
<MenuItem
|
||||
disabled
|
||||
sx={{
|
||||
color: remainingSlots < 0 ? "error.main" : "#222",
|
||||
fontWeight: 600,
|
||||
fontSize: 15,
|
||||
pointerEvents: "none",
|
||||
background: "inherit",
|
||||
}}
|
||||
>
|
||||
{remainingSlots >= 0
|
||||
? `${remainingSlots} invitee slot${
|
||||
remainingSlots === 1 ? "" : "s"
|
||||
} left`
|
||||
: `${Math.abs(remainingSlots)} over room capacity`}
|
||||
</MenuItem>
|
||||
{users.map((user: import("../interfaces").User) => {
|
||||
const email = user.email ?? user.name;
|
||||
const isSelected = invitees.includes(email);
|
||||
// User unavailable if booked for another event at this time
|
||||
const unavailable = bookings.some((b: Booking) => {
|
||||
if (isEdit && b.id === editBooking?.id) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
b.invitees?.includes(email) &&
|
||||
new Date(start) < new Date(b.end_time) &&
|
||||
new Date(end) > new Date(b.start_time)
|
||||
);
|
||||
});
|
||||
const disableUnselected =
|
||||
!isSelected &&
|
||||
(unavailable || invitees.length >= roomCapacity);
|
||||
let label;
|
||||
if (user && user.name && user.email) {
|
||||
label = `${user.name} <${user.email}>`;
|
||||
} else if (user && user.name) {
|
||||
label = user.name;
|
||||
} else {
|
||||
label = email;
|
||||
}
|
||||
return (
|
||||
b.invitees?.includes(email) &&
|
||||
new Date(start) < new Date(b.end_time) &&
|
||||
new Date(end) > new Date(b.start_time)
|
||||
<MenuItem
|
||||
key={email}
|
||||
value={email}
|
||||
selected={isSelected}
|
||||
disabled={disableUnselected}
|
||||
sx={
|
||||
disableUnselected
|
||||
? {
|
||||
color: "#444",
|
||||
opacity: 1,
|
||||
fontWeight: "normal",
|
||||
bgcolor: "inherit",
|
||||
cursor: "not-allowed",
|
||||
textDecoration: "line-through",
|
||||
}
|
||||
: {
|
||||
fontWeight: isSelected ? "bold" : "normal",
|
||||
bgcolor: isSelected
|
||||
? "rgba(25, 118, 210, 0.08)"
|
||||
: "inherit",
|
||||
}
|
||||
}
|
||||
>
|
||||
{isSelected && (
|
||||
<span
|
||||
style={{
|
||||
color: "var(--color-primary)",
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
✔
|
||||
</span>
|
||||
)}
|
||||
{label}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
const disableUnselected =
|
||||
!isSelected &&
|
||||
(unavailable || invitees.length >= roomCapacity);
|
||||
return (
|
||||
<MenuItem
|
||||
key={email}
|
||||
value={email}
|
||||
selected={isSelected}
|
||||
disabled={disableUnselected}
|
||||
sx={
|
||||
disableUnselected
|
||||
? {
|
||||
color: "#444",
|
||||
opacity: 1,
|
||||
fontWeight: "normal",
|
||||
bgcolor: "inherit",
|
||||
cursor: "not-allowed",
|
||||
textDecoration: "line-through",
|
||||
}
|
||||
: {
|
||||
fontWeight: isSelected ? "bold" : "normal",
|
||||
bgcolor: isSelected
|
||||
? "rgba(25, 118, 210, 0.08)"
|
||||
: "inherit",
|
||||
}
|
||||
}
|
||||
>
|
||||
{isSelected && (
|
||||
<span
|
||||
style={{
|
||||
color: "var(--color-primary)",
|
||||
marginRight: 8,
|
||||
}}
|
||||
>
|
||||
✔
|
||||
</span>
|
||||
)}
|
||||
{user.name ? `${user.name} <${email}>` : email}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
{errors.invitees && (
|
||||
<Box color="error.main" fontSize={13} mb={1}>
|
||||
})}
|
||||
</Select>
|
||||
<Box color="error.main" fontSize={13}>
|
||||
{errors.invitees}
|
||||
</Box>
|
||||
)}
|
||||
</FormControl>
|
||||
</FormControl>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{!isViewMode && <Button onClick={onClose}>Cancel</Button>}
|
||||
{isEdit && !isViewMode && (
|
||||
{isViewMode ? (
|
||||
<Button
|
||||
color="error"
|
||||
onClick={async () => {
|
||||
if (editBooking) {
|
||||
try {
|
||||
await deleteBooking(editBooking.id);
|
||||
if (onBookingSuccess) {
|
||||
onBookingSuccess(); // No booking param: just close
|
||||
}
|
||||
onClose();
|
||||
} catch (err: unknown) {
|
||||
let errorMsg =
|
||||
"Failed to delete booking. Please try again.";
|
||||
// Type guard for error with response
|
||||
type ErrorWithResponse = {
|
||||
response?: {
|
||||
data?: {
|
||||
detail?: string;
|
||||
onClick={onClose}
|
||||
color="primary"
|
||||
variant="contained"
|
||||
autoFocus
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
{isEdit && (
|
||||
<Button
|
||||
color="error"
|
||||
onClick={async () => {
|
||||
if (editBooking) {
|
||||
try {
|
||||
await deleteBooking(editBooking.id);
|
||||
if (onBookingSuccess) {
|
||||
onBookingSuccess();
|
||||
}
|
||||
onClose();
|
||||
} catch (err: unknown) {
|
||||
let errorMsg =
|
||||
"Failed to delete booking. Please try again.";
|
||||
type ErrorWithResponse = {
|
||||
response?: {
|
||||
data?: {
|
||||
detail?: string;
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
};
|
||||
message?: string;
|
||||
};
|
||||
const errorObj = err as ErrorWithResponse;
|
||||
if (
|
||||
errorObj &&
|
||||
typeof errorObj === "object" &&
|
||||
errorObj.response &&
|
||||
typeof errorObj.response === "object" &&
|
||||
errorObj.response.data &&
|
||||
typeof errorObj.response.data === "object"
|
||||
) {
|
||||
const data = errorObj.response.data;
|
||||
if (typeof data.detail === "string") {
|
||||
errorMsg = data.detail;
|
||||
} else if (typeof data.message === "string") {
|
||||
errorMsg = data.message;
|
||||
const errorObj = err as ErrorWithResponse;
|
||||
if (
|
||||
errorObj &&
|
||||
typeof errorObj === "object" &&
|
||||
errorObj.response &&
|
||||
typeof errorObj.response === "object" &&
|
||||
errorObj.response.data &&
|
||||
typeof errorObj.response.data === "object"
|
||||
) {
|
||||
const data = errorObj.response.data;
|
||||
if (typeof data.detail === "string") {
|
||||
errorMsg = data.detail;
|
||||
} else if (typeof data.message === "string") {
|
||||
errorMsg = data.message;
|
||||
}
|
||||
} else if (
|
||||
errorObj &&
|
||||
typeof errorObj === "object" &&
|
||||
typeof errorObj.message === "string"
|
||||
) {
|
||||
errorMsg = errorObj.message ?? errorMsg;
|
||||
}
|
||||
logger.error(
|
||||
"BookingForm: Error deleting booking",
|
||||
err
|
||||
);
|
||||
setErrors({ submit: errorMsg });
|
||||
}
|
||||
} else if (
|
||||
errorObj &&
|
||||
typeof errorObj === "object" &&
|
||||
typeof errorObj.message === "string"
|
||||
) {
|
||||
errorMsg = errorObj.message ?? errorMsg;
|
||||
}
|
||||
logger.error("BookingForm: Error deleting booking", err);
|
||||
setErrors({ submit: errorMsg });
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
disabled={isFormError}
|
||||
color="primary"
|
||||
>
|
||||
{isEdit ? "Update" : "Book"}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button type="submit" variant="contained" disabled={isFormError}>
|
||||
{isViewMode ? "Close" : isEdit ? "Update" : "Book"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
</Dialog>
|
||||
|
||||
@@ -45,9 +45,25 @@ const RoomSelect: React.FC<RoomSelectProps> = ({
|
||||
// [RoomSelect] Unmounted log removed
|
||||
};
|
||||
}, [rooms.length]);
|
||||
// Compute background color for selected room
|
||||
let selectBgColor: string | undefined = undefined;
|
||||
if (rooms.length > 0 && selectedRoomId) {
|
||||
const selectedRoom = rooms.find(
|
||||
(room) => String(room.id) === String(selectedRoomId)
|
||||
);
|
||||
if (selectedRoom) {
|
||||
const roomIdx = ((Number(selectedRoom.id) - 1) % 20) + 1;
|
||||
selectBgColor = `var(--room-color-${roomIdx})`;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<FormControl size="small" sx={{ minWidth }}>
|
||||
<InputLabel id="room-select-label">{label}</InputLabel>
|
||||
<InputLabel
|
||||
id="room-select-label"
|
||||
sx={{ backgroundColor: "#fff", opacity: 1, px: 0.5 }}
|
||||
>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="room-select-label"
|
||||
value={selectedRoomId}
|
||||
@@ -56,6 +72,11 @@ const RoomSelect: React.FC<RoomSelectProps> = ({
|
||||
logger.info(`[RoomSelect] Room changed to ${e.target.value}`);
|
||||
onChange(e.target.value as string | number);
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: selectBgColor,
|
||||
color: "#fff",
|
||||
transition: "background-color 0.2s",
|
||||
}}
|
||||
>
|
||||
{rooms.map((room) => {
|
||||
// Compute room index directly from room.id
|
||||
|
||||
@@ -21,8 +21,8 @@ import type { Booking } from "../interfaces";
|
||||
|
||||
interface BookingContextType {
|
||||
bookings: Booking[];
|
||||
/* eslint-disable-next-line no-unused-vars */
|
||||
fetchMonth: (month: string) => Promise<void>;
|
||||
refreshBookings: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const BookingContext = createContext<BookingContextType | undefined>(
|
||||
@@ -53,14 +53,24 @@ export const BookingProvider = ({ children }: { children: ReactNode }) => {
|
||||
// Flat array for UI filtering
|
||||
const bookings = Object.values(bookingsByMonth).flat();
|
||||
|
||||
// Initial load: fetch all bookings for all rooms for today
|
||||
useEffect(() => {
|
||||
// On mount, fetch current month
|
||||
// Helper to get current month string
|
||||
const getCurrentMonthStr = () => {
|
||||
const now = new Date();
|
||||
const yyyy = now.getFullYear();
|
||||
const mm = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const monthStr = `${yyyy}-${mm}`;
|
||||
fetchMonth(monthStr);
|
||||
return `${yyyy}-${mm}`;
|
||||
};
|
||||
|
||||
// Refresh bookings for current month
|
||||
const refreshBookings = async () => {
|
||||
const monthStr = getCurrentMonthStr();
|
||||
await fetchMonth(monthStr);
|
||||
};
|
||||
|
||||
// Initial load: fetch current month
|
||||
useEffect(() => {
|
||||
refreshBookings();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// SSE subscription for live updates
|
||||
@@ -175,7 +185,7 @@ export const BookingProvider = ({ children }: { children: ReactNode }) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BookingContext.Provider value={{ bookings, fetchMonth }}>
|
||||
<BookingContext.Provider value={{ bookings, fetchMonth, refreshBookings }}>
|
||||
{children}
|
||||
</BookingContext.Provider>
|
||||
);
|
||||
|
||||
@@ -332,18 +332,14 @@ const BookingPage: React.FC = () => {
|
||||
setFormOpen(true);
|
||||
};
|
||||
|
||||
const refetchBookings = async () => {
|
||||
// Bookings are now managed by BookingContext; context handles refresh
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { refreshBookings } = useBookings();
|
||||
const handleFormClose = (refresh = false, booking?: Booking) => {
|
||||
// logger.info("[BookingPage] Booking form closed", { refresh, booking });
|
||||
setFormOpen(false);
|
||||
setFormSlot(null);
|
||||
setEditBooking(null);
|
||||
if (refresh) {
|
||||
refetchBookings();
|
||||
if (refresh && typeof refreshBookings === "function") {
|
||||
refreshBookings();
|
||||
}
|
||||
if (booking) {
|
||||
navigate("/confirmation", { state: { booking } });
|
||||
|
||||
@@ -44,12 +44,13 @@ import type { Booking } from "../interfaces";
|
||||
const ConfirmationPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
// Always call hooks at the top level
|
||||
const { bookings, refreshBookings } = useBookings();
|
||||
// booking is passed from BookingForm or BookingPage
|
||||
|
||||
/**
|
||||
* Booking state, updated via SSE for live status.
|
||||
*/
|
||||
const { bookings } = useBookings();
|
||||
const [booking, setBooking] = useState<Booking | null>(
|
||||
location.state?.booking || null
|
||||
);
|
||||
@@ -135,7 +136,7 @@ const ConfirmationPage: React.FC = () => {
|
||||
* Fetch all rooms for editing.
|
||||
*/
|
||||
const {
|
||||
// ...existing code...
|
||||
data: allRooms = [],
|
||||
isLoading: roomsLoading,
|
||||
error: roomsError,
|
||||
} = useQuery({
|
||||
@@ -158,11 +159,25 @@ const ConfirmationPage: React.FC = () => {
|
||||
const handleBack = () => navigate("/booking");
|
||||
const handleFormClose = (refresh = false) => {
|
||||
setEditing(false);
|
||||
if (refresh) {
|
||||
// Optionally refetch booking data here
|
||||
if (refresh && typeof refreshBookings === "function") {
|
||||
refreshBookings();
|
||||
}
|
||||
};
|
||||
|
||||
// Enrich booking with room details if missing
|
||||
let enrichedBooking = booking;
|
||||
if (
|
||||
enrichedBooking &&
|
||||
!enrichedBooking.room &&
|
||||
enrichedBooking.room_id &&
|
||||
Array.isArray(allRooms)
|
||||
) {
|
||||
const foundRoom = allRooms.find((r) => r.id === enrichedBooking.room_id);
|
||||
if (foundRoom) {
|
||||
enrichedBooking = { ...enrichedBooking, room: { name: foundRoom.name } };
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -186,16 +201,10 @@ const ConfirmationPage: React.FC = () => {
|
||||
}}
|
||||
onBookingSuccess={(resultBooking?: Booking) => {
|
||||
if (!resultBooking) {
|
||||
// Deleted
|
||||
// logger.info("[ConfirmationPage] Booking deleted", booking);
|
||||
setDeleted(true);
|
||||
setEditing(false);
|
||||
setBooking(null);
|
||||
} else {
|
||||
// logger.info(
|
||||
// "[ConfirmationPage] Booking success (created or edited)",
|
||||
// resultBooking
|
||||
// );
|
||||
setEditing(false);
|
||||
setBooking(resultBooking);
|
||||
}
|
||||
@@ -203,7 +212,7 @@ const ConfirmationPage: React.FC = () => {
|
||||
/>
|
||||
) : (
|
||||
<BookingConfirmation
|
||||
booking={booking}
|
||||
booking={enrichedBooking}
|
||||
deleted={deleted}
|
||||
onEdit={handleEdit}
|
||||
onBack={handleBack}
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.28.4.tgz#96fdf1af1b8859c8474ab39c295312bfb7c24b04"
|
||||
integrity sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==
|
||||
|
||||
"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.9", "@babel/core@^7.27.4", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
|
||||
"@babel/core@^7.1.0", "@babel/core@^7.11.1", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.16.0", "@babel/core@^7.18.9", "@babel/core@^7.23.0", "@babel/core@^7.23.2", "@babel/core@^7.23.9", "@babel/core@^7.24.4", "@babel/core@^7.27.4", "@babel/core@^7.7.2", "@babel/core@^7.8.0":
|
||||
version "7.28.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.28.4.tgz#12a550b8794452df4c8b084f95003bce1742d496"
|
||||
integrity sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==
|
||||
@@ -245,7 +245,7 @@
|
||||
"@babel/template" "^7.27.2"
|
||||
"@babel/types" "^7.28.4"
|
||||
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.0", "@babel/parser@^7.23.9", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4":
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.0", "@babel/parser@^7.23.9", "@babel/parser@^7.24.4", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3", "@babel/parser@^7.28.4":
|
||||
version "7.28.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.4.tgz#da25d4643532890932cc03f7705fe19637e03fa8"
|
||||
integrity sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==
|
||||
@@ -4084,16 +4084,16 @@
|
||||
integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==
|
||||
|
||||
"@types/express-serve-static-core@*", "@types/express-serve-static-core@^5.0.0":
|
||||
version "5.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz#2fa94879c9d46b11a5df4c74ac75befd6b283de6"
|
||||
integrity sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz#74f47555b3d804b54cb7030e6f9aa0c7485cfc5b"
|
||||
integrity sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express-serve-static-core@^4.17.21", "@types/express-serve-static-core@^4.17.33":
|
||||
"@types/express-serve-static-core@^4.17.21":
|
||||
version "4.19.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz#e01324c2a024ff367d92c66f48553ced0ab50267"
|
||||
integrity sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==
|
||||
@@ -4103,6 +4103,16 @@
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express-serve-static-core@^4.17.33":
|
||||
version "4.19.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.7.tgz#f1d306dcc03b1aafbfb6b4fe684cce8a31cffc10"
|
||||
integrity sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express@*":
|
||||
version "5.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-5.0.3.tgz#6c4bc6acddc2e2a587142e1d8be0bce20757e956"
|
||||
@@ -4238,11 +4248,11 @@
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node@*":
|
||||
version "24.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.6.2.tgz#59b99878b6fed17e698e7d09e51c729c5877736a"
|
||||
integrity sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==
|
||||
version "24.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-24.7.0.tgz#a34c9f0d3401db396782e440317dd5d8373c286f"
|
||||
integrity sha512-IbKooQVqUBrlzWTi79E8Fw78l8k1RNtlDDNWsFZs7XonuQSJ8oNYfEeclhprUldXISRMLzBpILuKgPlIxm+/Yw==
|
||||
dependencies:
|
||||
undici-types "~7.13.0"
|
||||
undici-types "~7.14.0"
|
||||
|
||||
"@types/node@16.7.13":
|
||||
version "16.7.13"
|
||||
@@ -4332,7 +4342,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044"
|
||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||
|
||||
"@types/react@*", "@types/react@>=16":
|
||||
"@types/react@*":
|
||||
version "19.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.0.tgz#8412946e7e1efb0de9bb59b3aa87676d96add385"
|
||||
integrity sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==
|
||||
@@ -4346,6 +4356,13 @@
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@>=16":
|
||||
version "19.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.2.tgz#ba123a75d4c2a51158697160a4ea2ff70aa6bf36"
|
||||
integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
|
||||
dependencies:
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/resolve@1.17.1":
|
||||
version "1.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"
|
||||
@@ -5576,9 +5593,9 @@ base64-js@^1.3.1:
|
||||
integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
|
||||
|
||||
baseline-browser-mapping@^2.8.9:
|
||||
version "2.8.12"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.12.tgz#7cb875f4c5b5ab4528109df277b2f0e1971ba27e"
|
||||
integrity sha512-vAPMQdnyKCBtkmQA6FMCBvU9qFIppS3nzyXnEM+Lo2IAhG4Mpjv9cCxMudhgV3YdNNJv6TNqXy97dfRVL2LmaQ==
|
||||
version "2.8.13"
|
||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.8.13.tgz#3d49a18ee27114765401f4985bdc27018603854e"
|
||||
integrity sha512-7s16KR8io8nIBWQyCYhmFhd+ebIzb9VKTzki+wOJXHTxTnV6+mFGH3+Jwn1zoKaY9/H9T/0BcKCZnzXljPnpSQ==
|
||||
|
||||
batch@0.6.1:
|
||||
version "0.6.1"
|
||||
@@ -5834,9 +5851,9 @@ caniuse-api@^3.0.0:
|
||||
lodash.uniq "^4.5.0"
|
||||
|
||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001746:
|
||||
version "1.0.30001747"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001747.tgz#2cfbbb7f1f046439ebaf34bba337ee3d3474c7e5"
|
||||
integrity sha512-mzFa2DGIhuc5490Nd/G31xN1pnBnYMadtkyTjefPI7wzypqgCEpeWu9bJr0OnDsyKrW75zA9ZAt7pbQFmwLsQg==
|
||||
version "1.0.30001748"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001748.tgz#628a5a9293014e58f8ba1216bb4966b04c58bee0"
|
||||
integrity sha512-5P5UgAr0+aBmNiplks08JLw+AW/XG/SurlgZLgB1dDLfAw7EfRGxIwzPHxdSCGY/BTKDqIVyJL87cCN6s0ZR0w==
|
||||
|
||||
case-sensitive-paths-webpack-plugin@^2.4.0:
|
||||
version "2.4.0"
|
||||
@@ -6906,9 +6923,9 @@ ejs@^3.1.6, ejs@^3.1.8:
|
||||
jake "^10.8.5"
|
||||
|
||||
electron-to-chromium@^1.5.227:
|
||||
version "1.5.230"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.230.tgz#06ddb4a6302a78b2a3e8dcf1dd2563bcfdd546c9"
|
||||
integrity sha512-A6A6Fd3+gMdaed9wX83CvHYJb4UuapPD5X5SLq72VZJzxHSY0/LUweGXRWmQlh2ln7KV7iw7jnwXK7dlPoOnHQ==
|
||||
version "1.5.232"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.232.tgz#3de180ee54c14c58d56a290f588eef3a934ebda6"
|
||||
integrity sha512-ENirSe7wf8WzyPCibqKUG1Cg43cPaxH4wRR7AJsX7MCABCHBIOFqvaYODSLKUuZdraxUTHRE/0A2Aq8BYKEHOg==
|
||||
|
||||
emittery@^0.10.2:
|
||||
version "0.10.2"
|
||||
@@ -7352,6 +7369,16 @@ eslint-plugin-react-hooks@^4.3.0:
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596"
|
||||
integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==
|
||||
|
||||
eslint-plugin-react-hooks@^6.1.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-6.1.1.tgz#c04c051e382444bb62cdca9cf4df68ffe0845b8a"
|
||||
integrity sha512-St9EKZzOAQF704nt2oJvAKZHjhrpg25ClQoaAlHmPZuajFldVLqRDW4VBNAS01NzeiQF0m0qhG1ZA807K6aVaQ==
|
||||
dependencies:
|
||||
"@babel/core" "^7.24.4"
|
||||
"@babel/parser" "^7.24.4"
|
||||
zod "^3.22.4 || ^4.0.0"
|
||||
zod-validation-error "^3.0.3 || ^4.0.0"
|
||||
|
||||
eslint-plugin-react@7.37.5, eslint-plugin-react@^7.27.1:
|
||||
version "7.37.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz#2975511472bdda1b272b34d779335c9b0e877065"
|
||||
@@ -10583,9 +10610,9 @@ map-or-similar@^1.5.0:
|
||||
integrity sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==
|
||||
|
||||
markdown-to-jsx@^7.1.8:
|
||||
version "7.7.13"
|
||||
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.13.tgz#8144d89e396cdf3264bec7c97a21bc47cfc0773b"
|
||||
integrity sha512-DiueEq2bttFcSxUs85GJcQVrOr0+VVsPfj9AEUPqmExJ3f8P/iQNvZHltV4tm1XVhu1kl0vWBZWT3l99izRMaA==
|
||||
version "7.7.15"
|
||||
resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.7.15.tgz#7dad08e2b2cf35460a1c157f5d3e7bc875bcb511"
|
||||
integrity sha512-U5dw5oRajrPTE2oJQWAbLK8RgbCDJ264AjW3fGABq+/rZjQ0E/WGVCLKAHvpKHQFUwoWoK8ZZWVPNLR/biYMhg==
|
||||
|
||||
math-intrinsics@^1.1.0:
|
||||
version "1.1.0"
|
||||
@@ -14256,10 +14283,10 @@ undici-types@~5.26.4:
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
undici-types@~7.13.0:
|
||||
version "7.13.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.13.0.tgz#a20ba7c0a2be0c97bd55c308069d29d167466bff"
|
||||
integrity sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==
|
||||
undici-types@~7.14.0:
|
||||
version "7.14.0"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.14.0.tgz#4c037b32ca4d7d62fae042174604341588bc0840"
|
||||
integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==
|
||||
|
||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||
version "2.0.1"
|
||||
@@ -15278,3 +15305,13 @@ yocto-queue@^1.0.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.2.1.tgz#36d7c4739f775b3cbc28e6136e21aa057adec418"
|
||||
integrity sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==
|
||||
|
||||
"zod-validation-error@^3.0.3 || ^4.0.0":
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
|
||||
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
|
||||
|
||||
"zod@^3.22.4 || ^4.0.0":
|
||||
version "4.1.12"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.12.tgz#64f1ea53d00eab91853195653b5af9eee68970f0"
|
||||
integrity sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==
|
||||
|
||||
Reference in New Issue
Block a user