Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 6694fae

Browse files
authored
TILA-1969 reservation success processing (#554)
* TILA-1969 reservation success processing * TILA-1969 remote logout * TILA-1969 force login * TILA-1969 show receipt in reservation view * TILA-1969 cleanup * TILA-1969 route confirmed reservation * TILA-1969 use linkbutton * TILA-1969 incomplete return * TILA-1969 cleanup
1 parent b18a7af commit 6694fae

21 files changed

Lines changed: 760 additions & 59 deletions

File tree

tilavaraus.graphql

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3384,7 +3384,9 @@ type ReservationStaffCreateMutationPayload {
33843384
clientMutationId: String
33853385
}
33863386

3387-
"""An enumeration."""
3387+
"""
3388+
An enumeration.
3389+
"""
33883390
enum ReservationState {
33893391
SCHEDULED_RESERVATION
33903392
SCHEDULED_PERIOD

ui/components/common/Navigation/NavigationUserMenu/NavigationUserMenu.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import React from "react";
22
import { IconSignout, Navigation as HDSNavigation } from "hds-react";
3-
import { signIn, signOut, useSession } from "next-auth/react";
3+
import { signIn, useSession } from "next-auth/react";
44
import { useTranslation } from "next-i18next";
55
import { useRouter } from "next/router";
66
import styled, { css } from "styled-components";
77
import { NavigationUserMenuUserCard } from "./NavigationUserMenuUserCard";
8-
import {
9-
authenticationIssuer,
10-
authenticationLogoutApiRoute,
11-
} from "../../../../modules/const";
8+
import { authenticationIssuer } from "../../../../modules/const";
129
import { MenuItem } from "../types";
10+
import { useLogout } from "../../../../hooks/useLogout";
1311

1412
const StyledUserMenu = styled(HDSNavigation.User)<{
1513
$active?: boolean;
@@ -103,6 +101,7 @@ const NavigationUserMenu = () => {
103101
const router = useRouter();
104102
const session = useSession();
105103
const { t } = useTranslation();
104+
const { logout } = useLogout();
106105

107106
const user = session.data?.user;
108107
const isActive = userMenuItems
@@ -116,7 +115,7 @@ const NavigationUserMenu = () => {
116115
};
117116

118117
const handleSignOut = () => {
119-
signOut({ redirect: true, callbackUrl: authenticationLogoutApiRoute });
118+
logout();
120119
};
121120

122121
return (
@@ -125,6 +124,7 @@ const NavigationUserMenu = () => {
125124
authenticated={session.status === "authenticated"}
126125
label={t("common:login")}
127126
onSignIn={handleSignIn}
127+
closeOnItemClick
128128
$active={isActive}
129129
>
130130
{user ? (

ui/components/reservation/ReservationConfirmation.tsx

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
import { ReservationState } from "common/types/common";
2-
import { IconArrowRight, IconCalendar, IconSignout } from "hds-react";
2+
import {
3+
IconArrowRight,
4+
IconCalendar,
5+
IconLinkExternal,
6+
IconSignout,
7+
} from "hds-react";
38
import { useRouter } from "next/router";
49
import React, { useMemo } from "react";
510
import { Trans, useTranslation } from "next-i18next";
@@ -8,19 +13,24 @@ import styled from "styled-components";
813
import { fontMedium, fontRegular, H2 } from "common/src/common/typography";
914
import { Reservation } from "common/src/reservation-form/types";
1015
import {
16+
PaymentOrderType,
1117
ReservationsReservationStateChoices,
18+
ReservationType,
1219
ReservationUnitType,
1320
} from "common/types/gql-types";
1421
import { Subheading } from "common/src/reservation-form/styles";
22+
import { breakpoints } from "common/src/common/style";
1523
import { getReservationUnitInstructionsKey } from "../../modules/reservationUnit";
1624
import { getTranslation, reservationsUrl } from "../../modules/util";
17-
import { BlackButton } from "../../styles/util";
25+
import { BlackButton, LinkButton } from "../../styles/util";
1826
import { Paragraph } from "./styles";
1927
import { reservationUnitPath } from "../../modules/const";
28+
import { useLogout } from "../../hooks/useLogout";
2029

2130
type Props = {
22-
reservation: Reservation;
31+
reservation: Reservation | ReservationType;
2332
reservationUnit: ReservationUnitType;
33+
order?: PaymentOrderType;
2434
};
2535

2636
const Wrapper = styled.div`
@@ -33,6 +43,15 @@ const ActionContainer1 = styled.div`
3343
margin: var(--spacing-m) 0 var(--spacing-l);
3444
display: flex;
3545
gap: var(--spacing-m);
46+
flex-direction: column;
47+
48+
> button {
49+
max-width: 20rem;
50+
}
51+
52+
@media (min-width: ${breakpoints.l}) {
53+
flex-direction: row;
54+
}
3655
`;
3756

3857
const ActionContainer2 = styled.div`
@@ -63,9 +82,11 @@ const InlineStyledLink = styled(Link)`
6382
const ReservationConfirmation = ({
6483
reservation,
6584
reservationUnit,
85+
order,
6686
}: Props): JSX.Element => {
6787
const { t } = useTranslation();
6888
const router = useRouter();
89+
const { logout } = useLogout();
6990

7091
const instructionsKey = useMemo(
7192
() =>
@@ -94,7 +115,7 @@ const ReservationConfirmation = ({
94115
requiresHandling ? "Handling" : ""
95116
}`}
96117
t={t}
97-
values={{ user: reservation?.user.email }}
118+
values={{ user: reservation?.user?.email }}
98119
components={{
99120
br: <br />,
100121
lnk: (
@@ -118,6 +139,16 @@ const ReservationConfirmation = ({
118139
>
119140
{t("reservations:saveToCalendar")}
120141
</BlackButton>
142+
{order?.receiptUrl && (
143+
<BlackButton
144+
data-testid="reservation__confirmation--button__receipt-link"
145+
onClick={() => window.open(order.receiptUrl, "_blank")}
146+
variant="secondary"
147+
iconRight={<IconLinkExternal aria-hidden />}
148+
>
149+
{t("reservations:downloadReceipt")}
150+
</BlackButton>
151+
)}
121152
</ActionContainer1>
122153
)}
123154
{getTranslation(reservationUnit, instructionsKey) && (
@@ -141,9 +172,9 @@ const ReservationConfirmation = ({
141172
{t("common:gotoFrontpage")}
142173
<IconArrowRight aria-hidden size="m" />
143174
</StyledLink>
144-
<StyledLink href={reservationsUrl}>
175+
<LinkButton onClick={() => logout()}>
145176
{t("common:logout")} <IconSignout size="m" aria-hidden />
146-
</StyledLink>
177+
</LinkButton>
147178
</ActionContainer2>
148179
</div>
149180
</Wrapper>
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { breakpoints } from "common/src/common/style";
2+
import { fontMedium, H2 } from "common/src/common/typography";
3+
import { IconArrowRight, IconSignout } from "hds-react";
4+
import Link from "next/link";
5+
import React from "react";
6+
import { useTranslation } from "react-i18next";
7+
import styled from "styled-components";
8+
import Container from "../common/Container";
9+
import { Paragraph } from "./styles";
10+
import { singleSearchUrl } from "../../modules/util";
11+
import { useLogout } from "../../hooks/useLogout";
12+
import { LinkButton } from "../../styles/util";
13+
14+
type Props = {
15+
type: "reservation" | "order";
16+
};
17+
18+
const StyledContainer = styled(Container)`
19+
padding: var(--spacing-m) var(--spacing-m) var(--spacing-layout-m);
20+
21+
@media (min-width: ${breakpoints.m}) {
22+
max-width: 1000px;
23+
margin-bottom: var(--spacing-layout-l);
24+
}
25+
`;
26+
27+
const Wrapper = styled.div`
28+
align-items: flex-start;
29+
`;
30+
31+
const Heading = styled(H2).attrs({ as: "h1" })``;
32+
33+
const ActionContainer = styled.div`
34+
display: flex;
35+
gap: var(--spacing-m);
36+
flex-direction: column;
37+
align-items: flex-start;
38+
`;
39+
40+
const StyledLink = styled(Link)`
41+
display: flex;
42+
align-items: center;
43+
gap: var(--spacing-2-xs);
44+
text-decoration: underline;
45+
color: var(--color-black) !important;
46+
${fontMedium}
47+
`;
48+
49+
const ReservationFail = ({ type }: Props) => {
50+
const { t } = useTranslation();
51+
const { logout } = useLogout();
52+
53+
const headingKey =
54+
type === "reservation"
55+
? "reservationExpired"
56+
: type === "order"
57+
? "orderInvalid"
58+
: null;
59+
60+
return (
61+
<StyledContainer>
62+
<Wrapper>
63+
<div>
64+
<Heading>{t(`reservations:${headingKey}`)}</Heading>
65+
{type === "reservation" && (
66+
<Paragraph style={{ margin: "var(--spacing-xl) 0" }}>
67+
{t("reservations:reservationExpiredDescription")}
68+
</Paragraph>
69+
)}
70+
<ActionContainer
71+
style={{
72+
marginTop: "var(--spacing-3-xl)",
73+
}}
74+
>
75+
<StyledLink href={singleSearchUrl({})}>
76+
{t("reservations:backToSearch")}
77+
<IconArrowRight aria-hidden size="m" />
78+
</StyledLink>
79+
<StyledLink href="/">
80+
{t("common:gotoFrontpage")}
81+
<IconArrowRight aria-hidden size="m" />
82+
</StyledLink>
83+
<LinkButton onClick={() => logout()}>
84+
{t("common:logout")} <IconSignout size="m" aria-hidden />
85+
</LinkButton>
86+
</ActionContainer>
87+
</div>
88+
</Wrapper>
89+
</StyledContainer>
90+
);
91+
};
92+
93+
export default ReservationFail;

ui/components/reservation/ReservationInfoCard.tsx

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ const ReservationInfoCard = ({
152152
[i18n.language]
153153
);
154154

155+
if (!reservation || !reservationUnit) return null;
156+
155157
return (
156158
<Wrapper $type={type}>
157159
{mainImage?.mediumUrl && (
@@ -168,7 +170,7 @@ const ReservationInfoCard = ({
168170
</Heading>
169171
{["confirmed", "complete"].includes(type) && (
170172
<Subheading>
171-
{t("reservations:reservationNumber")}: {reservation?.pk}
173+
{t("reservations:reservationNumber")}: {reservation.pk}
172174
</Subheading>
173175
)}
174176
<Subheading>{getTranslation(reservationUnit.unit, "name")}</Subheading>
@@ -177,11 +179,11 @@ const ReservationInfoCard = ({
177179
{capitalize(timeString)}, {formatDurationMinutes(duration)}
178180
</Strong>
179181
</Value>
180-
{reservation?.description &&
182+
{reservation.description &&
181183
["confirmed", "complete"].includes(type) && (
182184
<Value>
183185
{t("reservationCalendar:label.description")}:{" "}
184-
{reservation?.description}
186+
{reservation.description}
185187
</Value>
186188
)}
187189
<Value>
@@ -190,7 +192,7 @@ const ReservationInfoCard = ({
190192
shouldDisplayTaxPercentage &&
191193
`(${t("common:inclTax", {
192194
taxPercentage: formatters.strippedDecimal.format(
193-
reservation?.taxPercentageValue
195+
reservation.taxPercentageValue
194196
),
195197
})})`}
196198
</Value>
@@ -204,9 +206,9 @@ const ReservationInfoCard = ({
204206
{t("reservations:ageGroup")}: {ageGroup}
205207
</Value>
206208
)}
207-
{reservation?.numPersons > 0 && ["complete"].includes(type) && (
209+
{reservation.numPersons > 0 && ["complete"].includes(type) && (
208210
<Value>
209-
{t("reservations:numPersons")}: {reservation?.numPersons}
211+
{t("reservations:numPersons")}: {reservation.numPersons}
210212
</Value>
211213
)}
212214
</Content>

ui/cypress/e2e/reservations.cy.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,7 @@ describe("Tilavaraus user reservations", () => {
152152
cy.contains("div", "Payment terms FI").should("be.visible");
153153
cy.contains("div", "Cancellation terms FI").should("be.visible");
154154

155-
cy.contains("div", "Pricing terms FI").should("not.be.visible");
156-
accordionToggler("reservation__pricing-terms").click();
157-
cy.contains("div", "Pricing terms FI").should("be.visible");
155+
cy.contains("div", "Pricing terms FI").should("not.exist");
158156

159157
cy.contains("div", "Sopparijuttuja").should("not.be.visible");
160158
cy.contains("div", "Toinen rivi").should("not.be.visible");

ui/hooks/useLogout.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useRouter } from "next/router";
2+
import { useLocalStorage } from "react-use";
3+
import { authenticationLogoutApiRoute } from "../modules/const";
4+
5+
type Output = {
6+
shouldLogout: boolean;
7+
removeShouldLogout: () => void;
8+
logout: () => void;
9+
};
10+
11+
export const useLogout = (): Output => {
12+
const router = useRouter();
13+
const [shouldLogout, setShouldLogout, removeShouldLogout] = useLocalStorage(
14+
"shouldLogout",
15+
false
16+
);
17+
18+
const logout = () => {
19+
setShouldLogout(true);
20+
router.push(authenticationLogoutApiRoute);
21+
};
22+
23+
return { shouldLogout, removeShouldLogout, logout };
24+
};

ui/mocks/handlers/reservation.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,9 +456,10 @@ const reservationByPk = graphql.query<Query, QueryReservationUnitByPkArgs>(
456456

457457
if (pk === 4) {
458458
data.price = 0;
459-
data.begin = addDays(new Date(), 10).toISOString();
460-
data.end = addHours(addDays(new Date(), 10), 2).toISOString();
459+
data.begin = addDays(new Date(), 11).toISOString();
460+
data.end = addHours(addDays(new Date(), 11), 2).toISOString();
461461
data.reservationUnits[0].pk = 888;
462+
data.reservationUnits[0].canApplyFreeOfCharge = true;
462463
data.reservationUnits[0].cancellationRule = {
463464
id: "234",
464465
canBeCancelledTimeBefore: 0,

ui/modules/queries/reservation.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ export const GET_RESERVATION = gql`
205205
priceNet
206206
taxPercentageValue
207207
orderStatus
208+
orderUuid
208209
reservationUnits {
209210
pk
210211
nameFi
@@ -362,3 +363,22 @@ export const ADJUST_RESERVATION_TIME = gql`
362363
}
363364
}
364365
`;
366+
367+
export const GET_ORDER = gql`
368+
query order($orderUuid: String!) {
369+
order(orderUuid: $orderUuid) {
370+
reservationPk
371+
status
372+
paymentType
373+
}
374+
}
375+
`;
376+
377+
export const REFRESH_ORDER = gql`
378+
mutation refreshOrder($input: RefreshOrderMutationInput!) {
379+
refreshOrder(input: $input) {
380+
orderUuid
381+
status
382+
}
383+
}
384+
`;

ui/modules/util.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ const imagePriority = ["main", "map", "ground_plan", "other"].map((n) =>
322322
export const getMainImage = (
323323
ru: ReservationUnit | ReservationUnitType | ReservationUnitByPkType
324324
): Image | ReservationUnitImageType | null => {
325-
if (!ru.images || ru.images.length === 0) {
325+
if (!ru || !ru.images || ru.images.length === 0) {
326326
return null;
327327
}
328328
const images = [...ru.images].sort((a, b) => {

0 commit comments

Comments
 (0)