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

Commit 96d203b

Browse files
committed
add: mass reject application sections
1 parent 98d775f commit 96d203b

4 files changed

Lines changed: 141 additions & 7 deletions

File tree

apps/admin-ui/src/i18n/messages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,8 @@ const translations: ITranslations = {
418418
rejected: ["Hylätty"],
419419
btnReject: ["Hylkää"],
420420
btnRevert: ["Palauta"],
421+
btnRejectAll: ["Hylkää kaikki"],
422+
btnRestoreAll: ["Palauta kaikki"],
421423
headings: {
422424
id: ["id"],
423425
customer: ["Hakija"],

apps/admin-ui/src/spa/applications/[id]/index.tsx

Lines changed: 118 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import {
3030
type MutationUpdateReservationUnitOptionArgs,
3131
type ReservationUnitOptionNode,
3232
type Maybe,
33+
type MutationRejectAllSectionOptionsArgs,
34+
type MutationRestoreAllSectionOptionsArgs,
3335
} from "common/types/gql-types";
3436
import { formatDuration } from "common/src/common/util";
3537
import { convertWeekday, type Day } from "common/src/conversion";
@@ -49,7 +51,11 @@ import { BirthDate } from "@/component/BirthDate";
4951
import { Container } from "@/styles/layout";
5052
import { ValueBox } from "../ValueBox";
5153
import { TimeSelector } from "../TimeSelector";
52-
import { APPLICATION_ADMIN_QUERY } from "../queries";
54+
import {
55+
APPLICATION_ADMIN_QUERY,
56+
REJECT_ALL_SECTION_OPTIONS,
57+
RESTORE_ALL_SECTION_OPTIONS,
58+
} from "../queries";
5359
import { getApplicantName, getApplicationStatusColor } from "@/helpers";
5460
// TODO move
5561
import { UPDATE_RESERVATION_UNIT_OPTION } from "@/spa/recurring-reservations/application-rounds/[id]/allocation/queries";
@@ -156,9 +162,6 @@ const StyledAccordion = styled(Accordion).attrs({
156162
} as React.CSSProperties,
157163
})`
158164
margin-top: 48px;
159-
h4 {
160-
margin-top: 4rem;
161-
}
162165
`;
163166

164167
const PreCard = styled.div`
@@ -211,6 +214,7 @@ const HeadingContainer = styled.div`
211214
display: flex;
212215
justify-content: space-between;
213216
margin-top: var(--spacing-s);
217+
align-items: start;
214218
`;
215219

216220
const ApplicationSectionsContainer = styled.div`
@@ -429,6 +433,104 @@ function RejectOptionButton({
429433
);
430434
}
431435

436+
function RejectAllOptionsButton({
437+
section,
438+
applicationStatus,
439+
refetch,
440+
}: {
441+
section: ApplicationSectionNode;
442+
applicationStatus: ApplicationStatusChoice;
443+
refetch: () => Promise<ApolloQueryResult<Query>>;
444+
}) {
445+
const { t } = useTranslation();
446+
447+
const [rejectMutation, { loading: rejectLoading }] = useMutation<
448+
Query,
449+
MutationRejectAllSectionOptionsArgs
450+
>(REJECT_ALL_SECTION_OPTIONS);
451+
452+
const [restoreMutation, { loading: restoreLoading }] = useMutation<
453+
Query,
454+
MutationRestoreAllSectionOptionsArgs
455+
>(RESTORE_ALL_SECTION_OPTIONS);
456+
457+
const { notifyError } = useNotification();
458+
459+
const isLoading = rejectLoading || restoreLoading;
460+
461+
const mutate = async (pk: Maybe<number> | undefined, restore: boolean) => {
462+
if (pk == null) {
463+
// TODO this is an error
464+
return;
465+
}
466+
if (isLoading) {
467+
return;
468+
}
469+
try {
470+
const mutation = restore ? restoreMutation : rejectMutation;
471+
await mutation({
472+
variables: {
473+
input: {
474+
pk,
475+
},
476+
},
477+
});
478+
refetch();
479+
} catch (err) {
480+
const mutationErrors = getValidationErrors(err);
481+
if (mutationErrors.length > 0) {
482+
// TODO handle other codes also
483+
const isInvalidState = mutationErrors.find(
484+
(e) => e.code === "CANNOT_REJECT_SECTION_OPTIONS"
485+
);
486+
if (isInvalidState) {
487+
notifyError(t("errors.cantRejectAlreadyAllocated"));
488+
} else {
489+
// TODO this should show them with cleaner formatting (multiple errors)
490+
// TODO these should be translated
491+
const message = mutationErrors.map((e) => e.message).join(", ");
492+
notifyError(t("errors.formValidationError", { message }));
493+
}
494+
} else {
495+
notifyError(t("errors.errorRejectingOption"));
496+
}
497+
}
498+
};
499+
500+
const handleRejectAll = async () => {
501+
mutate(section.pk, false);
502+
};
503+
504+
const handleRestoreAll = async () => {
505+
mutate(section.pk, true);
506+
};
507+
508+
// codegen types allow undefined so have to do this for debugging
509+
if (section.allocations == null) {
510+
// eslint-disable-next-line no-console
511+
console.warn("section.allocations is null", section);
512+
}
513+
514+
const inAllocation =
515+
applicationStatus === ApplicationStatusChoice.InAllocation;
516+
const isRejected = section.reservationUnitOptions.every((x) => x.rejected);
517+
const hasAllocations = section.allocations != null && section.allocations > 0;
518+
const canReject = inAllocation && !hasAllocations;
519+
return (
520+
<Button
521+
disabled={!canReject}
522+
size="small"
523+
variant="secondary"
524+
onClick={() => (isRejected ? handleRestoreAll() : handleRejectAll())}
525+
isLoading={isLoading}
526+
>
527+
{isRejected
528+
? t("Application.btnRestoreAll")
529+
: t("Application.btnRejectAll")}
530+
</Button>
531+
);
532+
}
533+
432534
interface DataType extends ReservationUnitOptionNode {
433535
index: number;
434536
}
@@ -551,7 +653,16 @@ function ApplicationSectionDetails({
551653
/>
552654
<ValueBox label={t("ApplicationEvent.dates")} value={dates} />
553655
</EventProps>
554-
<H4>{t("ApplicationEvent.requestedReservationUnits")}</H4>
656+
<HeadingContainer style={{ alignItems: "center" }}>
657+
<H4 as="h3">{t("ApplicationEvent.requestedReservationUnits")}</H4>
658+
<RejectAllOptionsButton
659+
section={section}
660+
refetch={refetch}
661+
applicationStatus={
662+
application.status ?? ApplicationStatusChoice.Draft
663+
}
664+
/>
665+
</HeadingContainer>
555666
<ApplicationSectionsContainer>
556667
{rows.map((row) => (
557668
<div style={{ display: "contents" }} key={row.pk}>
@@ -561,7 +672,7 @@ function ApplicationSectionDetails({
561672
</div>
562673
))}
563674
</ApplicationSectionsContainer>
564-
<H4>{t("ApplicationEvent.requestedTimes")}</H4>
675+
<H4 as="h3">{t("ApplicationEvent.requestedTimes")}</H4>
565676
<EventSchedules>
566677
<TimeSelector applicationSection={section} />
567678
<Card
@@ -667,7 +778,7 @@ function ApplicationDetails({
667778
</ShowWhenTargetInvisible>
668779
<Container>
669780
<HeadingContainer>
670-
<H2 ref={ref} style={{ margin: "0" }}>
781+
<H2 as="h1" ref={ref} style={{ margin: "0" }}>
671782
{customerName}
672783
</H2>
673784
{application.status != null && (

apps/admin-ui/src/spa/applications/queries.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,23 @@ export const APPLICATION_ADMIN_QUERY = gql`
1111
}
1212
}
1313
`;
14+
15+
export const REJECT_ALL_SECTION_OPTIONS = gql`
16+
mutation rejectAllSectionOptions(
17+
$input: RejectAllSectionOptionsMutationInput!
18+
) {
19+
rejectAllSectionOptions(input: $input) {
20+
pk
21+
}
22+
}
23+
`;
24+
25+
export const RESTORE_ALL_SECTION_OPTIONS = gql`
26+
mutation restoreAllSectionOptions(
27+
$input: RestoreAllSectionOptionsMutationInput!
28+
) {
29+
restoreAllSectionOptions(input: $input) {
30+
pk
31+
}
32+
}
33+
`;

packages/common/src/queries/application.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ export const APPLICATION_ADMIN_FRAGMENT = gql`
212212
}
213213
applicationSections {
214214
...ApplicationSectionUIFragment
215+
allocations
215216
reservationUnitOptions {
216217
rejected
217218
allocatedTimeSlots {

0 commit comments

Comments
 (0)