Skip to content

Commit

Permalink
Version 1.2.0 (#23)
Browse files Browse the repository at this point in the history
* Updated package version

* Reordered todo stuff

* Handling null color values from api

* Added additional map for saving color changes

* Splitted colors and room assignments

* Updated todo

* Added last modified filed to loaded tiles

* Transmitting back ack request to back-end to store session values

* Getting along with whole booking object transmission from API

* Skipping cancelled bookings

* Aligning booking by id response with API

* Aligning booking by name response with API

* Updated todo

* Fixed bug with reloading clients in booking details

* Updated navlinks for booking details and client card

* Updated todo

* Booking can be generic

* Handling case when client data is included in booking response

* Loading client cards with booking data included

* Removed booking-short endpoint

* Added booking id to client-by-tile request

* Enhanced client search experience by splitting search requests into periods of search

* Updated todo

* Handled html entities in data from API

* Updated todo

* Using post to request police and istat data publication

* Added download ricevuta button

* Removed entity from tile data

* Added province of birth

* Updated readme

Co-authored-by: Daniil Ryzhkov <drop.sovet@gmail.com>
  • Loading branch information
dropik and Daniil Ryzhkov authored Oct 26, 2022
1 parent 0469e9f commit bc01004
Show file tree
Hide file tree
Showing 37 changed files with 765 additions and 406 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
111 changes: 78 additions & 33 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,59 @@
import { ChangesMap, TileColor, TileData } from "./redux/tilesSlice";
import { TileColor } from "./redux/tilesSlice";

export type CityTaxData = {
standard: number,
children: number,
over10Days: number
};

export type BookingData = {
export type Booking<TPerson> = {
id: string,
status: "new" | "modified" | "cancelled",
name: string,
lastModified: string,
from: string,
to: string,
rooms: TileData[]
color?: TileColor,
tiles: Tile<TPerson>[]
};

export type BookingShortData = {
export type Tile<TPerson> = {
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,
Expand All @@ -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<Floor[]>("/api/v1/floors");
}
Expand Down Expand Up @@ -84,53 +121,52 @@ export function fetchRoomTypesAsync(): Promise<{ data: RoomType[] }> {
return fetchJsonDataAsync<RoomType[]>("/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<number>[], sessionId: string } }> {
return fetchJsonDataAsync<{ bookings: Booking<number>[], sessionId: string }>(`/api/v1/bookings-by-session?from=${from}&to=${to}${sessionId ? `&sessionId=${sessionId}` : ""}`);
}

export function postChangesAsync(changes: ChangesMap): Promise<void> {
return postDataAsync("/api/v1/changes", changes);
export function ackBookingsAsync(request: AckBookingsRequest): Promise<void> {
return postDataAsync("/api/v1/ack-bookings", request);
}

export async function fetchBookingById(bookingId: string): Promise<{ data: BookingData }> {
return fetchJsonDataAsync<BookingData>(`/api/v1/booking?id=${bookingId}`);
export function postColorAssignments(assignments: ColorAssignments): Promise<void> {
return postDataAsync("/api/v1/color-assignments", assignments);
}

export async function fetchBookingShortById(bookingId: string): Promise<{ data: BookingShortData}> {
return fetchJsonDataAsync<BookingShortData>(`/api/v1/booking-short?id=${bookingId}`);
export function postRoomAssignmentsAsync(assignments: RoomAssignments): Promise<void> {
return postDataAsync("/api/v1/room-assignments", assignments);
}

export async function fetchClientsByTile(tileId: string): Promise<{ data: ClientData[] }> {
return fetchJsonDataAsync<ClientData[]>(`/api/v1/clients?tileId=${tileId}`);
export async function fetchBookingById(bookingId: string, from: string): Promise<{ data: Booking<Client[]> }> {
return fetchJsonDataAsync<Booking<Client[]>>(`/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<Client[]>(`/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<void> {
return postDataAsync("/api/v1/police", { date });
}

export async function fetchCityTaxAsync(from: string, to: string): Promise<{ data: CityTaxData }> {
return fetchJsonDataAsync<CityTaxData>(`/api/v1/stats/city-tax?from=${from}&to=${to}`);
export async function postIstatExportRequestAsync(date: string): Promise<void> {
return postDataAsync("/api/v1/istat", { date });
}

export async function fetchBookings(name: string, from: string, to: string): Promise<{ data: BookingShortData[] }> {
return fetchJsonDataAsync<BookingShortData[]>(`/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<ClientData[]>(`/api/v1/clients?query=${query}`);
export async function fetchCityTaxAsync(from: string, to: string): Promise<{ data: CityTaxData }> {
return fetchJsonDataAsync<CityTaxData>(`/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<BookingShort[]>(`/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<ClientWithBooking[]>(`/api/v1/clients-by-query?query=${query}&from=${from}&to=${to}`);
}

async function fetchJsonDataAsync<T>(query: string): Promise<{ data: T }> {
Expand All @@ -145,6 +181,15 @@ async function fetchJsonDataAsync<T>(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<TData, TResponse>(url: string, data: TData): Promise<TResponse> {
const response = await fetch(url, {
method: "POST",
Expand Down
2 changes: 1 addition & 1 deletion src/components/AppRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default function AppRoutes(): JSX.Element {
<Routes>
<Route path="/" element={<Table />} />
<Route path="bookings" element={<Bookings />}>
<Route path=":bookingId" element={<BookingDetails />} />
<Route path=":from/:bookingId" element={<BookingDetails />} />
</Route>
<Route path="tools" element={<Tools />} />
<Route path="clients" element={<Clients />} />
Expand Down
42 changes: 42 additions & 0 deletions src/components/BookingDetails/StayDetails.tsx
Original file line number Diff line number Diff line change
@@ -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<Client[]>,
tile: Tile<Client[]>,
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 (
<BookingDetailsContext.Provider value={{ clients: tile.persons }}>
<TileContext.Provider value={{ data: tileData, cropRight: false, cropLeft: false }}>
<ExpandableTile variant="in-content" isFirst={isFirst} />
</TileContext.Provider>
</BookingDetailsContext.Provider>
);
}, [tile, booking, isFirst]);
return el;
});
9 changes: 9 additions & 0 deletions src/components/BookingDetails/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createContext } from "react";

import { Client } from "../../api";

export const BookingDetailsContext = createContext<{
clients: Client[]
}>({
clients: []
});
32 changes: 16 additions & 16 deletions src/components/BookingDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<BookingData | undefined>(undefined);
const [booking, setBooking] = useState<Booking<Client[]> | undefined>(undefined);
const skeletonRooms = [0, 1];

const periodStr = booking ?
Expand All @@ -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);
Expand All @@ -47,7 +49,7 @@ export default function BookingDetails(): JSX.Element {
isSubscribed = false;
setBooking(undefined);
};
}, [dispatch, bookingId]);
}, [dispatch, from, bookingId]);

return (
<Stack
Expand Down Expand Up @@ -76,7 +78,7 @@ export default function BookingDetails(): JSX.Element {
pr: "1rem",
pl: "1rem"
}}>
<Typography variant="titleMedium">{booking ? booking.name : <M3Skeleton width="6rem" />}</Typography>
<Typography variant="titleMedium">{booking ? evaluateEntitiesInString(booking.name) : <M3Skeleton width="6rem" />}</Typography>
<Typography variant="bodySmall">{periodStr ? periodStr : <M3Skeleton width="10rem" />}</Typography>
</Stack>
</Box>
Expand All @@ -86,15 +88,13 @@ export default function BookingDetails(): JSX.Element {
boxSizing: "border-box",
pb: "1rem"
}}>
{booking ? booking.rooms.map((room, index) => (
<TileContext.Provider key={room.id} value={{ data: room, cropRight: false, cropLeft: false }}>
<ExpandableTile variant="in-content" isFirst={index === 0} />
</TileContext.Provider>
)) : skeletonRooms.map((room) => (
<TileContext.Provider key={room} value={{ cropRight: false, cropLeft: false }}>
<ExpandableTile variant="in-content" isFirst={room === 0} />
</TileContext.Provider>
))}
{booking
? booking.tiles.map((tile, index) => <StayDetails key={tile.id} tile={tile} booking={booking} isFirst={index === 0} />)
: skeletonRooms.map((room) => (
<TileContext.Provider key={room} value={{ cropRight: false, cropLeft: false }}>
<ExpandableTile variant="in-content" isFirst={room === 0} />
</TileContext.Provider>
))}
</Stack>
</Stack>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Bookings/Booking/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export default function Button({ children }: ButtonProps): JSX.Element {
const booking = useContext(BookingContext);

return (
<M3NavLink to={`/bookings/${booking.id}`}>
<M3NavLink to={`/bookings/${booking.from}/${booking.id}`}>
{({ isActive }) => (
<M3ListItemButton selected={isActive} sx={{
height: "4.75rem",
Expand Down
3 changes: 2 additions & 1 deletion src/components/Bookings/Booking/ShortInfo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Typography from "@mui/material/Typography";

import BookingContext from "./context";
import ListItemText from "./ListItemText";
import { evaluateEntitiesInString } from "../../../utils";

export default function ShortInfo(): JSX.Element {
const booking = useContext(BookingContext);
Expand All @@ -18,7 +19,7 @@ export default function ShortInfo(): JSX.Element {
pb: "1rem"
}}>
<ListItemText>
<Typography variant="titleMedium">{booking.name}</Typography>
<Typography variant="titleMedium">{evaluateEntitiesInString(booking.name)}</Typography>
</ListItemText>
<ListItemText>
<Typography variant="bodySmall">{`${formattedFrom} - ${formattedTo}`}</Typography>
Expand Down
6 changes: 4 additions & 2 deletions src/components/Bookings/Booking/context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createContext } from "react";

import { BookingShortData } from "../../../api";
import { BookingShort } from "../../../api";

const BookingContext = createContext<BookingShortData>({
const BookingContext = createContext<BookingShort>({
id: "",
status: "new",
name: "",
lastModified: "",
from: "",
to: "",
occupations: 0,
Expand Down
Loading

0 comments on commit bc01004

Please sign in to comment.