Skip to content

Commit

Permalink
add: cancel all reservations in application section
Browse files Browse the repository at this point in the history
  • Loading branch information
joonatank committed Dec 18, 2024
1 parent cfe8618 commit 2d1392e
Show file tree
Hide file tree
Showing 35 changed files with 1,124 additions and 261 deletions.
11 changes: 11 additions & 0 deletions apps/admin-ui/gql/gql-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ export type ApplicationSectionNode = Node & {
allocations?: Maybe<Scalars["Int"]["output"]>;
application: ApplicationNode;
appliedReservationsPerWeek: Scalars["Int"]["output"];
extUuid: Scalars["UUID"]["output"];
/** The ID of the object */
id: Scalars["ID"]["output"];
name: Scalars["String"]["output"];
Expand Down Expand Up @@ -1777,6 +1778,7 @@ export type Query = {
application?: Maybe<ApplicationNode>;
applicationRound?: Maybe<ApplicationRoundNode>;
applicationRounds?: Maybe<ApplicationRoundNodeConnection>;
applicationSection?: Maybe<ApplicationSectionNode>;
applicationSections?: Maybe<ApplicationSectionNodeConnection>;
applications?: Maybe<ApplicationNodeConnection>;
bannerNotification?: Maybe<BannerNotificationNode>;
Expand Down Expand Up @@ -1908,6 +1910,10 @@ export type QueryApplicationRoundsArgs = {
pk?: InputMaybe<Array<InputMaybe<Scalars["Int"]["input"]>>>;
};

export type QueryApplicationSectionArgs = {
id: Scalars["ID"]["input"];
};

export type QueryApplicationSectionsArgs = {
after?: InputMaybe<Scalars["String"]["input"]>;
ageGroup?: InputMaybe<Array<InputMaybe<Scalars["Int"]["input"]>>>;
Expand Down Expand Up @@ -3638,6 +3644,7 @@ export type ReservationUnitCreateMutationInput = {
reservationsMaxDaysBefore?: InputMaybe<Scalars["Int"]["input"]>;
reservationsMinDaysBefore?: InputMaybe<Scalars["Int"]["input"]>;
resources?: InputMaybe<Array<InputMaybe<Scalars["Int"]["input"]>>>;
searchTerms?: InputMaybe<Array<InputMaybe<Scalars["String"]["input"]>>>;
serviceSpecificTerms?: InputMaybe<Scalars["String"]["input"]>;
spaces?: InputMaybe<Array<InputMaybe<Scalars["Int"]["input"]>>>;
surfaceArea?: InputMaybe<Scalars["Int"]["input"]>;
Expand Down Expand Up @@ -3709,6 +3716,7 @@ export type ReservationUnitCreateMutationPayload = {
reservationsMaxDaysBefore?: Maybe<Scalars["Int"]["output"]>;
reservationsMinDaysBefore?: Maybe<Scalars["Int"]["output"]>;
resources?: Maybe<Array<Maybe<Scalars["Int"]["output"]>>>;
searchTerms?: Maybe<Array<Maybe<Scalars["String"]["output"]>>>;
serviceSpecificTerms?: Maybe<Scalars["String"]["output"]>;
spaces?: Maybe<Array<Maybe<Scalars["Int"]["output"]>>>;
surfaceArea?: Maybe<Scalars["Int"]["output"]>;
Expand Down Expand Up @@ -3846,6 +3854,7 @@ export type ReservationUnitNode = Node & {
reservationsMaxDaysBefore?: Maybe<Scalars["Int"]["output"]>;
reservationsMinDaysBefore?: Maybe<Scalars["Int"]["output"]>;
resources: Array<ResourceNode>;
searchTerms: Array<Scalars["String"]["output"]>;
serviceSpecificTerms?: Maybe<TermsOfUseNode>;
spaces: Array<SpaceNode>;
surfaceArea?: Maybe<Scalars["Int"]["output"]>;
Expand Down Expand Up @@ -4221,6 +4230,7 @@ export type ReservationUnitUpdateMutationInput = {
reservationsMaxDaysBefore?: InputMaybe<Scalars["Int"]["input"]>;
reservationsMinDaysBefore?: InputMaybe<Scalars["Int"]["input"]>;
resources?: InputMaybe<Array<InputMaybe<Scalars["Int"]["input"]>>>;
searchTerms?: InputMaybe<Array<InputMaybe<Scalars["String"]["input"]>>>;
serviceSpecificTerms?: InputMaybe<Scalars["String"]["input"]>;
spaces?: InputMaybe<Array<InputMaybe<Scalars["Int"]["input"]>>>;
surfaceArea?: InputMaybe<Scalars["Int"]["input"]>;
Expand Down Expand Up @@ -4292,6 +4302,7 @@ export type ReservationUnitUpdateMutationPayload = {
reservationsMaxDaysBefore?: Maybe<Scalars["Int"]["output"]>;
reservationsMinDaysBefore?: Maybe<Scalars["Int"]["output"]>;
resources?: Maybe<Array<Maybe<Scalars["Int"]["output"]>>>;
searchTerms?: Maybe<Array<Maybe<Scalars["String"]["output"]>>>;
serviceSpecificTerms?: Maybe<Scalars["String"]["output"]>;
spaces?: Maybe<Array<Maybe<Scalars["Int"]["output"]>>>;
surfaceArea?: Maybe<Scalars["Int"]["output"]>;
Expand Down
17 changes: 2 additions & 15 deletions apps/admin-ui/src/spa/ReservationUnit/edit/form.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { filterNonNullable, toNumber } from "common/src/helpers";
import { convertTime, filterNonNullable, toNumber } from "common/src/helpers";
import {
fromApiDate,
fromUIDate,
Expand Down Expand Up @@ -761,19 +761,6 @@ function convertImage(image?: Node["images"][0]): ImageFormType {
};
}

/// Primary use case is to clip out seconds from backend time strings
/// Assumed only to be used for backend time strings which are in format HH:MM or HH:MM:SS
/// NOTE does not handle incorrect time strings (ex. bar:foo)
/// NOTE does not have any boundary checks (ex. 25:99 is allowed)
const convertTime = (t?: string) => {
if (t == null || t === "") {
return "";
}
// NOTE split has incorrect typing
const [h, m, _]: Array<string | undefined> = t.split(":");
return `${h ?? "00"}:${m ?? "00"}`;
};

// Always return all 7 days
// Always return at least one reservableTime
function convertSeasonalList(
Expand All @@ -785,7 +772,7 @@ function convertSeasonalList(

const times = filterNonNullable(season?.reservableTimes).map((rt) => ({
begin: convertTime(rt.begin),
end: convertTime(rt?.end),
end: convertTime(rt.end),
}));
return {
pk: season?.pk ?? 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,8 +383,8 @@ function TimeRequested({
const durationString = createDurationString(applicationSection, t);

const aes = filterNonNullable(applicationSection?.suitableTimeRanges);
const primaryTimes = formatTimeRangeList(aes, Priority.Primary);
const secondaryTimes = formatTimeRangeList(aes, Priority.Secondary);
const primaryTimes = formatTimeRangeList(t, aes, Priority.Primary);
const secondaryTimes = formatTimeRangeList(t, aes, Priority.Secondary);

return (
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
ApplicationSectionAllocationsQuery,
ApplicationRoundFilterQuery,
} from "@gql/gql-types";
import i18next from "i18next";
import { type TFunction } from "next-i18next";
import { filterNonNullable } from "common/src/helpers";
import { formatDuration } from "common/src/common/util";
Expand Down Expand Up @@ -151,11 +150,11 @@ export function parseApiTime(time: string): number | null {
return h1 + m1 / 60;
}

export const getTimeSeries = (
export function getTimeSeries(
day: string,
begin: string,
end: string
): string[] => {
): string[] {
const [, startHours, startMinutes] = begin.split("-").map(Number);
const [, endHours, endMinutes] = end.split("-").map(Number);
const timeSlots: string[] = [];
Expand All @@ -168,22 +167,23 @@ export const getTimeSeries = (
if (endMinutes === 0) timeSlots.pop();

return timeSlots;
};
}

// TODO is this parse? or format? it looks like a format
function formatTimeRange(
t: TFunction,
range: Pick<SuitableTimeRangeNode, "dayOfTheWeek" | "beginTime" | "endTime">
): string {
// TODO convert the day of the week
const day = convertWeekday(range.dayOfTheWeek);
const weekday = i18next.t(`dayShort.${day}`);
const weekday = t(`dayShort.${day}`);
// TODO don't use substring to convert times (wrap it in a function)
return `${weekday} ${Number(
range.beginTime.substring(0, 2)
)}-${Number(range.endTime.substring(0, 2))}`;
}

export function formatTimeRangeList(
t: TFunction,
aes: Pick<
SuitableTimeRangeNode,
"dayOfTheWeek" | "beginTime" | "endTime" | "priority"
Expand All @@ -196,7 +196,7 @@ export function formatTimeRangeList(
);

return filterNonNullable(schedules)
.map((schedule) => formatTimeRange(schedule))
.map((schedule) => formatTimeRange(t, schedule))
.join(", ");
}

Expand Down
119 changes: 119 additions & 0 deletions apps/ui/components/CancellationForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import React, { useEffect } from "react";
import styled from "styled-components";
import { useForm } from "react-hook-form";
import { Button, IconCross } from "hds-react";
import { useTranslation } from "next-i18next";
import { fontMedium } from "common/src/common/typography";
import { type CancelReasonFieldsFragment } from "@gql/gql-types";
import Sanitize from "./common/Sanitize";
import {
convertLanguageCode,
getTranslationSafe,
} from "common/src/common/util";
import { ControlledSelect } from "common/src/components/form";
import { AutoGrid, ButtonContainer, Flex } from "common/styles/util";
import { ButtonLikeLink } from "./common/ButtonLikeLink";
import TermsBox from "common/src/termsbox/TermsBox";
import { AccordionWithState } from "./Accordion";
import { breakpoints } from "common";

const Actions = styled(ButtonContainer).attrs({
$justifyContent: "space-between",
})`
grid-column: 1 / -1;
`;

const Form = styled.form`
label {
${fontMedium};
}
`;

export type CancelFormValues = {
reason: number;
};

const FormWrapper = styled(Flex)`
@media (min-width: ${breakpoints.m}) {
grid-row: 2 / -1;
grid-column: 1;
}
`;

export function CancellationForm(props: {
onNext: (values: CancelFormValues) => void;
cancelReasons: CancelReasonFieldsFragment[];
cancellationTerms: string | null;
backLink: string;
isLoading?: boolean;
isDisabled?: boolean;
}): JSX.Element {
const {
cancelReasons,
onNext,
isLoading,
isDisabled,
cancellationTerms,
backLink,
} = props;
const { t, i18n } = useTranslation();
const lang = convertLanguageCode(i18n.language);

const reasons = cancelReasons.map((node) => ({
label: getTranslationSafe(node, "reason", lang),
value: node?.pk ?? 0,
}));

const form = useForm<CancelFormValues>();
const { register, handleSubmit, watch, control } = form;

// TODO can we remove this? should be auto registered when the form is created
// should we add zod schema for the required fields?
useEffect(() => {
register("reason", { required: true });
}, [register]);

return (
<FormWrapper>
{cancellationTerms != null && (
<AccordionWithState
heading={t("reservationUnit:cancellationTerms")}
disableBottomMargin
>
<TermsBox body={<Sanitize html={cancellationTerms} />} />
</AccordionWithState>
)}
<Form onSubmit={handleSubmit(onNext)}>
<AutoGrid>
<ControlledSelect
name="reason"
control={control}
label={t("reservations:cancel.reason")}
options={reasons}
required
disabled={isDisabled}
/>
<Actions>
<ButtonLikeLink
data-testid="reservation-cancel__button--back"
href={backLink}
size="large"
>
<IconCross aria-hidden="true" />
{t("reservations:cancelButton")}
</ButtonLikeLink>
<Button
variant="primary"
type="submit"
disabled={isDisabled || !watch("reason")}
data-testid="reservation-cancel__button--cancel"
isLoading={isLoading}
>
{t("reservations:cancel.reservation")}
</Button>
</Actions>
</AutoGrid>
</Form>
</FormWrapper>
);
}
24 changes: 10 additions & 14 deletions apps/ui/components/application/ApprovedReservations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import {
ReservationStateChoice,
} from "@/gql/gql-types";
import {
getApplicationPath,
getApplicationReservationPath,
getApplicationSectionPath,
getReservationUnitPath,
} from "@/modules/urls";
import { breakpoints, fontMedium, fontRegular, H5 } from "common";
import { errorToast } from "common/src/common/toast";
import {
getTranslationSafe,
toApiDate,
Expand Down Expand Up @@ -528,9 +527,7 @@ function ReservationsTable({
const router = useRouter();

const handleCancel = (pk: number) => {
const appPath = getApplicationPath(application.pk, "view");
const url = `${appPath}/${pk}/cancel`;
router.push(url);
router.push(getApplicationReservationPath(application.pk, pk));
};

const cols = [
Expand Down Expand Up @@ -805,17 +802,16 @@ export function ApplicationSection({
{t("application:view.reservationsTab.showAllReservations")}
<IconLinkExternal />
</ButtonLikeLink>
<Button
variant="secondary"
theme="black"
size="small"
onClick={() => {
errorToast({ text: "Not implemented: cancel application" });
}}
iconRight={<IconCross />}
<ButtonLikeLink
href={getApplicationSectionPath(
applicationSection.pk,
application.pk,
"cancel"
)}
>
{t("application:view.reservationsTab.cancelApplication")}
</Button>
<IconCross />
</ButtonLikeLink>
</ButtonContainer>
</Flex>
);
Expand Down
Loading

0 comments on commit 2d1392e

Please sign in to comment.