Merged in feat/SW-1371-delete-ancillary (pull request #1455)

feat(SW-1371): delete ancillary

* feat(SW-1371): delete ancillary

* Remove outline from dialog

* Consistent return type from mutation

* Error flow


Approved-by: Michael Zetterberg
Approved-by: Pontus Dreij
This commit is contained in:
Niclas Edenvin
2025-03-04 14:22:15 +00:00
parent f80c2e9583
commit c88aa92aba
12 changed files with 211 additions and 48 deletions

View File

@@ -29,6 +29,10 @@
} }
} }
.dialog {
outline: none;
}
.modal { .modal {
background-color: var(--Base-Surface-Primary-light-Normal); background-color: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Corner-radius-Medium); border-radius: var(--Corner-radius-Medium);

View File

@@ -31,7 +31,7 @@ export default function Dialog({
{trigger} {trigger}
<ModalOverlay className={styles.overlay} isDismissable> <ModalOverlay className={styles.overlay} isDismissable>
<Modal> <Modal>
<AriaDialog role="alertdialog"> <AriaDialog role="alertdialog" className={styles.dialog}>
{({ close }) => ( {({ close }) => (
<section className={styles.modal}> <section className={styles.modal}>
<header className={styles.header}> <header className={styles.header}>

View File

@@ -0,0 +1,65 @@
import { useIntl } from "react-intl"
import { trpc } from "@/lib/trpc/client"
import Dialog from "@/components/Dialog"
import { DeleteIcon } from "@/components/Icons"
import Button from "@/components/TempDesignSystem/Button"
import { toast } from "@/components/TempDesignSystem/Toasts"
import useLang from "@/hooks/useLang"
export default function RemoveButton({
confirmationNumber,
code,
title,
onSuccess,
}: {
confirmationNumber: string
code: string
title?: string
onSuccess: () => void
}) {
const lang = useLang()
const intl = useIntl()
const removePackage = trpc.booking.removePackage.useMutation()
return (
<Dialog
bodyText={intl.formatMessage({
id: "Are you sure you want to remove this product?",
})}
proceedText={intl.formatMessage({ id: "Remove" })}
proceedIsPending={removePackage.isPending}
cancelButtonText={intl.formatMessage({ id: "Cancel" })}
titleText={`${intl.formatMessage({ id: "Remove" })} ${title}`}
trigger={
<Button intent="text" size="small" variant="icon" theme="base">
<DeleteIcon />
{intl.formatMessage({ id: "Remove" })}
</Button>
}
proceedOnClick={(close) => {
removePackage.mutate(
{
language: lang,
confirmationNumber,
codes: [code],
},
{
onSuccess: (data) => {
if (!data) {
throw new Error()
}
close()
onSuccess()
},
onError: () => {
toast.error(intl.formatMessage({ id: "Something went wrong!" }))
},
}
)
}}
/>
)
}

View File

@@ -1,6 +1,7 @@
import { useRouter } from "next/navigation"
import { useIntl } from "react-intl" import { useIntl } from "react-intl"
import { CheckCircleIcon, DeleteIcon, EditIcon } from "@/components/Icons" import { CheckCircleIcon, EditIcon } from "@/components/Icons"
import Accordion from "@/components/TempDesignSystem/Accordion" import Accordion from "@/components/TempDesignSystem/Accordion"
import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem" import AccordionItem from "@/components/TempDesignSystem/Accordion/AccordionItem"
import Button from "@/components/TempDesignSystem/Button" import Button from "@/components/TempDesignSystem/Button"
@@ -8,6 +9,8 @@ import Divider from "@/components/TempDesignSystem/Divider"
import Body from "@/components/TempDesignSystem/Text/Body" import Body from "@/components/TempDesignSystem/Text/Body"
import Subtitle from "@/components/TempDesignSystem/Text/Subtitle" import Subtitle from "@/components/TempDesignSystem/Text/Subtitle"
import RemoveButton from "./RemoveButton"
import styles from "./addedAncillaries.module.css" import styles from "./addedAncillaries.module.css"
import type { AddedAncillariesProps } from "@/types/components/myPages/myStay/ancillaries" import type { AddedAncillariesProps } from "@/types/components/myPages/myStay/ancillaries"
@@ -17,6 +20,7 @@ export function AddedAncillaries({
booking, booking,
}: AddedAncillariesProps) { }: AddedAncillariesProps) {
const intl = useIntl() const intl = useIntl()
const router = useRouter()
return ( return (
<div className={styles.container}> <div className={styles.container}>
@@ -76,30 +80,29 @@ export function AddedAncillaries({
</div> </div>
<div className={styles.footerMobile}> <div className={styles.footerMobile}>
<div className={styles.actions}> {booking.confirmationNumber && ancillary.code ? (
<Button <div className={styles.actions}>
intent="text" <Button
size="small" intent="text"
variant="icon" size="small"
theme="base" variant="icon"
> theme="base"
<EditIcon /> >
{intl.formatMessage({ id: "Modify" })} <EditIcon />
</Button> {intl.formatMessage({ id: "Modify" })}
<Divider </Button>
variant="vertical" <Divider
color="baseSurfaceSubtleNormal" variant="vertical"
/> color="baseSurfaceSubtleNormal"
<Button />
intent="text" <RemoveButton
size="small" confirmationNumber={booking.confirmationNumber}
variant="icon" code={ancillary.code}
theme="base" title={ancillaryItem?.title}
> onSuccess={() => router.refresh()}
<DeleteIcon /> />
{intl.formatMessage({ id: "Remove" })} </div>
</Button> ) : null}
</div>
</div> </div>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
@@ -135,27 +138,29 @@ export function AddedAncillaries({
</> </>
)} )}
</div> </div>
<div className={styles.actions}> {booking.confirmationNumber && ancillary.code ? (
<Button <div className={styles.actions}>
intent="text" <Button
size="small" intent="text"
variant="icon" size="small"
theme="base" variant="icon"
> theme="base"
<EditIcon /> >
{intl.formatMessage({ id: "Modify" })} <EditIcon />
</Button> {intl.formatMessage({ id: "Modify" })}
<Divider variant="vertical" color="baseSurfaceSubtleNormal" /> </Button>
<Button <Divider
intent="text" variant="vertical"
size="small" color="baseSurfaceSubtleNormal"
variant="icon" />
theme="base" <RemoveButton
> confirmationNumber={booking.confirmationNumber}
<DeleteIcon /> code={ancillary.code}
{intl.formatMessage({ id: "Remove" })} title={ancillaryItem?.title}
</Button> onSuccess={() => router.refresh()}
</div> />
</div>
) : null}
</div> </div>
</div> </div>
</> </>

View File

@@ -1,5 +1,6 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+46 8 517 517 00": "+46 8 517 517 00",
"Are you sure you want to remove this product?": "Er du sikker på, at du vil fjerne dette produkt?",
"/night per adult": "/nat per voksen", "/night per adult": "/nat per voksen",
"<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)", "<b>Included</b> (based on availability)": "<b>Inkluderet</b> (baseret på tilgængelighed)",
"<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)", "<b>Total price</b> (incl VAT)": "<b>Samlet pris</b> (inkl. moms)",

View File

@@ -1,5 +1,6 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+46 8 517 517 00": "+46 8 517 517 00",
"Are you sure you want to remove this product?": "Möchten Sie dieses Produkt wirklich entfernen?",
"/night per adult": "/Nacht pro Erwachsenem", "/night per adult": "/Nacht pro Erwachsenem",
"<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)", "<b>Included</b> (based on availability)": "<b>Inbegriffen</b> (je nach Verfügbarkeit)",
"<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)", "<b>Total price</b> (incl VAT)": "<b>Gesamtpreis</b> (inkl. MwSt.)",

View File

@@ -1,4 +1,5 @@
{ {
"Are you sure you want to remove this product?": "Are you sure you want to remove this product?",
"+46 8 517 517 00": "+46 8 517 517 00", "+46 8 517 517 00": "+46 8 517 517 00",
"/night per adult": "/night per adult", "/night per adult": "/night per adult",
"<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)", "<b>Included</b> (based on availability)": "<b>Included</b> (based on availability)",

View File

@@ -1,5 +1,6 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+46 8 517 517 00": "+46 8 517 517 00",
"Are you sure you want to remove this product?": "Haluatko varmasti poistaa tämän tuotteen?",
"/night per adult": "/yötä aikuista kohti", "/night per adult": "/yötä aikuista kohti",
"<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)", "<b>Included</b> (based on availability)": "<b>Sisältyy</b> (saatavuuden mukaan)",
"<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)", "<b>Total price</b> (incl VAT)": "<b>Kokonaishinta</b> (sis. ALV)",

View File

@@ -1,5 +1,6 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+46 8 517 517 00": "+46 8 517 517 00",
"Are you sure you want to remove this product?": "Er du sikker på at du vil fjerne dette produktet?",
"/night per adult": "/natt per voksen", "/night per adult": "/natt per voksen",
"<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)", "<b>Included</b> (based on availability)": "<b>Inkludert</b> (basert på tilgjengelighet)",
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)", "<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl. mva)",

View File

@@ -1,5 +1,6 @@
{ {
"+46 8 517 517 00": "+46 8 517 517 00", "+46 8 517 517 00": "+46 8 517 517 00",
"Are you sure you want to remove this product?": "Är du säker på att du vill ta bort den här produkten?",
"/night per adult": "/natt per vuxen", "/night per adult": "/natt per vuxen",
"<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)", "<b>Included</b> (based on availability)": "<b>Ingår</b> (baserat på tillgänglighet)",
"<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)", "<b>Total price</b> (incl VAT)": "<b>Totalpris</b> (inkl moms)",

View File

@@ -96,6 +96,12 @@ export const addPackageInput = z.object({
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]), language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
}) })
export const removePackageInput = z.object({
confirmationNumber: z.string(),
codes: z.array(z.string()),
language: z.nativeEnum(Lang).transform((val) => langToApiLang[val]),
})
export const priceChangeInput = z.object({ export const priceChangeInput = z.object({
confirmationNumber: z.string(), confirmationNumber: z.string(),
}) })

View File

@@ -7,10 +7,11 @@ import { router, safeProtectedServiceProcedure } from "@/server/trpc"
import { getMembership } from "@/utils/user" import { getMembership } from "@/utils/user"
import { import {
cancelBookingInput,
addPackageInput, addPackageInput,
cancelBookingInput,
createBookingInput, createBookingInput,
priceChangeInput, priceChangeInput,
removePackageInput,
} from "./input" } from "./input"
import { createBookingSchema } from "./output" import { createBookingSchema } from "./output"
@@ -48,6 +49,14 @@ const addPackageFailCounter = meter.createCounter(
"trpc.bookings.add-package-fail" "trpc.bookings.add-package-fail"
) )
const removePackageCounter = meter.createCounter("trpc.bookings.remove-package")
const removePackageSuccessCounter = meter.createCounter(
"trpc.bookings.remove-package-success"
)
const removePackageFailCounter = meter.createCounter(
"trpc.bookings.remove-package-fail"
)
async function getMembershipNumber( async function getMembershipNumber(
session: Session | null session: Session | null
): Promise<string | undefined> { ): Promise<string | undefined> {
@@ -379,4 +388,72 @@ export const bookingMutationRouter = router({
return verifiedData.data return verifiedData.data
}), }),
removePackage: safeProtectedServiceProcedure
.input(removePackageInput)
.mutation(async function ({ ctx, input }) {
const accessToken = ctx.session?.token.access_token ?? ctx.serviceToken
const { confirmationNumber, codes, language } = input
const headers = {
Authorization: `Bearer ${accessToken}`,
}
const loggingAttributes = {
confirmationNumber,
codes,
language,
}
removePackageCounter.add(1, loggingAttributes)
console.info(
"api.booking.remove-package start",
JSON.stringify({
request: loggingAttributes,
headers,
})
)
const apiResponse = await api.remove(
api.endpoints.v1.Booking.packages(confirmationNumber),
{
headers,
} as RequestInit,
{ language, codes }
)
if (!apiResponse.ok) {
const text = await apiResponse.text()
removePackageFailCounter.add(1, {
confirmationNumber,
error_type: "http_error",
error: JSON.stringify({
status: apiResponse.status,
}),
})
console.error(
"api.booking.remove-package error",
JSON.stringify({
error: {
status: apiResponse.status,
statusText: apiResponse.statusText,
text,
},
query: loggingAttributes,
})
)
return false
}
removePackageSuccessCounter.add(1, loggingAttributes)
console.info(
"api.booking.remove-package success",
JSON.stringify({
query: loggingAttributes,
})
)
return true
}),
}) })