From e73d0a45b59044aa5577124ff45db417a08142a1 Mon Sep 17 00:00:00 2001 From: Joonatan Kuosa Date: Tue, 10 Sep 2024 14:37:41 +0300 Subject: [PATCH] refactor: pricing API change in core core branch: https://github.com/City-of-Helsinki/tilavarauspalvelu-core/pull/1317 refs: TILA-3525 --- apps/admin-ui/gql/gql-types.ts | 46 +- apps/admin-ui/src/i18n/messages.ts | 29 +- .../ReservationUnit/edit/ActivationGroup.tsx | 13 +- .../spa/ReservationUnit/edit/ImageEditor.tsx | 19 +- .../spa/ReservationUnit/edit/PricingType.tsx | 353 +++++++-------- .../src/spa/ReservationUnit/edit/form.ts | 246 ++++++----- .../src/spa/ReservationUnit/edit/index.tsx | 199 ++++----- .../src/spa/reservations/[id]/index.tsx | 12 +- .../src/spa/reservations/[id]/util.test.ts | 75 ++-- .../src/spa/reservations/[id]/util.ts | 35 +- apps/admin-ui/src/styles/layout.tsx | 8 +- .../calendar/ReservationCalendarControls.tsx | 1 + apps/ui/components/reservation-unit/Head.tsx | 2 +- .../reservation-unit/QuickReservation.tsx | 1 + .../reservation-unit/RelatedUnits.tsx | 3 +- apps/ui/components/reservation/EditStep0.tsx | 1 + .../reservation/ReservationCard.tsx | 1 + .../reservation/ReservationInfoCard.tsx | 1 + .../SingleSearchReservationUnitCard.tsx | 3 +- apps/ui/gql/gql-types.ts | 60 +-- .../modules/__tests__/reservationUnit.test.ts | 408 ++++-------------- apps/ui/modules/reservationUnit.ts | 80 ++-- apps/ui/pages/reservation-unit/[id].tsx | 42 +- packages/common/gql/gql-types.ts | 40 +- .../components/form/ControlledNumberInput.tsx | 62 +-- packages/common/src/helpers.ts | 22 +- packages/common/src/queries/fragments.tsx | 2 - packages/common/src/reservation-pricing.ts | 8 +- tilavaraus.graphql | 50 ++- 29 files changed, 745 insertions(+), 1077 deletions(-) diff --git a/apps/admin-ui/gql/gql-types.ts b/apps/admin-ui/gql/gql-types.ts index 39681b03ec..27e3300948 100644 --- a/apps/admin-ui/gql/gql-types.ts +++ b/apps/admin-ui/gql/gql-types.ts @@ -1689,14 +1689,6 @@ export enum PriceUnit { PerWeek = "PER_WEEK", } -/** An enumeration. */ -export enum PricingType { - /** Maksuton */ - Free = "FREE", - /** Maksullinen */ - Paid = "PAID", -} - /** An enumeration. */ export enum Priority { Primary = "PRIMARY", @@ -4089,8 +4081,6 @@ export type ReservationUnitPricingNode = Node & { lowestPriceNet?: Maybe; pk?: Maybe; priceUnit: PriceUnit; - pricingType?: Maybe; - status: Status; taxPercentage: TaxPercentageNode; }; @@ -4098,12 +4088,11 @@ export type ReservationUnitPricingSerializerInput = { begins: Scalars["Date"]["input"]; highestPrice?: InputMaybe; highestPriceNet?: InputMaybe; + isActivatedOnBegins?: InputMaybe; lowestPrice?: InputMaybe; lowestPriceNet?: InputMaybe; pk?: InputMaybe; priceUnit?: InputMaybe; - pricingType?: InputMaybe; - status: Status; taxPercentage?: InputMaybe; }; @@ -4737,12 +4726,20 @@ export type SpaceUpdateMutationPayload = { /** An enumeration. */ export enum Status { - /** aktiivinen */ - Active = "ACTIVE", - /** tuleva */ - Future = "FUTURE", - /** mennyt */ - Past = "PAST", + /** Peruttu */ + Cancelled = "CANCELLED", + /** Luonnos */ + Draft = "DRAFT", + /** Rauennut */ + Expired = "EXPIRED", + /** Käsitelty */ + Handled = "HANDLED", + /** Käsittelyssä */ + InAllocation = "IN_ALLOCATION", + /** Vastaanotettu */ + Received = "RECEIVED", + /** Päätökset lähetetty */ + ResultSent = "RESULT_SENT", } export type SuitableTimeRangeNode = Node & { @@ -5236,12 +5233,11 @@ export type UpdateReservationUnitPricingSerializerInput = { begins?: InputMaybe; highestPrice?: InputMaybe; highestPriceNet?: InputMaybe; + isActivatedOnBegins?: InputMaybe; lowestPrice?: InputMaybe; lowestPriceNet?: InputMaybe; pk?: InputMaybe; priceUnit?: InputMaybe; - pricingType?: InputMaybe; - status?: InputMaybe; taxPercentage?: InputMaybe; }; @@ -5928,10 +5924,8 @@ export type PricingFieldsFragment = { id: string; begins: string; priceUnit: PriceUnit; - pricingType?: PricingType | null; lowestPrice: string; highestPrice: string; - status: Status; taxPercentage: { id: string; pk?: number | null; value: string }; }; @@ -6810,10 +6804,8 @@ export type ReservationUnitEditQuery = { id: string; begins: string; priceUnit: PriceUnit; - pricingType?: PricingType | null; lowestPrice: string; highestPrice: string; - status: Status; taxPercentage: { id: string; pk?: number | null; value: string }; }>; applicationRoundTimeSlots: Array<{ @@ -8405,10 +8397,8 @@ export type ReservationQuery = { id: string; begins: string; priceUnit: PriceUnit; - pricingType?: PricingType | null; lowestPrice: string; highestPrice: string; - status: Status; taxPercentage: { id: string; pk?: number | null; value: string }; }>; metadataSet?: { @@ -8706,10 +8696,8 @@ export type ReservationUnitPricingFragment = { id: string; begins: string; priceUnit: PriceUnit; - pricingType?: PricingType | null; lowestPrice: string; highestPrice: string; - status: Status; taxPercentage: { id: string; pk?: number | null; value: string }; }>; }; @@ -9688,7 +9676,6 @@ export const PricingFieldsFragmentDoc = gql` id begins priceUnit - pricingType lowestPrice highestPrice taxPercentage { @@ -9696,7 +9683,6 @@ export const PricingFieldsFragmentDoc = gql` pk value } - status } `; export const ReservationUnitPricingFragmentDoc = gql` diff --git a/apps/admin-ui/src/i18n/messages.ts b/apps/admin-ui/src/i18n/messages.ts index fd195682ed..e8e44b0488 100644 --- a/apps/admin-ui/src/i18n/messages.ts +++ b/apps/admin-ui/src/i18n/messages.ts @@ -222,6 +222,16 @@ const translations: ITranslations = { "Ei pystytty luomaan varauksia yli 2 vuoden päähän", ], RESERVATION_SERIES_ALREADY_STARTED: ["Toistuva varaus on jo alkanut"], + RESERVATION_UNIT_PRICINGS_MISSING: [ + "Varausyksiköllä ei ole hinnoittelua", + ], + RESERVATION_UNIT_PRICINGS_NO_ACTIVE_PRICINGS: [ + "Varausyksiköllä ei ole aktiivisia hinnoitteluita", + ], + RESERVATION_UNIT_PRICINGS_DUPLICATE_DATE: ["Päivämäärä on jo käytössä"], + RESERVATION_UNIT_PRICINGS_INVALID_PRICES: [ + "Hinnoittelussa on virheellisiä hintoja", + ], }, descriptive: { "Reservation overlaps with reservation before due to buffer time.": [ @@ -1334,10 +1344,11 @@ const translations: ITranslations = { pricingType: ["Varausyksikön maksullisuus"], pricingTerms: ["Hinnoitteluperiaate"], pricingTypes: { - PAID: ["Maksullinen"], - FREE: ["Maksuton"], + paid: ["Maksullinen"], + free: ["Maksuton"], }, - priceChange: ["Hintaan on tulossa muutos"], + hasFuturePrice: ["Hintaan on tulossa muutos"], + begins: ["Alkaa"], openingTime: ["Alkamisaika"], closingTime: ["Päättymisaika"], }, @@ -1413,12 +1424,6 @@ const translations: ITranslations = { `Kuvaus kirjoitetaan standardointipohjan mukaisesti. Lisää linkkejä ulkoisille verkkosivuille kuten käyttöohjeisiin vain tarvittaessa. Tarkista linkkien toimivuus ja saavutettavuus säännöllisesti. Käytäthän muotoiluja harkiten. `, ], - images: [ - `Liitä vähintään kolme kuvaa. Kuvien tulisi olla todenmukaisia ja hyvälaatuisia. - Suositus: - lisää ensisijaisesti vaakatasossa kuvattuja kuvia, ei kuitenkaan panoramoja. jpeg/jpg ja png, max 1 M - Kuvissa näkyviltä ihmisiltä tulee olla kuvauslupa. Kuvissa ei saa näkyä turvakameroita.`, - ], publishingSettings: [ `Voit ajastaa varausyksikön julkaistavaksi tai piilotettavaksi asiakkailta tiettynä ajankohtana.`, ], @@ -1705,6 +1710,12 @@ const translations: ITranslations = { deleteImage: ["Poista"], mainImage: ["Pääkuva"], useAsMainImage: ["Käytä pääkuvana"], + tooltip: [ + `Liitä vähintään kolme kuvaa. Kuvien tulisi olla todenmukaisia ja hyvälaatuisia. + Suositus: + lisää ensisijaisesti vaakatasossa kuvattuja kuvia, ei kuitenkaan panoramoja. jpeg/jpg ja png, max 1 M + Kuvissa näkyviltä ihmisiltä tulee olla kuvauslupa. Kuvissa ei saa näkyä turvakameroita.`, + ], }, priceUnit: { FIXED: ["Per kerta"], diff --git a/apps/admin-ui/src/spa/ReservationUnit/edit/ActivationGroup.tsx b/apps/admin-ui/src/spa/ReservationUnit/edit/ActivationGroup.tsx index a658d5b16d..f4a215860b 100644 --- a/apps/admin-ui/src/spa/ReservationUnit/edit/ActivationGroup.tsx +++ b/apps/admin-ui/src/spa/ReservationUnit/edit/ActivationGroup.tsx @@ -34,17 +34,14 @@ export function ActivationGroup({ style, className, }: ControllerProps): JSX.Element { - const { field } = useController({ control, name }); + const { + field: { value, onChange }, + } = useController({ control, name }); return ( - - {field.value ? ( + + {value ? ( {children} diff --git a/apps/admin-ui/src/spa/ReservationUnit/edit/ImageEditor.tsx b/apps/admin-ui/src/spa/ReservationUnit/edit/ImageEditor.tsx index 67e8fcfb1d..3d6ecdc3d2 100644 --- a/apps/admin-ui/src/spa/ReservationUnit/edit/ImageEditor.tsx +++ b/apps/admin-ui/src/spa/ReservationUnit/edit/ImageEditor.tsx @@ -38,6 +38,16 @@ function RUImage({ image }: { image: ImageFormType }): JSX.Element { let fakePk = -1; const FileInputContainer = styled.div` + & button { + --background-color-hover: var(--color-black-5); + --color-hover: var(--color-black); + --background-color-focus: transparent; + --color-focus: var(--color-black) --focus-outline-color: + var(--color-focus-outline); + --color: var(--color-black); + --border-color: var(--color-black); + } + div:nth-of-type(3) { width: 100%; button { @@ -61,7 +71,10 @@ const FileInputContainer = styled.div` } `; -const SmallButton = styled(Button)` +const SmallButton = styled(Button).attrs({ + variant: "secondary", + theme: "black", +})` border: 0; padding: 0; min-height: 0; @@ -94,7 +107,6 @@ function ReservationUnitImage({ {t("ImageEditor.mainImage")} ) : ( makeIntoMainImage(image.pk ?? 0)} > @@ -102,7 +114,6 @@ function ReservationUnitImage({ )} deleteImage(image.pk ?? 0)} > @@ -177,7 +188,7 @@ export function ImageEditor({ dragAndDropInputLabel=" " maxSize={5242880} onChange={(files) => addImage(files)} - tooltipText={t("ReservationUnitEditor.tooltip.images")} + tooltipText={t("ImageEditor.tooltip")} /> diff --git a/apps/admin-ui/src/spa/ReservationUnit/edit/PricingType.tsx b/apps/admin-ui/src/spa/ReservationUnit/edit/PricingType.tsx index 0807c84559..83fe9ca726 100644 --- a/apps/admin-ui/src/spa/ReservationUnit/edit/PricingType.tsx +++ b/apps/admin-ui/src/spa/ReservationUnit/edit/PricingType.tsx @@ -1,19 +1,16 @@ import React from "react"; import styled from "styled-components"; import { useTranslation } from "react-i18next"; -import { - DateInput, - IconAlertCircleFill, - NumberInput, - RadioButton, - Select, -} from "hds-react"; -import { PriceUnit, PricingType } from "@gql/gql-types"; +import { IconAlertCircleFill, RadioButton } from "hds-react"; +import { PriceUnit } from "@gql/gql-types"; import { Controller, UseFormReturn } from "react-hook-form"; import { addDays } from "date-fns"; import { AutoGrid } from "@/styles/layout"; import { getTranslatedError } from "@/common/util"; import { type ReservationUnitEditFormValues, PaymentTypes } from "./form"; +import { ControlledDateInput } from "common/src/components/form"; +import { ControlledSelect } from "common/src/components/form/ControlledSelect"; +import { ControlledNumberInput } from "common/src/components/form/ControlledNumberInput"; const Error = styled.div` margin-top: var(--spacing-3-xs); @@ -27,9 +24,25 @@ const Error = styled.div` `; type Props = { - index: number; + pk: number; form: UseFormReturn; - taxPercentageOptions: { label: string; value: number }[]; + taxPercentageOptions: TaxOption[]; +}; + +function removeTax(price: number, taxPercentage: number) { + const tmp = (price * 100) / (100 + taxPercentage); + return Math.floor(tmp * 100) / 100; +} + +function addTax(price: number, taxPercentage: number) { + const tmp = price * ((100 + taxPercentage) / 100); + return Math.floor(tmp * 100) / 100; +} + +export type TaxOption = { + label: string; + pk: number; + value: number; }; function PaidPricingPart({ @@ -39,10 +52,10 @@ function PaidPricingPart({ }: { form: UseFormReturn; index: number; - taxPercentageOptions: { label: string; value: number }[]; + taxPercentageOptions: TaxOption[]; }) { const { t } = useTranslation(); - const { control, setValue, formState, register, watch } = form; + const { control, setValue, formState, watch } = form; const { errors } = formState; const unitPriceOptions = Object.values(PriceUnit).map((choice) => ({ @@ -55,234 +68,150 @@ function PaidPricingPart({ value, })); - const removeTax = (price: number, taxPercentage: number) => { - const tmp = (price * 100) / (100 + taxPercentage); - const tmp2 = Math.round(tmp * 100) / 100; - return tmp2; - }; - - const addTax = (price: number, taxPercentage: number) => { - const tmp = price * ((100 + taxPercentage) / 100); - const tmp2 = Math.round(tmp * 100) / 100; - return tmp2; - }; - const pricing = watch(`pricings.${index}`); - - const taxPercentage = watch(`pricings.${index}.taxPercentage`).value ?? 0; + const taxPercentagePk = watch(`pricings.${index}.taxPercentage`); + const taxPercentage = + taxPercentageOptions.find((x) => x.pk === taxPercentagePk)?.value ?? 0; // TODO mobile number keyboard? return ( <> - ( - { - onChange({ pk: v.value, value: Number(v.label) }); - const low = Number(pricing?.lowestPrice); - const high = Number(pricing?.highestPrice); - const tax = pricing?.taxPercentage.value ?? 0; - if (!Number.isNaN(low)) { - const lowNet = removeTax(low, tax); - setValue(`pricings.${index}.lowestPriceNet`, lowNet); - } - if (!Number.isNaN(high)) { - const highNet = removeTax(high, tax); - setValue(`pricings.${index}.highestPriceNet`, highNet); - } - }} - value={ - taxPercentageOptions.find( - (option) => option.value === value.pk - ) ?? null - } - error={getTranslatedError( - t, - errors.pricings?.[index]?.taxPercentage?.message - )} - invalid={errors.pricings?.[index]?.taxPercentage?.message != null} - /> + required + label={t(`ReservationUnitEditor.label.taxPercentage`)} + options={taxPercentageOptions.map((x) => ({ + label: x.label, + value: x.pk, + }))} + afterChange={(val) => { + const low = pricing.lowestPrice; + const high = pricing.highestPrice; + const tax = + taxPercentageOptions.find((x) => x.pk === val)?.value ?? 0; + if (!Number.isNaN(low)) { + const lowNet = removeTax(low, tax); + setValue(`pricings.${index}.lowestPriceNet`, lowNet); + } + if (!Number.isNaN(high)) { + const highNet = removeTax(high, tax); + setValue(`pricings.${index}.highestPriceNet`, highNet); + } + }} + error={getTranslatedError( + t, + errors.pricings?.[index]?.taxPercentage?.message )} /> - { - const val = Number(e.currentTarget.value); - if (!Number.isNaN(val)) { - setValue( - `pricings.${index}.lowestPrice`, - addTax(val, taxPercentage) - ); - } - }, - setValueAs: (val) => (val !== "" ? Number(val) : null), - })} - id={`pricings.${index}.lowestPriceNet`} + { + if (value != null) { + setValue( + `pricings.${index}.lowestPrice`, + addTax(value, taxPercentage) + ); + } + }} label={t("ReservationUnitEditor.label.lowestPriceNet")} - minusStepButtonAriaLabel={t("common.decreaseByOneAriaLabel")} - plusStepButtonAriaLabel={t("common.increaseByOneAriaLabel")} - step={1} min={0} - max={undefined} errorText={getTranslatedError( t, errors.pricings?.[index]?.lowestPriceNet?.message )} - invalid={errors.pricings?.[index]?.lowestPriceNet?.message != null} /> - { - const val = Number(e.currentTarget.value); - if (!Number.isNaN(val)) { - setValue( - `pricings.${index}.lowestPriceNet`, - removeTax(val, taxPercentage) - ); - } - }, - setValueAs: (val) => (val !== "" ? Number(val) : null), - })} - id={`pricings.${index}.lowestPrice`} + { + if (value != null) { + setValue( + `pricings.${index}.lowestPriceNet`, + removeTax(value, taxPercentage) + ); + } + }} label={t("ReservationUnitEditor.label.lowestPrice")} - minusStepButtonAriaLabel={t("common.decreaseByOneAriaLabel")} - plusStepButtonAriaLabel={t("common.increaseByOneAriaLabel")} - step={1} + tooltipText={t("ReservationUnitEditor.tooltip.lowestPrice")} min={0} - max={undefined} errorText={getTranslatedError( t, errors.pricings?.[index]?.lowestPrice?.message )} - invalid={errors.pricings?.[index]?.lowestPrice?.message != null} - tooltipText={t("ReservationUnitEditor.tooltip.lowestPrice")} /> - { - const val = Number(e.currentTarget.value); - if (!Number.isNaN(val)) { - setValue( - `pricings.${index}.highestPrice`, - addTax(val, taxPercentage) - ); - } - }, - setValueAs: (val) => (val !== "" ? Number(val) : null), - })} + { + if (value != null) { + setValue( + `pricings.${index}.highestPrice`, + addTax(value, taxPercentage) + ); + } + }} label={t("ReservationUnitEditor.label.highestPriceNet")} - minusStepButtonAriaLabel={t("common.decreaseByOneAriaLabel")} - plusStepButtonAriaLabel={t("common.increaseByOneAriaLabel")} - step={1} min={0} - max={undefined} errorText={getTranslatedError( t, errors.pricings?.[index]?.highestPriceNet?.message )} - invalid={errors.pricings?.[index]?.highestPriceNet?.message != null} /> - { - const val = Number(e.currentTarget.value); - if (!Number.isNaN(val)) { - setValue( - `pricings.${index}.highestPriceNet`, - removeTax(val, taxPercentage) - ); - } - }, - setValueAs: (val) => (val !== "" ? Number(val) : null), - })} + { + if (value != null) { + setValue( + `pricings.${index}.highestPriceNet`, + removeTax(value, taxPercentage) + ); + } + }} label={t("ReservationUnitEditor.label.highestPrice")} - minusStepButtonAriaLabel={t("common.decreaseByOneAriaLabel")} - plusStepButtonAriaLabel={t("common.increaseByOneAriaLabel")} - step={1} + tooltipText={t("ReservationUnitEditor.tooltip.highestPrice")} min={0} - max={undefined} errorText={getTranslatedError( t, errors.pricings?.[index]?.highestPrice?.message )} - invalid={errors.pricings?.[index]?.highestPrice?.message != null} - tooltipText={t("ReservationUnitEditor.tooltip.highestPrice")} /> - ( -