diff --git a/README.md b/README.md index 60099c19..7219c072 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ A client side part of a web app for managing hotel bookings by organazing them i - **React Router**: single page app routing. ## Changes +### v1.2.0 +- Getting along with web service API. Slightly changed endpoints, data structures etc. +- Handling html entities which may occur in fields from back-end response. +- Splitting clients query by periods of time to improve responsiveness of the search. +- Considering police and istat data publications directly to their web services, without downloading intermediate files. +- Download police ricevuta directly from their web service. ### v1.1.2 - Allow only booking name in bookings fetch request. ### v1.1.1 diff --git a/package.json b/package.json index db7aa22e..df3bd459 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "booking-calendar", - "version": "1.1.0", + "version": "1.2.0", "description": "Calendar view of reservations", "main": "dist/bundle.js", "scripts": { diff --git a/src/api.ts b/src/api.ts index 37cae1af..fc47ee5a 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,4 +1,4 @@ -import { ChangesMap, TileColor, TileData } from "./redux/tilesSlice"; +import { TileColor } from "./redux/tilesSlice"; export type CityTaxData = { standard: number, @@ -6,33 +6,54 @@ export type CityTaxData = { over10Days: number }; -export type BookingData = { +export type Booking = { id: string, + status: "new" | "modified" | "cancelled", name: string, + lastModified: string, from: string, to: string, - rooms: TileData[] + color?: TileColor, + tiles: Tile[] }; -export type BookingShortData = { +export type Tile = { id: string, + from: string, + nights: number, + roomType: string, + persons: TPerson, + roomId?: number +}; + +export type BookingShort = { + id: string, + status: "new" | "modified" | "cancelle", name: string, + lastModified: string, from: string, to: string, - occupations: number, color: TileColor + occupations: number, }; -export type ClientData = { +export type Client = { id: string, bookingId: string, name: string, surname: string, dateOfBirth: string, placeOfBirth?: string, + provinceOfBirth?: string, stateOfBirth?: string }; +export type ClientWithBooking = { + bookingName: string, + bookingFrom: string, + bookingTo: string +} & Client; + export type Room = { id: number, floorId: number, @@ -52,6 +73,22 @@ export type RoomType = { maxOccupancy: number, }; +export type ColorAssignments = { + [key: string]: TileColor +}; + +export type RoomAssignments = { + [key: string]: number | null +} + +export type AckBookingsRequest = { + bookings: { + bookingId: string, + lastModified: string + }[], + sessionId: string +} + export function fetchFloorsAsync(): Promise<{ data: Floor[] }> { return fetchJsonDataAsync("/api/v1/floors"); } @@ -84,53 +121,52 @@ export function fetchRoomTypesAsync(): Promise<{ data: RoomType[] }> { return fetchJsonDataAsync("/api/v1/room-types"); } -export function fetchTilesAsync(from: string, to: string, sessionId?: string): Promise<{ data: { tiles: TileData[], sessionId: string } }> { - return fetchJsonDataAsync<{ tiles: TileData[], sessionId: string }>(`/api/v1/tiles?from=${from}&to=${to}${sessionId ? `&sessionId=${sessionId}` : ""}`); +export function fetchBookingsBySessionAsync(from: string, to: string, sessionId?: string): Promise<{ data: { bookings: Booking[], sessionId: string } }> { + return fetchJsonDataAsync<{ bookings: Booking[], sessionId: string }>(`/api/v1/bookings-by-session?from=${from}&to=${to}${sessionId ? `&sessionId=${sessionId}` : ""}`); } -export function postChangesAsync(changes: ChangesMap): Promise { - return postDataAsync("/api/v1/changes", changes); +export function ackBookingsAsync(request: AckBookingsRequest): Promise { + return postDataAsync("/api/v1/ack-bookings", request); } -export async function fetchBookingById(bookingId: string): Promise<{ data: BookingData }> { - return fetchJsonDataAsync(`/api/v1/booking?id=${bookingId}`); +export function postColorAssignments(assignments: ColorAssignments): Promise { + return postDataAsync("/api/v1/color-assignments", assignments); } -export async function fetchBookingShortById(bookingId: string): Promise<{ data: BookingShortData}> { - return fetchJsonDataAsync(`/api/v1/booking-short?id=${bookingId}`); +export function postRoomAssignmentsAsync(assignments: RoomAssignments): Promise { + return postDataAsync("/api/v1/room-assignments", assignments); } -export async function fetchClientsByTile(tileId: string): Promise<{ data: ClientData[] }> { - return fetchJsonDataAsync(`/api/v1/clients?tileId=${tileId}`); +export async function fetchBookingById(bookingId: string, from: string): Promise<{ data: Booking }> { + return fetchJsonDataAsync>(`/api/v1/booking?id=${bookingId}&from=${from}`); } -export async function fetchPoliceDataAsync(date: string): Promise<{ data: Blob }> { - return fetchBlobDataAsync(`/api/v1/stats/police?date=${date}`); +export async function fetchClientsByTile(bookingId: string, tileId: string): Promise<{ data: Client[] }> { + return fetchJsonDataAsync(`/api/v1/clients-by-tile?bookingId=${bookingId}&tileId=${tileId}`); } -export async function fetchIstatDataAsync(date: string): Promise<{ data: Blob }> { - return fetchBlobDataAsync(`/api/v1/stats/istat?date=${date}`); +export async function postPoliceExportRequestAsync(date: string): Promise { + return postDataAsync("/api/v1/police", { date }); } -export async function fetchCityTaxAsync(from: string, to: string): Promise<{ data: CityTaxData }> { - return fetchJsonDataAsync(`/api/v1/stats/city-tax?from=${from}&to=${to}`); +export async function postIstatExportRequestAsync(date: string): Promise { + return postDataAsync("/api/v1/istat", { date }); } -export async function fetchBookings(name: string, from: string, to: string): Promise<{ data: BookingShortData[] }> { - return fetchJsonDataAsync(`/api/v1/bookings?name=${name}&from=${from}&to=${to}`); +export async function fetchPoliceRicevutaAsync(date: string): Promise<{ data: Blob }> { + return fetchBlobDataAsync(`/api/v1/police/ricevuta?date=${date}`); } -export async function fetchClients(query: string): Promise<{ data: ClientData[] }> { - return fetchJsonDataAsync(`/api/v1/clients?query=${query}`); +export async function fetchCityTaxAsync(from: string, to: string): Promise<{ data: CityTaxData }> { + return fetchJsonDataAsync(`/api/v1/city-tax?from=${from}&to=${to}`); } -async function fetchBlobDataAsync(query: string): Promise<{ data: Blob }> { - const response = await fetch(query); - if (!response.ok) { - throw new Error("Resopnse error"); - } - const data = await response.blob(); - return { data }; +export async function fetchBookings(name: string, from: string, to: string): Promise<{ data: BookingShort[] }> { + return fetchJsonDataAsync(`/api/v1/bookings-by-name?name=${name}&from=${from}&to=${to}`); +} + +export async function fetchClientsByQuery(query: string, from: string, to: string): Promise<{ data: ClientWithBooking[] }> { + return fetchJsonDataAsync(`/api/v1/clients-by-query?query=${query}&from=${from}&to=${to}`); } async function fetchJsonDataAsync(query: string): Promise<{ data: T }> { @@ -145,6 +181,15 @@ async function fetchJsonDataAsync(query: string): Promise<{ data: T }> { return { data }; } +async function fetchBlobDataAsync(query: string): Promise<{ data: Blob }> { + const response = await fetch(query); + if (!response.ok) { + throw new Error("Resopnse error"); + } + const data = await response.blob(); + return { data }; +} + async function postDataAsync(url: string, data: TData): Promise { const response = await fetch(url, { method: "POST", diff --git a/src/components/AppRoutes.tsx b/src/components/AppRoutes.tsx index 0e328ecd..bfa58939 100644 --- a/src/components/AppRoutes.tsx +++ b/src/components/AppRoutes.tsx @@ -13,7 +13,7 @@ export default function AppRoutes(): JSX.Element { } /> }> - } /> + } /> } /> } /> diff --git a/src/components/BookingDetails/StayDetails.tsx b/src/components/BookingDetails/StayDetails.tsx new file mode 100644 index 00000000..4704df2d --- /dev/null +++ b/src/components/BookingDetails/StayDetails.tsx @@ -0,0 +1,42 @@ +import React, { memo, useMemo } from "react"; + +import { Booking, Client, Tile } from "../../api"; +import { TileColor, TileData } from "../../redux/tilesSlice"; + +import ExpandableTile from "../ExpandableTile"; +import { TileContext } from "../Tile/context"; +import { BookingDetailsContext } from "./context"; + +export type StayDetailsProps = { + booking: Booking, + tile: Tile, + isFirst: boolean +} + +export default memo(function StayDetails(props: StayDetailsProps): JSX.Element { + const { booking, tile, isFirst } = props; + + + const el = useMemo(() => { + const tileData: TileData = { + id: tile.id, + bookingId: booking.id, + name: booking.name, + from: tile.from, + nights: tile.nights, + roomType: tile.roomType, + persons: tile.persons.length, + color: booking.color ?? `booking${Math.floor(Math.random() * 7) + 1}` as TileColor, + roomId: tile.roomId + }; + + return ( + + + + + + ); + }, [tile, booking, isFirst]); + return el; +}); diff --git a/src/components/BookingDetails/context.ts b/src/components/BookingDetails/context.ts new file mode 100644 index 00000000..da7584a2 --- /dev/null +++ b/src/components/BookingDetails/context.ts @@ -0,0 +1,9 @@ +import { createContext } from "react"; + +import { Client } from "../../api"; + +export const BookingDetailsContext = createContext<{ + clients: Client[] +}>({ + clients: [] +}); diff --git a/src/components/BookingDetails/index.tsx b/src/components/BookingDetails/index.tsx index fd8b6a62..a0b86e36 100644 --- a/src/components/BookingDetails/index.tsx +++ b/src/components/BookingDetails/index.tsx @@ -5,19 +5,21 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import { BookingData, fetchBookingById } from "../../api"; +import { Booking, Client, fetchBookingById } from "../../api"; import { useAppDispatch } from "../../redux/hooks"; import { show as showMessage } from "../../redux/snackbarMessageSlice"; import { TileContext } from "../Tile/context"; import ExpandableTile from "../ExpandableTile"; import M3Skeleton from "../m3/M3Skeleton"; +import StayDetails from "./StayDetails"; +import { evaluateEntitiesInString } from "../../utils"; export default function BookingDetails(): JSX.Element { const theme = useTheme(); - const { bookingId } = useParams(); + const { from, bookingId } = useParams(); const dispatch = useAppDispatch(); - const [booking, setBooking] = useState(undefined); + const [booking, setBooking] = useState | undefined>(undefined); const skeletonRooms = [0, 1]; const periodStr = booking ? @@ -28,9 +30,9 @@ export default function BookingDetails(): JSX.Element { let isSubscribed = true; async function fetchData() { - if (bookingId) { + if (from && bookingId) { try { - const response = await fetchBookingById(bookingId); + const response = await fetchBookingById(bookingId, from); if (isSubscribed) { setBooking(response.data); @@ -47,7 +49,7 @@ export default function BookingDetails(): JSX.Element { isSubscribed = false; setBooking(undefined); }; - }, [dispatch, bookingId]); + }, [dispatch, from, bookingId]); return ( - {booking ? booking.name : } + {booking ? evaluateEntitiesInString(booking.name) : } {periodStr ? periodStr : } @@ -86,15 +88,13 @@ export default function BookingDetails(): JSX.Element { boxSizing: "border-box", pb: "1rem" }}> - {booking ? booking.rooms.map((room, index) => ( - - - - )) : skeletonRooms.map((room) => ( - - - - ))} + {booking + ? booking.tiles.map((tile, index) => ) + : skeletonRooms.map((room) => ( + + + + ))} ); diff --git a/src/components/Bookings/Booking/Button.tsx b/src/components/Bookings/Booking/Button.tsx index fde407f3..f9e1ff9d 100644 --- a/src/components/Bookings/Booking/Button.tsx +++ b/src/components/Bookings/Booking/Button.tsx @@ -12,7 +12,7 @@ export default function Button({ children }: ButtonProps): JSX.Element { const booking = useContext(BookingContext); return ( - + {({ isActive }) => ( - {booking.name} + {evaluateEntitiesInString(booking.name)} {`${formattedFrom} - ${formattedTo}`} diff --git a/src/components/Bookings/Booking/context.ts b/src/components/Bookings/Booking/context.ts index 04d1283e..89160684 100644 --- a/src/components/Bookings/Booking/context.ts +++ b/src/components/Bookings/Booking/context.ts @@ -1,10 +1,12 @@ import { createContext } from "react"; -import { BookingShortData } from "../../../api"; +import { BookingShort } from "../../../api"; -const BookingContext = createContext({ +const BookingContext = createContext({ id: "", + status: "new", name: "", + lastModified: "", from: "", to: "", occupations: 0, diff --git a/src/components/Bookings/Booking/index.tsx b/src/components/Bookings/Booking/index.tsx index 8c9287dd..8a1d8d21 100644 --- a/src/components/Bookings/Booking/index.tsx +++ b/src/components/Bookings/Booking/index.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { BookingShortData } from "../../../api"; +import { BookingShort } from "../../../api"; import BookingContext from "./context"; import Button from "./Button"; @@ -9,7 +9,7 @@ import ShortInfo from "./ShortInfo"; import RoomsCount from "./RoomsCount"; type BookingProps = { - booking: BookingShortData + booking: BookingShort }; export default function Booking({ booking }: BookingProps): JSX.Element { diff --git a/src/components/Bookings/List.tsx b/src/components/Bookings/List.tsx index 7a78cdb5..718f4b42 100644 --- a/src/components/Bookings/List.tsx +++ b/src/components/Bookings/List.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from "react"; import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; -import { BookingShortData, fetchBookings } from "../../api"; +import { BookingShort, fetchBookings } from "../../api"; import { useAppDispatch } from "../../redux/hooks"; import { show as showMessage } from "../../redux/snackbarMessageSlice"; @@ -18,7 +18,7 @@ type ListProps = { export default function List({ name, from, to, isValid }: ListProps): JSX.Element { const dispatch = useAppDispatch(); - const [bookings, setBookings] = useState([]); + const [bookings, setBookings] = useState([]); useEffect(() => { let subscribed = true; diff --git a/src/components/Clients/ClientCard/Details.tsx b/src/components/Clients/ClientCard/Details.tsx index ca7fbfbd..69c9bb8f 100644 --- a/src/components/Clients/ClientCard/Details.tsx +++ b/src/components/Clients/ClientCard/Details.tsx @@ -1,43 +1,25 @@ -import React, { useEffect, useState } from "react"; +import React from "react"; import { Link } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Box from "@mui/material/Box"; -import { BookingShortData, fetchBookingShortById } from "../../../api"; +import { ClientWithBooking } from "../../../api"; import { setBookingsFormFrom, setBookingsFormName, setBookingsFormTo } from "../../../redux/bookingsFormSlice"; import { useAppDispatch } from "../../../redux/hooks"; -import { show as showMessage } from "../../../redux/snackbarMessageSlice"; import M3TextButton from "../../m3/M3TextButton"; -import M3Skeleton from "../../m3/M3Skeleton"; +import { evaluateEntitiesInString } from "../../../utils"; type DetailsProps = { - bookingId: string + client: ClientWithBooking }; -export default function Details({ bookingId }: DetailsProps): JSX.Element { +export default function Details({ client }: DetailsProps): JSX.Element { const dispatch = useAppDispatch(); const theme = useTheme(); - const [booking, setBooking] = useState(undefined); - const periodStr = booking ? - `${(new Date(booking.from)).toLocaleDateString()} - ${(new Date(booking.to)).toLocaleDateString()}` : - undefined; - - useEffect(() => { - async function fetchData(): Promise { - try { - const response = await fetchBookingShortById(bookingId); - setBooking(response.data); - } catch(error) { - dispatch(showMessage({ type: "error" })); - } - } - - fetchData(); - }, [bookingId, dispatch]); + const periodStr = `${(new Date(client.bookingFrom)).toLocaleDateString()} - ${(new Date(client.bookingTo)).toLocaleDateString()}`; return ( Prenotazione - {booking ? booking.name : } - {periodStr ? periodStr : } + {evaluateEntitiesInString(client.bookingName)} + {periodStr} - {booking ? ( - - { - dispatch(setBookingsFormFrom(booking.from)); - dispatch(setBookingsFormTo(booking.to)); - dispatch(setBookingsFormName(booking.name)); - }}> - Mostra prenotazione - - - ) : } + + { + dispatch(setBookingsFormFrom(client.bookingFrom)); + dispatch(setBookingsFormTo(client.bookingTo)); + dispatch(setBookingsFormName(client.bookingName)); + }}> + Mostra prenotazione + + ); diff --git a/src/components/Clients/ClientCard/index.tsx b/src/components/Clients/ClientCard/index.tsx index 24008b64..c6952ed4 100644 --- a/src/components/Clients/ClientCard/index.tsx +++ b/src/components/Clients/ClientCard/index.tsx @@ -6,14 +6,15 @@ import Typography from "@mui/material/Typography"; import ExpandMoreOutlined from "@mui/icons-material/ExpandMoreOutlined"; import ExpandLessOutlined from "@mui/icons-material/ExpandLessOutlined"; -import { ClientData } from "../../../api"; +import { ClientWithBooking } from "../../../api"; import M3IconButton from "../../m3/M3IconButton"; import Details from "./Details"; import M3Skeleton from "../../m3/M3Skeleton"; +import { evaluateEntitiesInString } from "../../../utils"; type ClientCardProps = { - client?: ClientData + client?: ClientWithBooking }; export default function ClientCard({ client }: ClientCardProps): JSX.Element { @@ -30,11 +31,11 @@ export default function ClientCard({ client }: ClientCardProps): JSX.Element { }}> - {client ? `${client.name} ${client.surname}` : } + {client ? `${evaluateEntitiesInString(client.name)} ${evaluateEntitiesInString(client.surname)}` : } {client ? (new Date(client.dateOfBirth).toLocaleDateString()) : } - {client ? `${client.placeOfBirth ? `${client.placeOfBirth} - ` : ""}${client.stateOfBirth}` : } + {client ? evaluateEntitiesInString(`${client.placeOfBirth ? `${client.placeOfBirth}${client.provinceOfBirth ? ` (${client.provinceOfBirth})` : ""} - ` : ""}${client.stateOfBirth}`) : } {client ? ( setOpen(!open)}> @@ -44,7 +45,7 @@ export default function ClientCard({ client }: ClientCardProps): JSX.Element { {client ? ( -
+
) : null} diff --git a/src/components/Clients/index.tsx b/src/components/Clients/index.tsx index 48da4e43..fc3ed6b9 100644 --- a/src/components/Clients/index.tsx +++ b/src/components/Clients/index.tsx @@ -7,7 +7,8 @@ import InputAdornment from "@mui/material/InputAdornment"; import SearchOutlined from "@mui/icons-material/SearchOutlined"; import Cancel from "@mui/icons-material/Cancel"; -import { ClientData, fetchClients } from "../../api"; +import * as Utils from "../../utils"; +import { ClientWithBooking, fetchClientsByQuery } from "../../api"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { show as showMessage } from "../../redux/snackbarMessageSlice"; @@ -18,7 +19,7 @@ import ClientCard from "./ClientCard"; export default function Clients(): JSX.Element { const dispatch = useAppDispatch(); const [query, setQuery] = useState(""); - const [clients, setClients] = useState([]); + const [clients, setClients] = useState([]); const [isLoading, setIsLoading] = useState(false); const drawerOpened = useAppSelector((state) => state.drawer.open); const skeletonClients = [0, 1]; @@ -32,24 +33,40 @@ export default function Clients(): JSX.Element { useEffect(() => { let isSubscribed = true; - async function fetchData() { + async function fetchData(from: string, to: string) { try { - const response = await fetchClients(query); + const response = await fetchClientsByQuery(query, from, to); if (isSubscribed) { - setClients(response.data); + setClients((prevClients) => [...prevClients, ...response.data]); } } catch(error) { dispatch(showMessage({ type: "error" })); - } finally { - setIsLoading(false); } } - if (query !== "") { - setIsLoading(true); - fetchData(); + async function executeFetchSequence(): Promise { + if (query !== "") { + setIsLoading(true); + setClients([]); + const dateCounter = new Date("2021-01-01"); + const now = Utils.dateToString(new Date()); + while (Utils.daysBetweenDates(Utils.dateToString(dateCounter), now) > 0) { + if (!isSubscribed) { + break; + } + const from = Utils.dateToString(dateCounter); + dateCounter.setMonth(dateCounter.getMonth() + 6); + const to = Utils.dateToString(dateCounter); + await fetchData(from, to); + } + if (isSubscribed) { + setIsLoading(false); + } + } } + executeFetchSequence(); + return () => { isSubscribed = false; }; }, [dispatch, query]); @@ -89,9 +106,8 @@ export default function Clients(): JSX.Element { display: "grid", gridTemplateColumns: `repeat(${drawerOpened ? 3 : 4}, 1fr)` }}> - {(isLoading && clients.length === 0) ? - skeletonClients.map((client) => ) : - clients.map((client) => )} + {clients.map((client) => )} + {isLoading ? skeletonClients.map((client) => ) : null} diff --git a/src/components/ExpandableTile/Details/Client/BirthInfo.tsx b/src/components/ExpandableTile/Details/Client/BirthInfo.tsx index 712e403a..527990bd 100644 --- a/src/components/ExpandableTile/Details/Client/BirthInfo.tsx +++ b/src/components/ExpandableTile/Details/Client/BirthInfo.tsx @@ -2,6 +2,7 @@ import React, { useContext } from "react"; import Typography from "@mui/material/Typography"; import ClientContext from "./context"; +import { evaluateEntitiesInString } from "../../../../utils"; export default function BirthInfo(): JSX.Element { const client = useContext(ClientContext); @@ -10,7 +11,7 @@ export default function BirthInfo(): JSX.Element { { `${(new Date(client.dateOfBirth)).toLocaleDateString()} - - ${client.placeOfBirth ? `${client.placeOfBirth} - ` : ""} + ${client.placeOfBirth ? evaluateEntitiesInString(`${client.placeOfBirth}${client.provinceOfBirth ? ` (${client.provinceOfBirth})` : ""} - `) : ""} ${client.stateOfBirth}` } diff --git a/src/components/ExpandableTile/Details/Client/FullName.tsx b/src/components/ExpandableTile/Details/Client/FullName.tsx index 40d1f370..7c37356d 100644 --- a/src/components/ExpandableTile/Details/Client/FullName.tsx +++ b/src/components/ExpandableTile/Details/Client/FullName.tsx @@ -2,11 +2,12 @@ import React, { useContext } from "react"; import Typography from "@mui/material/Typography"; import ClientContext from "./context"; +import { evaluateEntitiesInString } from "../../../../utils"; export default function FullName(): JSX.Element { const client = useContext(ClientContext); return ( - {`${client.name} ${client.surname}`} + {`${evaluateEntitiesInString(client.name)} ${evaluateEntitiesInString(client.surname)}`} ); } diff --git a/src/components/ExpandableTile/Details/Client/context.ts b/src/components/ExpandableTile/Details/Client/context.ts index afb11ed4..1db3c9ba 100644 --- a/src/components/ExpandableTile/Details/Client/context.ts +++ b/src/components/ExpandableTile/Details/Client/context.ts @@ -1,8 +1,8 @@ import { createContext } from "react"; -import { ClientData } from "../../../../api"; +import { Client } from "../../../../api"; -const ClientContext = createContext({ +const ClientContext = createContext({ id: "", bookingId: "", name: "", diff --git a/src/components/ExpandableTile/Details/Client/index.tsx b/src/components/ExpandableTile/Details/Client/index.tsx index 4dab6b34..5535abdb 100644 --- a/src/components/ExpandableTile/Details/Client/index.tsx +++ b/src/components/ExpandableTile/Details/Client/index.tsx @@ -1,14 +1,14 @@ import React from "react"; import Stack from "@mui/material/Stack"; -import { ClientData } from "../../../../api"; +import { Client } from "../../../../api"; import FullName from "./FullName"; import BirthInfo from "./BirthInfo"; import ClientContext from "./context"; type ClientProps = { - client: ClientData + client: Client }; export default function Client({ client }: ClientProps): JSX.Element { diff --git a/src/components/ExpandableTile/Details/Clients.tsx b/src/components/ExpandableTile/Details/Clients.tsx index 1d3bf5af..3111e968 100644 --- a/src/components/ExpandableTile/Details/Clients.tsx +++ b/src/components/ExpandableTile/Details/Clients.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect } from "react"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import { ClientData, fetchClientsByTile } from "../../../api"; +import { Client as ClientData, fetchClientsByTile } from "../../../api"; import { show as showMessage } from "../../../redux/snackbarMessageSlice"; import { useAppDispatch } from "../../../redux/hooks"; import { TileContext } from "../../Tile/context"; @@ -23,17 +23,18 @@ export default function Clients({ clients, setClients }: ClientsProps): JSX.Elem async function fetchData() { try { if (data) { - const response = await fetchClientsByTile(data.id); + const response = await fetchClientsByTile(data.bookingId, data.id); setClients(response.data); } } catch(error) { dispatch(showMessage({ type: "error" })); } } - fetchData(); - return () => setClients([]); - }, [data, dispatch, setClients]); + if (clients.length === 0) { + fetchData(); + } + }, [data, dispatch, setClients, clients.length]); return ( <> diff --git a/src/components/ExpandableTile/Details/ShowBookingButton.tsx b/src/components/ExpandableTile/Details/ShowBookingButton.tsx index f5258d12..27b92520 100644 --- a/src/components/ExpandableTile/Details/ShowBookingButton.tsx +++ b/src/components/ExpandableTile/Details/ShowBookingButton.tsx @@ -27,7 +27,7 @@ export default function ShowBookingButton({ show }: ShowBookingButtonProps): JSX return ( {show && data ? ( - + { dispatch(setBookingsFormFrom(data.from)); dispatch(setBookingsFormTo(Utils.getDateShift(data.from, data.nights))); diff --git a/src/components/ExpandableTile/Details/index.tsx b/src/components/ExpandableTile/Details/index.tsx index 34bbfc74..1c94d65a 100644 --- a/src/components/ExpandableTile/Details/index.tsx +++ b/src/components/ExpandableTile/Details/index.tsx @@ -1,8 +1,9 @@ import React, { useContext, useState } from "react"; import Stack from "@mui/material/Stack"; -import { ClientData } from "../../../api"; +import { Client } from "../../../api"; import { TileContext } from "../../Tile/context"; +import { BookingDetailsContext } from "../../BookingDetails/context"; import ExpandableTileContext from "../context"; import DetailsCollapse from "./DetailsCollapse"; @@ -17,8 +18,9 @@ type DetailsProps = { export default function Details({ open }: DetailsProps): JSX.Element { const { data } = useContext(TileContext); + const { clients: loadedClients } = useContext(BookingDetailsContext); const { variant } = useContext(ExpandableTileContext); - const [clients, setClients] = useState([]); + const [clients, setClients] = useState(loadedClients); return ( diff --git a/src/components/ExpandableTile/Header/HeadlineRow.tsx b/src/components/ExpandableTile/Header/HeadlineRow.tsx index 1712e5a3..7633229a 100644 --- a/src/components/ExpandableTile/Header/HeadlineRow.tsx +++ b/src/components/ExpandableTile/Header/HeadlineRow.tsx @@ -7,6 +7,7 @@ import ExpandableTileContext from "../context"; import MoreButton from "./MoreButton"; import M3Skeleton from "../../m3/M3Skeleton"; +import { evaluateEntitiesInString } from "../../../utils"; export default function HeadlineRow(): JSX.Element { const { data } = useContext(TileContext); @@ -14,7 +15,7 @@ export default function HeadlineRow(): JSX.Element { return ( - {data ? data.name : } + {data ? evaluateEntitiesInString(data.name) : } {variant === "popup" ? : null} ); diff --git a/src/components/ExpandableTile/Header/RoomType.tsx b/src/components/ExpandableTile/Header/RoomType.tsx index 75628fd8..58d3b32a 100644 --- a/src/components/ExpandableTile/Header/RoomType.tsx +++ b/src/components/ExpandableTile/Header/RoomType.tsx @@ -1,15 +1,16 @@ import React, { useContext } from "react"; import Typography from "@mui/material/Typography"; +import { evaluateEntitiesInString } from "../../../utils"; import { TileContext } from "../../Tile/context"; import M3Skeleton from "../../m3/M3Skeleton"; export default function RoomType(): JSX.Element { const { data } = useContext(TileContext); - const formattedRoomType = data ? `${data.entity[0].toLocaleUpperCase()}${data.entity.slice(1)}` : undefined; + const formattedRoomType = data ? `${data.roomType[0].toLocaleUpperCase()}${data.roomType.slice(1)}` : undefined; return ( - {formattedRoomType ? formattedRoomType : } + {formattedRoomType ? evaluateEntitiesInString(formattedRoomType) : } ); } diff --git a/src/components/ExpandableTile/index.tsx b/src/components/ExpandableTile/index.tsx index 7a9a3004..e867facb 100644 --- a/src/components/ExpandableTile/index.tsx +++ b/src/components/ExpandableTile/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState } from "react"; +import React, { memo, useRef, useState } from "react"; import ExpandableTileContext from "./context"; @@ -15,7 +15,7 @@ type ExpandableProps = { isFirst?: boolean }; -export default function ExpandableTile({ variant, anchorEl, onClose, isFirst }: ExpandableProps): JSX.Element { +export default memo(function ExpandableTile({ variant, anchorEl, onClose, isFirst }: ExpandableProps): JSX.Element { const [openDetails, setOpenDetails] = useState(variant === "in-content"); const headerRef = useRef(null); @@ -39,4 +39,4 @@ export default function ExpandableTile({ variant, anchorEl, onClose, isFirst }: ); -} +}); diff --git a/src/components/SaveAndResetWidget/ActionButtons.tsx b/src/components/SaveAndResetWidget/ActionButtons.tsx index 7c7c97c0..e57c951d 100644 --- a/src/components/SaveAndResetWidget/ActionButtons.tsx +++ b/src/components/SaveAndResetWidget/ActionButtons.tsx @@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack"; import RestoreIcon from "@mui/icons-material/Restore"; import SaveIcon from "@mui/icons-material/Save"; -import * as Api from "../../api"; +import { ColorAssignments, postRoomAssignmentsAsync, postColorAssignments, RoomAssignments } from "../../api"; import { useAppDispatch, useAppSelector } from "../../redux/hooks"; import { SaveAndResetWidgetContext } from "."; import * as TilesSlice from "../../redux/tilesSlice"; @@ -17,15 +17,37 @@ import M3TextButton from "../m3/M3TextButton"; export default function ActionButtons(): JSX.Element { const { status, setStatus } = useContext(SaveAndResetWidgetContext); const dispatch = useAppDispatch(); - const hasChanges = useAppSelector((state) => Object.keys(state.tiles.changesMap).length > 0); - const changes = useAppSelector((state) => state.tiles.changesMap); + const hasRoomChanges = useAppSelector((state) => Object.keys(state.tiles.roomChanges).length > 0); + const hasColorChanges = useAppSelector((state) => Object.keys(state.tiles.colorChanges).length > 0); + const hasChanges = hasRoomChanges || hasColorChanges; + const roomChanges = useAppSelector((state) => state.tiles.roomChanges); + const colorChanges = useAppSelector((state) => state.tiles.colorChanges); const open = status === "idle" && hasChanges; function save() { async function launchSaveAsync(): Promise { try { - await Api.postChangesAsync(changes); + if (hasRoomChanges) { + const roomAssignments: RoomAssignments = { }; + const roomChangesKeys = Object.keys(roomChanges); + for (const changeKey of roomChangesKeys) { + const change = roomChanges[changeKey]; + roomAssignments[changeKey] = change.newRoom !== undefined ? change.newRoom : null; + } + await postRoomAssignmentsAsync(roomAssignments); + } + + if (hasColorChanges) { + const colorAssignments: ColorAssignments = { }; + const colorChangesKeys = Object.keys(colorChanges); + for (const changeKey of colorChangesKeys) { + const change = colorChanges[changeKey]; + colorAssignments[changeKey] = change.newColor; + } + await postColorAssignments(colorAssignments); + } + dispatch(TilesSlice.saveChanges()); setStatus("fulfilled"); } catch (error) { diff --git a/src/components/Settings/Floor.tsx b/src/components/Settings/Floor.tsx index ca4c8fda..581601c2 100644 --- a/src/components/Settings/Floor.tsx +++ b/src/components/Settings/Floor.tsx @@ -28,6 +28,7 @@ import { SurfaceTint } from "../m3/Tints"; import Room from "./Room"; import M3Dialog from "../m3/M3Dialog"; import M3TextButton from "../m3/M3TextButton"; +import { evaluateEntitiesInString } from "../../utils"; type FloorProps = { id: number, @@ -40,7 +41,7 @@ export default function Floor({ id, floor }: FloorProps): JSX.Element { const [state, setState] = useState<"idle" | "edit" | "remove" | "createRoom">("idle"); const [isLoading, setIsLoading] = useState(false); const [name, setName] = useState(floor.name); - const tilesHaveChanges = useAppSelector((state) => Object.keys(state.tiles.changesMap).length > 0); + const tilesHaveChanges = useAppSelector((state) => Object.keys(state.tiles.roomChanges).length > 0); const roomTypes = useAppSelector((state) => Object.keys(state.roomTypes.data)); const [roomNumber, setRoomNumber] = useState(""); const [isRoomNumberValid, setIsRoomNumberValid] = useState(true); @@ -148,7 +149,7 @@ export default function Floor({ id, floor }: FloorProps): JSX.Element { {state !== "edit" ? ( - {floorName} + {evaluateEntitiesInString(floorName)} {state !== "createRoom" ? ( @@ -209,7 +210,7 @@ export default function Floor({ id, floor }: FloorProps): JSX.Element { > {roomTypes.map((roomTypeId) => ( - {`${roomTypeId[0].toLocaleUpperCase()}${roomTypeId.slice(1)}`} + {evaluateEntitiesInString(`${roomTypeId[0].toLocaleUpperCase()}${roomTypeId.slice(1)}`)} ))} diff --git a/src/components/Settings/Room.tsx b/src/components/Settings/Room.tsx index a273b588..bb28fdf9 100644 --- a/src/components/Settings/Room.tsx +++ b/src/components/Settings/Room.tsx @@ -24,6 +24,7 @@ import M3IconButton from "../m3/M3IconButton"; import M3FilledButton from "../m3/M3FilledButton"; import M3Dialog from "../m3/M3Dialog"; import M3TextButton from "../m3/M3TextButton"; +import { evaluateEntitiesInString } from "../../utils"; type RoomProps = { id: number, @@ -186,7 +187,7 @@ export default function Room({ id, floorId }: RoomProps): JSX.Element { > {roomTypes.map((roomTypeId) => ( - {`${roomTypeId[0].toLocaleUpperCase()}${roomTypeId.slice(1)}`} + {evaluateEntitiesInString(`${roomTypeId[0].toLocaleUpperCase()}${roomTypeId.slice(1)}`)} ))} diff --git a/src/components/Table/Section/Floor/Room/Header.tsx b/src/components/Table/Section/Floor/Room/Header.tsx index f8c02f13..9de08bc8 100644 --- a/src/components/Table/Section/Floor/Room/Header.tsx +++ b/src/components/Table/Section/Floor/Room/Header.tsx @@ -1,7 +1,9 @@ import React from "react"; import Typography from "@mui/material/Typography"; +import { evaluateEntitiesInString } from "../../../../../utils"; import { Room } from "../../../../../redux/roomsSlice"; + import RowHeader from "../../Row/Header"; type HeaderProps = { @@ -21,7 +23,7 @@ export default function Header({ room }: HeaderProps): JSX.Element { }} variant="bodySmall" > - {significantRoomType} + {evaluateEntitiesInString(significantRoomType)} ); diff --git a/src/components/Table/Section/Header.tsx b/src/components/Table/Section/Header.tsx index 7ed0076c..aaf74503 100644 --- a/src/components/Table/Section/Header.tsx +++ b/src/components/Table/Section/Header.tsx @@ -5,6 +5,7 @@ import ExpandLessOutlinedIcon from "@mui/icons-material/ExpandLessOutlined"; import ExpandMoreOutlinedIcon from "@mui/icons-material/ExpandMoreOutlined"; import M3IconButton from "../../m3/M3IconButton"; +import { evaluateEntitiesInString } from "../../../utils"; type HeaderProps = { name: string, @@ -25,7 +26,7 @@ export default function Header({ name, collapseCallback }: HeaderProps): JSX.Ele pt: "1rem", pb: "1rem" }}> - {capitalizedFloor} + {evaluateEntitiesInString(capitalizedFloor)} { collapseCallback(); setIconState(!iconState); diff --git a/src/components/Tile/Body.tsx b/src/components/Tile/Body.tsx index 2221696b..3c52482c 100644 --- a/src/components/Tile/Body.tsx +++ b/src/components/Tile/Body.tsx @@ -5,11 +5,12 @@ import { useAppSelector, useLeftmostDate } from "../../redux/hooks"; import { getCanvasFontSize, getTextWidth } from "./utils"; import { TableContext } from "../Table/TextWidthCanvas"; import { TileContext } from "./context"; +import { evaluateEntitiesInString } from "../../utils"; export default function Body(): JSX.Element { const data = useContext(TileContext).data; const canvasRef = useContext(TableContext).canvasRef; - const significantEntity = data ? data.entity.replace("Camera ", "").replace("camera ", "") : ""; + const significantEntity = data ? data.roomType.replace("Camera ", "").replace("camera ", "") : ""; const leftmostDate = useLeftmostDate(); const adjustLayoutRequestId = useAppSelector((state) => state.layout.adjustLayoutRequestId); const bodyRef = useRef(null); @@ -46,6 +47,6 @@ export default function Body(): JSX.Element { }, [canvasRef, adjustLayoutRequestId, leftmostDate, significantEntity]); return ( - {body} + {evaluateEntitiesInString(body)} ); } diff --git a/src/components/Tile/Title.tsx b/src/components/Tile/Title.tsx index ed2b5b70..22614143 100644 --- a/src/components/Tile/Title.tsx +++ b/src/components/Tile/Title.tsx @@ -6,6 +6,7 @@ import { getCanvasFontSize, getTextWidth } from "./utils"; import { TableContext } from "../Table/TextWidthCanvas"; import { TileContext } from "./context"; import { TileData } from "../../redux/tilesSlice"; +import { evaluateEntitiesInString } from "../../utils"; export default function Title(): JSX.Element | null { const { data } = useContext(TileContext); @@ -119,6 +120,6 @@ function TitleWrappee({ data }: TitleWrappeeProps): JSX.Element { }, [canvasRef, adjustLayoutRequestId, leftmostDate, data.name, data.persons]); return ( - {title} + {evaluateEntitiesInString(title)} ); } diff --git a/src/components/Tools/index.tsx b/src/components/Tools/index.tsx index ad0f280f..69f7774b 100644 --- a/src/components/Tools/index.tsx +++ b/src/components/Tools/index.tsx @@ -7,7 +7,7 @@ import Collapse from "@mui/material/Collapse"; import TextField from "@mui/material/TextField"; import CircularProgress from "@mui/material/CircularProgress"; -import { CityTaxData, fetchCityTaxAsync, fetchIstatDataAsync, fetchPoliceDataAsync } from "../../api"; +import { CityTaxData, fetchCityTaxAsync, fetchPoliceRicevutaAsync, postIstatExportRequestAsync, postPoliceExportRequestAsync } from "../../api"; import * as Utils from "../../utils"; import { useAppDispatch, useAppSelector, useCurrentDate } from "../../redux/hooks"; import { show as showMessage } from "../../redux/snackbarMessageSlice"; @@ -30,8 +30,8 @@ export default function Tools(): JSX.Element { const drawerOpened = useAppSelector((state) => state.drawer.open); const [cityTaxData, setCityTaxData] = useState(undefined); const [isCityTaxLoading, setIsCityTaxLoading] = useState(false); - const anchorRef = useRef(null); const [isDownloadDataLoading, setIsDownloadDataLoading] = useState(false); + const anchorRef = useRef(null); const isValid = isFromValid && isToValid; const openDetails = cityTaxData !== undefined; @@ -54,18 +54,31 @@ export default function Tools(): JSX.Element { } } - function download( - onFetchAsync: (date: string) => Promise<{ data: Blob }>, - setFilename: (value: string) => string - ): void { + function requestExport(onPostAsync: (date: string) => Promise): void { + async function postDataAsync() { + try { + await onPostAsync(downloadDate); + dispatch(showMessage({ type: "success", message: "I dati sono stati mandati correttamente!" })); + } catch (error) { + dispatch(showMessage({ type: "error" })); + } finally { + setIsDownloadDataLoading(false); + } + } + + setIsDownloadDataLoading(true); + postDataAsync(); + } + + function downloadRicevuta(): void { async function fetchDataAsync() { try { - const response = await onFetchAsync(downloadDate); + const response = await fetchPoliceRicevutaAsync(downloadDate); const data = response.data; if (anchorRef.current) { if (data.size > 0) { anchorRef.current.href = URL.createObjectURL(data); - anchorRef.current.download = setFilename(downloadDate); + anchorRef.current.download = `polizia-ricevuta-${downloadDate}.pdf`; anchorRef.current.click(); } else { dispatch(showMessage({ type: "info", message: "Niente data da scaricare!" })); @@ -96,7 +109,7 @@ export default function Tools(): JSX.Element { color: theme.palette.onSurfaceVariant.light }}> - Scarica dati + Esporta dati { @@ -106,13 +119,14 @@ export default function Tools(): JSX.Element { }} renderInput={(props) => } /> - + {isDownloadDataLoading ? : ( <> - download(fetchPoliceDataAsync, (date) => `polizia-${date}.txt`)}>Polizia - download(fetchIstatDataAsync, (date) => `istat-${date}.pdf`)}>ISTAT + Scarica ricevuta + requestExport(postPoliceExportRequestAsync)}>Polizia + requestExport(postIstatExportRequestAsync)}>ISTAT )} diff --git a/src/redux/tilesSlice.ts b/src/redux/tilesSlice.ts index 042c0242..5e64e284 100644 --- a/src/redux/tilesSlice.ts +++ b/src/redux/tilesSlice.ts @@ -2,7 +2,7 @@ import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"; import { WritableDraft } from "immer/dist/internal"; import * as Utils from "../utils"; -import { fetchTilesAsync } from "../api"; +import { ackBookingsAsync, AckBookingsRequest, Booking, ColorAssignments, fetchBookingsBySessionAsync, postColorAssignments } from "../api"; import { show as showMessage } from "./snackbarMessageSlice"; import { fetchAsync as fetchFloorsAsync } from "./floorsSlice"; import { FetchPeriod } from "./tableSlice"; @@ -17,22 +17,27 @@ export type TileData = { from: string, nights: number, roomType: string, - entity: string, persons: number, color: TileColor, roomId?: number }; -export type ChangesMap = { +export type RoomChanges = { [key: string]: { - roomChanged: boolean, originalRoom?: number, - newRoom?: number, - originalColor?: TileColor, - newColor?: TileColor + newRoom?: number } }; +export type ColorChanges = { + [key: string]: { + originalColor: TileColor, + newColor: TileColor + } +}; + +type ColoredBooking = Required>; + export type State = { status: "idle" | "loading" | "failed", data: { @@ -54,7 +59,8 @@ export type State = { bookingsMap: { [key: string]: string[] }, - changesMap: ChangesMap, + roomChanges: RoomChanges, + colorChanges: ColorChanges, grabbedTile?: string, mouseYOnGrab: number, sessionId?: string @@ -67,17 +73,50 @@ const initialState: State = { unassignedMap: { }, grabbedMap: { }, bookingsMap: { }, - changesMap: { }, + roomChanges: { }, + colorChanges: { }, mouseYOnGrab: 0 }; -export const fetchAsync = createAsyncThunk( - "tiles/fetch", +export const fetchAsync = createAsyncThunk<{ bookings: ColoredBooking[], sessionId: string }, FetchPeriod>( + "bookings-by-session/fetch", async (arg: FetchPeriod, thunkApi) => { try { const state = thunkApi.getState() as RootState; - const response = await fetchTilesAsync(arg.from, arg.to, state.tiles.sessionId); - return response.data; + const response = await fetchBookingsBySessionAsync(arg.from, arg.to, state.tiles.sessionId); + const bookings = response.data.bookings; + const coloredBookings: ColoredBooking[] = []; + + const ackRequest: AckBookingsRequest = { + bookings: [], + sessionId: response.data.sessionId + }; + const assignments: ColorAssignments = { }; + + for (const booking of bookings) { + if (!booking.color) { + const newColor = `booking${Math.floor((Math.random() * 7)) + 1}` as TileColor; + assignments[booking.id] = newColor; + coloredBookings.push({ color: newColor, ...booking }); + } else { + coloredBookings.push({ color: booking.color, ...booking }); + } + + ackRequest.bookings.push({ + bookingId: booking.id, + lastModified: booking.lastModified + }); + } + + if (Object.keys(assignments).length > 0) { + await postColorAssignments(assignments); + } + + if (ackRequest.bookings.length > 0) { + await ackBookingsAsync(ackRequest); + } + + return { bookings: coloredBookings, sessionId: response.data.sessionId }; } catch(error) { thunkApi.dispatch(showMessage({ type: "error" })); throw thunkApi.rejectWithValue([]); @@ -94,7 +133,7 @@ export const tilesSlice = createSlice({ move: (state, action: PayloadAction<{ newY: number }>) => { tryMoveTile(state, action); if (state.grabbedTile) { - checkChangeReturnedToOriginal(state, state.grabbedTile); + checkRoomReturnedToOriginal(state, state.grabbedTile); } }, grab: (state, action: PayloadAction<{ tileId: string, mouseY: number }>) => { @@ -147,30 +186,36 @@ export const tilesSlice = createSlice({ }, unassign: (state, action: PayloadAction<{ tileId: string }>) => { tryRemoveAssignment(state, action); - checkChangeReturnedToOriginal(state, action.payload.tileId); + checkRoomReturnedToOriginal(state, action.payload.tileId); }, saveChanges: (state) => { - state.changesMap = { }; + state.roomChanges = { }; + state.colorChanges = { }; }, undoChanges: (state) => { unassignChangedTiles(state); reassignTiles(state); + reassignColors(state); }, setColor: (state, action: PayloadAction<{ tileId: string, color: TileColor }>) => { - const booking = state.data[action.payload.tileId].bookingId; - state.bookingsMap[booking].forEach((tileId) => { - if (state.changesMap[tileId] === undefined) { - state.changesMap[tileId] = { - roomChanged: false, - originalColor: state.data[tileId].color - }; - } else if (state.changesMap[tileId].originalColor === undefined) { - state.changesMap[tileId].originalColor = state.data[tileId].color; - } - state.changesMap[tileId].newColor = action.payload.color; + const bookingId = state.data[action.payload.tileId].bookingId; + + if (!state.colorChanges[bookingId]) { + state.colorChanges[bookingId] = { + originalColor: state.data[action.payload.tileId].color, + newColor: action.payload.color + }; + } else { + state.colorChanges[bookingId].newColor = action.payload.color; + } + + state.bookingsMap[bookingId].forEach((tileId) => { state.data[tileId].color = action.payload.color; - checkChangeReturnedToOriginal(state, tileId); }); + + if (state.colorChanges[bookingId].originalColor === state.colorChanges[bookingId].newColor) { + delete state.colorChanges[bookingId]; + } }, createRoom: (state, action: PayloadAction) => { const newRoom = action.payload; @@ -214,7 +259,7 @@ export const tilesSlice = createSlice({ }) .addCase(fetchAsync.fulfilled, (state, action) => { state.status = "idle"; - addFetchedTiles(state, action.payload.tiles); + addFetchedBookings(state, action.payload.bookings); state.sessionId = action.payload.sessionId; }) .addCase(fetchAsync.rejected, (state) => { @@ -236,36 +281,83 @@ export const { move, grab, drop, unassign, saveChanges, undoChanges, setColor, c export default tilesSlice.reducer; -function addFetchedTiles(state: WritableDraft, tiles: TileData[]): void { - tiles.forEach(tile => { - state.data[tile.id] = tile; - state.grabbedMap[tile.id] = false; - if (state.bookingsMap[tile.bookingId] === undefined) { - state.bookingsMap[tile.bookingId] = []; - } - state.bookingsMap[tile.bookingId].push(tile.id); - const roomId = tile.roomId; - if (roomId !== undefined) { - if (state.assignedMap[roomId] === undefined) { - state.assignedMap[roomId] = {}; +function addFetchedBookings(state: WritableDraft, bookings: ColoredBooking[]): void { + for (const booking of bookings) { + // remove all previous tiles and changes if booking was already fetched + if (state.bookingsMap[booking.id]) { + const previousTiles = state.bookingsMap[booking.id]; + for (const tileId of previousTiles) { + const tile = state.data[tileId]; + const roomId = tile.roomId; + const dateCounter = new Date(tile.from); + if (roomId !== undefined) { + for (let i = 0; i < tile.nights; i++) { + const x = Utils.dateToString(dateCounter); + state.assignedMap[roomId][x] = undefined; + dateCounter.setDate(dateCounter.getDate() + 1); + } + } else { + for (let i = 0; i < tile.nights; i++) { + const x = Utils.dateToString(dateCounter); + delete state.unassignedMap[x][tileId]; + dateCounter.setDate(dateCounter.getDate() + 1); + } + } + if (state.roomChanges[tileId]) { + delete state.roomChanges[tileId]; + } + delete state.data[tileId]; } - const dateCounter = new Date(tile.from); - for (let i = 0; i < tile.nights; i++) { - state.assignedMap[roomId][Utils.dateToString(dateCounter)] = tile.id; - dateCounter.setDate(dateCounter.getDate() + 1); + delete state.bookingsMap[booking.id]; + if (state.colorChanges[booking.id]) { + delete state.colorChanges[booking.id]; } - } else { - const dateCounter = new Date(tile.from); - for (let i = 0; i < tile.nights; i++) { - const x = Utils.dateToString(dateCounter); - if (state.unassignedMap[x] === undefined) { - state.unassignedMap[x] = { }; + } + + // assign new tiles + if (booking.status === "cancelled") { + continue; + } + + state.bookingsMap[booking.id] = []; + for (const tile of booking.tiles) { + const newTile: TileData = { + id: tile.id, + bookingId: booking.id, + name: booking.name, + from: tile.from, + nights: tile.nights, + roomType: tile.roomType, + persons: tile.persons, + color: booking.color, + roomId: tile.roomId + }; + + state.data[newTile.id] = newTile; + state.grabbedMap[newTile.id] = false; + state.bookingsMap[booking.id].push(newTile.id); + const roomId = newTile.roomId; + const dateCounter = new Date(newTile.from); + if (roomId !== undefined) { + if (state.assignedMap[roomId] === undefined) { + state.assignedMap[roomId] = {}; + } + for (let i = 0; i < newTile.nights; i++) { + state.assignedMap[roomId][Utils.dateToString(dateCounter)] = newTile.id; + dateCounter.setDate(dateCounter.getDate() + 1); + } + } else { + for (let i = 0; i < newTile.nights; i++) { + const x = Utils.dateToString(dateCounter); + if (state.unassignedMap[x] === undefined) { + state.unassignedMap[x] = { }; + } + state.unassignedMap[x][newTile.id] = newTile.id; + dateCounter.setDate(dateCounter.getDate() + 1); } - state.unassignedMap[x][tile.id] = tile.id; - dateCounter.setDate(dateCounter.getDate() + 1); } } - }); + } } function tryMoveTile( @@ -312,9 +404,9 @@ function tryRemoveAssignment(state: WritableDraft, action: PayloadAction< } function unassignChangedTiles(state: WritableDraft): void { - for (const tileId of Object.keys(state.changesMap)) { + for (const tileId of Object.keys(state.roomChanges)) { const tileData = state.data[tileId]; - const newRoom = state.changesMap[tileId].newRoom; + const newRoom = state.roomChanges[tileId].newRoom; if (newRoom !== undefined) { state.data[tileId].roomId = undefined; @@ -333,9 +425,9 @@ function unassignChangedTiles(state: WritableDraft): void { } function reassignTiles(state: WritableDraft): void { - for (const tileId of Object.keys(state.changesMap)) { + for (const tileId of Object.keys(state.roomChanges)) { const tileData = state.data[tileId]; - const originalRoom = state.changesMap[tileId].originalRoom; + const originalRoom = state.roomChanges[tileId].originalRoom; if (originalRoom !== undefined) { state.data[tileId].roomId = originalRoom; @@ -350,23 +442,24 @@ function reassignTiles(state: WritableDraft): void { dateCounter.setDate(dateCounter.getDate() + 1); } } + } + state.roomChanges = { }; +} - const originalColor = state.changesMap[tileId].originalColor; - if (originalColor) { - state.data[tileId].color = originalColor; +function reassignColors(state: WritableDraft): void { + for (const bookingId of Object.keys(state.colorChanges)) { + const tileIds = state.bookingsMap[bookingId]; + const change = state.colorChanges[bookingId]; + for (const tileId of tileIds) { + state.data[tileId].color = change.originalColor; } - - delete state.changesMap[tileId]; } + state.colorChanges = { }; } -function checkChangeReturnedToOriginal(state: WritableDraft, tileId: string): void { - if ( - state.changesMap[tileId] && - (state.changesMap[tileId].originalRoom === state.changesMap[tileId].newRoom) && - (state.changesMap[tileId].originalColor === state.changesMap[tileId].newColor) - ) { - delete state.changesMap[tileId]; +function checkRoomReturnedToOriginal(state: WritableDraft, tileId: string): void { + if (state.roomChanges[tileId] && (state.roomChanges[tileId].originalRoom === state.roomChanges[tileId].newRoom)) { + delete state.roomChanges[tileId]; } } @@ -430,14 +523,10 @@ function assignTile(state: WritableDraft, tileId: string, newY: number): } function saveRoomChange(state: WritableDraft, tileId: string, prevY: number | undefined, newY: number | undefined): void { - if (!state.changesMap[tileId]) { - state.changesMap[tileId] = { - roomChanged: true, + if (!state.roomChanges[tileId]) { + state.roomChanges[tileId] = { originalRoom: prevY }; - } else if (!state.changesMap[tileId].roomChanged) { - state.changesMap[tileId].roomChanged = true; - state.changesMap[tileId].originalRoom = prevY; } - state.changesMap[tileId].newRoom = newY; + state.roomChanges[tileId].newRoom = newY; } diff --git a/src/utils.ts b/src/utils.ts index 49274658..095a396c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -31,3 +31,17 @@ export function getFirstLetterUppercase(str: string): string { export function getFullRoomType(entity: string, roomType: string): string { return `${getFirstLetterUppercase(entity)} (${getFirstLetterUppercase(roomType)})`; } + +export function evaluateEntitiesInString(source: string): string { + const split = source.split("&#"); + if (split.length <= 1) { + return source; + } + let result = split[0]; + for (let i = 1; i < split.length; i++) { + const charCodeSplit = split[i].split(";"); + const charCode = Number.parseInt(charCodeSplit[0]); + result += `${String.fromCharCode(charCode)}${charCodeSplit[1]}`; + } + return result; +} diff --git a/todo.txt b/todo.txt index d5674206..0b9cca9c 100644 --- a/todo.txt +++ b/todo.txt @@ -1,17 +1 @@ -- Consider that we have to explicitly save sessions stuff and first loaded tiles via a specific API call. -- Tile color got from API can be null. -- Reload all tiles that were associated with fetched booking on fetching tiles. -- Fetching whole bookings instead of tiles. Should be flattened in front-end. -- Split saving colors and room assignments. Colors should be associated with bookings. -- Add from dates to booking and booking short by id fetch. -- Add booking id to clients by tile request. -- Add date interval to clients by query request. Load asynchronously clients spliting the whole period from 01/01/2021 to nowdays into more smaller periods for more responsiveness. -- Handle html entities in back-end response. -- Handle live modified tiles. -- Use POST for police data export. Don't download it, just wait for ok status. -- Add download police ricevuta button. -- Remove entity property from tile data. -- Use POST method for istat data export. Same as police, don't download, just verify ok status. -- Add province of birth to client data. - Handle touchscreen. -- Police data sending directly to police web portal?? diff --git a/webpack.config.js b/webpack.config.js index 94281e9b..f44cf5d0 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -57,26 +57,28 @@ module.exports = { throw new Error("webpack-dev-server is not defined"); } - devServer.app.get("/api/v1/stats/police", (request, response) => { + devServer.app.post("/api/v1/police", (_, response) => { setTimeout(() => { - const date = new Date(request.query["date"]); - if (date.getDate() % 2 === 0) { - response.send("Exported!"); - } else { - response.send(""); - } + response.json("ok"); + }, 500); + }); + + devServer.app.post("/api/v1/istat", (_, response) => { + setTimeout(() => { + response.json("ok"); }, 500); }); - devServer.app.get("/api/v1/stats/istat", (request, response) => { + devServer.app.get("/api/v1/police/ricevuta", (request, response) => { setTimeout(() => { - response.set("content-type", "application/pdf"); - response.set("content-disposition", `attachment; filename="istat-${request.query["date"]}.pdf"`); + response.set("Content-Type", "application/pdf"); + response.set("Content-Transfer-Encoding", "binary"); + response.set("Content-Disposition", `attachment; filename="polizia-ricevuta-${request.query["date"]}.pdf"`); response.send("Exported!"); }, 500); }); - devServer.app.get("/api/v1/stats/city-tax", (_, response) => { + devServer.app.get("/api/v1/city-tax", (_, response) => { setTimeout(() => { response.json({ standard: 100, @@ -90,32 +92,71 @@ module.exports = { setTimeout(() => { response.json({ id: "1", + status: "new", name: "Vasya Pupkin", + lastModified: "2022-02-01", from: "2022-02-02", to: "2022-02-05", - rooms: [ + color: "booking1", + tiles: [ { id: "1", - bookingId: "1", - name: "Vasya Pupkin", from: "2022-02-02", nights: 3, roomType: "camera matrimoniale/doppia", entity: "camera matrimoniale", - persons: 2, - color: "booking1", + persons: [ + { + id: "0", + bookingId: "0", + name: "Ivan", + surname: "Petrov", + dateOfBirth: "1986-05-04", + placeOfBirth: "Canazei", + provinceOfBirth: "TN", + stateOfBirth: "Italia" + }, + { + id: "1", + bookingId: "1", + name: "Vasya", + surname: "Pupkin", + dateOfBirth: "1985-05-06", + placeOfBirth: "Canazei", + provinceOfBirth: "TN", + stateOfBirth: "Italia" + } + ], roomId: 2 }, { id: "2", - bookingId: "1", - name: "Ivan Petrov", from: "2022-02-02", nights: 3, roomType: "camera matrimoniale/doppia", entity: "camera matrimoniale", - persons: 2, - color: "booking1", + persons: [ + { + id: "0", + bookingId: "0", + name: "Ivan", + surname: "Petrov", + dateOfBirth: "1986-05-04", + placeOfBirth: "Canazei", + provinceOfBirth: "TN", + stateOfBirth: "Italia" + }, + { + id: "1", + bookingId: "1", + name: "Vasya", + surname: "Pupkin", + dateOfBirth: "1985-05-06", + placeOfBirth: "Canazei", + provinceOfBirth: "TN", + stateOfBirth: "Italia" + } + ], roomId: 4 } ] @@ -123,25 +164,14 @@ module.exports = { }, 500); }); - devServer.app.get("/api/v1/booking-short", (_, response) => { - setTimeout(() => { - response.json({ - id: "1", - name: "Vasya Pupkin", - from: "2022-02-02", - to: "2022-02-05", - occupations: 2, - color: "booking1" - }); - }, 500); - }); - - devServer.app.get("/api/v1/bookings", (_, response) => { + devServer.app.get("/api/v1/bookings-by-name", (_, response) => { setTimeout(() => { response.json([ { id: "0", + status: "new", name: "Ivan Petrov", + lastModified: "2022-02-01", from: "2022-02-02", to: "2022-02-05", occupations: 1, @@ -149,7 +179,9 @@ module.exports = { }, { id: "1", + status: "new", name: "Vasya Pupkin", + lastModified: "2022-02-01", from: "2022-03-01", to: "2022-03-04", occupations: 2, @@ -157,7 +189,9 @@ module.exports = { }, { id: "2", + status: "new", name: "Petr Sidorov", + lastModified: "2022-02-01", from: "2022-03-16", to: "2022-03-17", occupations: 1, @@ -165,7 +199,9 @@ module.exports = { }, { id: "3", + status: "new", name: "Petr Ivanov", + lastModified: "2022-02-01", from: "2022-04-01", to: "2022-04-03", occupations: 1, @@ -173,7 +209,9 @@ module.exports = { }, { id: "4", + status: "new", name: "Kirill Kirilov", + lastModified: "2022-02-01", from: "2022-04-02", to: "2022-04-03", occupations: 1, @@ -183,67 +221,85 @@ module.exports = { }, 500); }); - devServer.app.get("/api/v1/clients", (request, response) => { + devServer.app.get("/api/v1/clients-by-tile", (_, response) => { setTimeout(() => { - if (request.query["tileId"]) { - response.json([ - { - id: "0", - bookingId: "0", - name: "Ivan", - surname: "Petrov", - dateOfBirth: "1986-05-04", - placeOfBirth: "Canazei (TN)", - stateOfBirth: "Italia" - }, - { - id: "1", - bookingId: "1", - name: "Vasya", - surname: "Pupkin", - dateOfBirth: "1985-05-06", - placeOfBirth: "Canazei (TN)", - stateOfBirth: "Italia" - } - ]); - } else { - response.json([ - { - id: "0", - bookingId: "0", - name: "Ivan", - surname: "Petrov", - dateOfBirth: "1986-05-04", - placeOfBirth: "Canazei (TN)", - stateOfBirth: "Italia" - }, - { - id: "1", - name: "Vasya", - bookingId: "1", - surname: "Pupkin", - dateOfBirth: "1985-05-06", - placeOfBirth: "Canazei (TN)", - stateOfBirth: "Italia" - }, - { - id: "2", - bookingId: "2", - name: "Ilja", - surname: "Maksimov", - dateOfBirth: "1985-05-06", - stateOfBirth: "Russia" - }, - { - id: "3", - bookingId: "2", - name: "Stepan", - surname: "Ogurzov", - dateOfBirth: "1985-05-06", - stateOfBirth: "Russia" - } - ]); - } + response.json([ + { + id: "0", + bookingId: "0", + name: "Ivan", + surname: "Petrov", + dateOfBirth: "1986-05-04", + placeOfBirth: "Canazei", + provinceOfBirth: "TN", + stateOfBirth: "Italia" + }, + { + id: "1", + bookingId: "1", + name: "Vasya", + surname: "Pupkin", + dateOfBirth: "1985-05-06", + placeOfBirth: "Canazei", + provinceOfBirth: "TN", + stateOfBirth: "Italia" + } + ]); + }, 500); + }); + + devServer.app.get("/api/v1/clients-by-query", (_, response) => { + setTimeout(() => { + response.json([ + { + id: "0", + bookingId: "0", + name: "Ivan", + surname: "Petrov", + dateOfBirth: "1986-05-04", + placeOfBirth: "Canazei", + provinceOfBirth: "TN", + stateOfBirth: "Italia", + bookingName: "Vasya Pupkin", + bookingFrom: "2022-02-02", + bookingTo: "2022-02-05", + }, + { + id: "1", + name: "Vasya", + bookingId: "1", + surname: "Pupkin", + dateOfBirth: "1985-05-06", + placeOfBirth: "Canazei", + provinceOfBirth: "TN", + stateOfBirth: "Italia", + bookingName: "Vasya Pupkin", + bookingFrom: "2022-02-02", + bookingTo: "2022-02-05", + }, + { + id: "2", + bookingId: "2", + name: "Ilja", + surname: "Maksimov", + dateOfBirth: "1985-05-06", + stateOfBirth: "Russia", + bookingName: "Vasya Pupkin", + bookingFrom: "2022-02-02", + bookingTo: "2022-02-05", + }, + { + id: "3", + bookingId: "2", + name: "Stepan", + surname: "Ogurzov", + dateOfBirth: "1985-05-06", + stateOfBirth: "Russia", + bookingName: "Vasya Pupkin", + bookingFrom: "2022-02-02", + bookingTo: "2022-02-05", + } + ]); }, 500); }); @@ -411,119 +467,183 @@ module.exports = { }, 500); }); - devServer.app.get("/api/v1/tiles", (request, response) => { + devServer.app.get("/api/v1/bookings-by-session", (request, response) => { setTimeout(() => { const sessionId = request.query["sessionId"]; if (!sessionId) { response.json({ - tiles: [ + bookings: [ { id: "0", - bookingId: "0", - name: "Petr Ivanov", + status: "new", + name: "Petr D'Ivanov", + lastModified: "2022-02-02", from: "2022-09-15", - nights: 40, - roomType: "camera matrimoniale/doppia", - entity: "camera doppia", - persons: 2, + to: "2022-10-25", color: "booking1", - roomId: 2 + tiles: [ + { + id: "0", + from: "2022-09-15", + nights: 40, + roomType: "camera matrimoniale/doppia", + entity: "camera doppia", + persons: 2, + roomId: 2 + } + ] }, { id: "1", - bookingId: "1", + status: "new", name: "Ivan Petrov", + lastModified: "2022-02-02", from: "2022-10-25", - nights: 2, - roomType: "camera matrimoniale/doppia", - entity: "camera doppia", - persons: 2, + to: "2022-10-27", color: "booking2", - roomId: 1 + tiles: [ + { + id: "1", + from: "2022-10-25", + nights: 2, + roomType: "camera matrimoniale/doppia", + entity: "camera doppia", + persons: 2, + roomId: 1 + }, + { + id: "6", + from: "2022-10-25", + nights: 2, + roomType: "camera matrimoniale/doppia", + entity: "camera doppia", + persons: 2, + roomId: 4 + }, + ] }, { id: "2", - bookingId: "2", + status: "new", name: "Vasya Pupkin", + lastModified: "2022-02-02", from: "2022-10-20", - nights: 3, - roomType: "camera matrimoniale/doppia", - entity: "camera doppia", - persons: 2, + to: "2022-10-23", color: "booking3", - roomId: 5 + tiles: [ + { + id: "2", + from: "2022-10-20", + nights: 3, + roomType: "camera matrimoniale/doppia", + entity: "camera doppia", + persons: 2, + roomId: 5 + } + ] }, { id: "3", - bookingId: "3", + status: "new", name: "Petr Petrov", + lastModified: "2022-02-02", from: "2022-10-01", - nights: 4, - roomType: "camera tripla", - entity: "camera tripla", - persons: 3, - color: "booking4" + to: "2022-10-05", + color: "booking4", + tiles: [ + { + id: "3", + from: "2022-10-01", + nights: 4, + roomType: "camera tripla", + entity: "camera tripla", + persons: 3 + } + ] }, { id: "4", - bookingId: "4", + status: "new", name: "Ivan Vasiliev", + lastModified: "2022-02-02", from: "2022-10-28", - nights: 4, - roomType: "camera singola", - entity: "camera singola", - persons: 1, - color: "booking5" + to: "2022-11-01", + tiles: [ + { + id: "4", + from: "2022-10-28", + nights: 4, + roomType: "camera singola", + entity: "camera singola", + persons: 1 + } + ] }, { id: "5", - bookingId: "5", + status: "new", name: "Vasya Ivanov", + lastModified: "2022-02-02", from: "2022-09-01", - nights: 60, - roomType: "camera matrimoniale/doppia", - entity: "camera doppia", - persons: 2, - color: "booking6" - }, - { - id: "6", - bookingId: "1", - name: "Sasha Smirnov", - from: "2022-10-25", - nights: 2, - roomType: "camera matrimoniale/doppia", - entity: "camera doppia", - persons: 2, - color: "booking2", - roomId: 4 + to: "2022-10-31", + color: "booking6", + tiles: [ + { + id: "5", + from: "2022-09-01", + nights: 60, + roomType: "camera matrimoniale/doppia", + entity: "camera doppia", + persons: 2 + } + ] }, { id: "7", - bookingId: "7", + status: "cancelled", name: "Sasha Smirnov", + lastModified: "2022-02-02", from: "2022-10-23", - nights: 2, - roomType: "camera matrimoniale/doppia", - entity: "camera doppia", - persons: 2, + to: "2022-10-25", color: "booking3", - roomId: 4 + tiles: [ + { + id: "7", + from: "2022-10-23", + nights: 2, + roomType: "camera matrimoniale/doppia", + entity: "camera doppia", + persons: 2, + roomId: 4 + } + ] } ], - sessionId: Math.floor(Math.random() * 10000) + sessionId: `${Math.floor(Math.random() * 10000)}` }); } else { response.json({ - tiles: [], + bookings: [], sessionId: sessionId }); } }, 500); }); - devServer.app.post("/api/v1/changes", (_, response) => { + devServer.app.post("/api/v1/room-assignments", (_, response) => { + setTimeout(() => { + response.json("ok"); + }, 500); + }); + + devServer.app.post("/api/v1/color-assignments", (_, response) => { + setTimeout(() => { + response.json("ok"); + }, 500); + }); + + devServer.app.post("/api/v1/ack-bookings", (_, response) => { setTimeout(() => { response.json("ok"); }, 500);