From 5dc8a403ef3522da8b5b2a6489d549605efb59b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Fri, 14 Feb 2025 15:14:58 +0100 Subject: [PATCH] feat(dashboard): Pickup option changes (#11306) **What** - update the create and edit shipping option flows to support pickup (shipping) option - modify "mark as delivered" for pickup case --- CLOSES CMRC-906 CMRC-907 --- .../src/i18n/translations/$schema.json | 57 +++++++- .../dashboard/src/i18n/translations/en.json | 17 ++- .../location-general-section.tsx | 16 ++- .../create-shipping-option-details-form.tsx | 124 ++++++++++-------- .../create-shipping-options-form.tsx | 10 +- .../create-shipping-options-prices-form.tsx | 29 +++- ...on-service-zone-shipping-option-create.tsx | 17 ++- .../edit-shipping-option-form.tsx | 118 +++++++++-------- ...tion-service-zone-shipping-option-edit.tsx | 16 ++- .../order-fulfillment-section.tsx | 27 +++- .../routes/orders/order-detail/constants.ts | 1 + ...-shipping-options-for-cart-with-pricing.ts | 2 + .../list-shipping-options-for-cart.ts | 2 + 13 files changed, 310 insertions(+), 126 deletions(-) diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index df15ba7f77..c5e9498a8c 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -4806,6 +4806,9 @@ "markAsShipped": { "type": "string" }, + "markAsPickedUp": { + "type": "string" + }, "markAsDelivered": { "type": "string" }, @@ -4915,13 +4918,17 @@ }, "fulfillmentDelivered": { "type": "string" + }, + "fulfillmentPickedUp": { + "type": "string" } }, "required": [ "created", "canceled", "fulfillmentShipped", - "fulfillmentDelivered" + "fulfillmentDelivered", + "fulfillmentPickedUp" ], "additionalProperties": false }, @@ -4952,6 +4959,7 @@ "available", "inStock", "markAsShipped", + "markAsPickedUp", "markAsDelivered", "itemsToFulfillDesc", "locationDescription", @@ -5754,6 +5762,23 @@ ], "additionalProperties": false }, + "pickupOptions": { + "type": "object", + "properties": { + "edit": { + "type": "object", + "properties": { + "header": { + "type": "string" + } + }, + "required": ["header"], + "additionalProperties": false + } + }, + "required": ["edit"], + "additionalProperties": false + }, "shippingOptions": { "type": "object", "properties": { @@ -5779,6 +5804,25 @@ "required": ["header", "hint", "label", "successToast"], "additionalProperties": false }, + "pickup": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "hint": { + "type": "string" + }, + "label": { + "type": "string" + }, + "successToast": { + "type": "string" + } + }, + "required": ["header", "hint", "label", "successToast"], + "additionalProperties": false + }, "returns": { "type": "object", "properties": { @@ -5815,7 +5859,7 @@ "type": "string" } }, - "required": ["shipping", "returns", "tabs", "action"], + "required": ["shipping", "pickup", "returns", "tabs", "action"], "additionalProperties": false }, "delete": { @@ -5996,6 +6040,12 @@ "shipping_other": { "type": "string" }, + "pickup_one": { + "type": "string" + }, + "pickup_other": { + "type": "string" + }, "returns_one": { "type": "string" }, @@ -6006,6 +6056,8 @@ "required": [ "shipping_one", "shipping_other", + "pickup_one", + "pickup_other", "returns_one", "returns_other" ], @@ -6201,6 +6253,7 @@ "fulfillmentSets", "sidebar", "salesChannels", + "pickupOptions", "shippingOptions", "serviceZones" ], diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index e0c44e373a..716c5fea25 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -1281,6 +1281,7 @@ "available": "Available", "inStock": "In stock", "markAsShipped": "Mark as shipped", + "markAsPickedUp": "Mark as picked up", "markAsDelivered": "Mark as delivered", "itemsToFulfillDesc": "Choose items and quantities to fulfill", "locationDescription": "Choose which location you want to fulfill items from.", @@ -1310,7 +1311,8 @@ "created": "Fulfillment created successfully", "canceled": "Fulfillment successfully canceled", "fulfillmentShipped": "Cannot cancel an already shipped fulfillment", - "fulfillmentDelivered": "Fulfillment marked as delivered successfully" + "fulfillmentDelivered": "Fulfillment marked as delivered successfully", + "fulfillmentPickedUp": "Fulfillment marked as picked up successfully" }, "trackingLabel": "Tracking", "shippingFromLabel": "Shipping from", @@ -1520,6 +1522,11 @@ "action": "Connect sales channels", "successToast": "Sales channels were successfully updated." }, + "pickupOptions": { + "edit": { + "header": "Edit Pickup Option" + } + }, "shippingOptions": { "create": { "shipping": { @@ -1528,6 +1535,12 @@ "label": "Shipping options", "successToast": "Shipping option {{name}} was successfully created." }, + "pickup": { + "header": "Create Pickup Option for {{zone}}", + "hint": "Create a new pickup option to define how products are picked up from this location.", + "label": "Pickup options", + "successToast": "Pickup option {{name}} was successfully created." + }, "returns": { "header": "Create a Return Option for {{zone}}", "hint": "Create a new return option to define how products are returned to this location.", @@ -1591,6 +1604,8 @@ "count": { "shipping_one": "{{count}} shipping option", "shipping_other": "{{count}} shipping options", + "pickup_one": "{{count}} pickup option", + "pickup_other": "{{count}} pickup options", "returns_one": "{{count}} return option", "returns_other": "{{count}} return options" }, diff --git a/packages/admin/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx b/packages/admin/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx index 8927b8693e..6b6507f5cf 100644 --- a/packages/admin/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-detail/components/location-general-section/location-general-section.tsx @@ -195,12 +195,14 @@ type ServiceZoneOptionsProps = { zone: HttpTypes.AdminServiceZone locationId: string fulfillmentSetId: string + type: FulfillmentSetType } function ServiceZoneOptions({ zone, locationId, fulfillmentSetId, + type, }: ServiceZoneOptionsProps) { const { t } = useTranslation() @@ -216,7 +218,7 @@ function ServiceZoneOptions({
- {t("stockLocations.shippingOptions.create.shipping.label")} + {t(`stockLocations.shippingOptions.create.${type}.label`)} ยท - {t("stockLocations.shippingOptions.fields.count.shipping", { + {t(`stockLocations.shippingOptions.fields.count.${type}`, { count: shippingOptionsCount, })} @@ -427,6 +435,7 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) { )} @@ -570,6 +579,7 @@ function FulfillmentSet(props: FulfillmentSetProps) { {fulfillmentSet?.service_zones.map((zone) => ( { const { t } = useTranslation() + const isPickup = type === FulfillmentSetType.Pickup + const shippingProfiles = useComboboxData({ queryFn: (params) => sdk.admin.shippingProfile.list(params), queryKey: ["shipping_profiles"], @@ -63,7 +70,7 @@ export const CreateShippingOptionDetailsForm = ({ {t( `stockLocations.shippingOptions.create.${ - isReturn ? "returns" : "shipping" + isPickup ? "pickup" : isReturn ? "returns" : "shipping" }.header`, { zone: zone.name, @@ -73,54 +80,56 @@ export const CreateShippingOptionDetailsForm = ({ {t( `stockLocations.shippingOptions.create.${ - isReturn ? "returns" : "shipping" + isReturn ? "returns" : isPickup ? "pickup" : "shipping" }.hint` )}
- { - return ( - - - {t("stockLocations.shippingOptions.fields.priceType.label")} - - - - - - - - - - ) - }} - /> + {!isPickup && ( + { + return ( + + + {t("stockLocations.shippingOptions.fields.priceType.label")} + + + + + + + + + + ) + }} + /> + )}
- - - + {!isPickup && ( + <> + + + + )}
) diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx index 07dc491ddf..902faf8e22 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx @@ -12,7 +12,10 @@ import { import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options" import { castNumber } from "../../../../../lib/cast-number" -import { ShippingOptionPriceType } from "../../../common/constants" +import { + FulfillmentSetType, + ShippingOptionPriceType, +} from "../../../common/constants" import { buildShippingOptionPriceRules } from "../../../common/utils/price-rule-helpers" import { CreateShippingOptionDetailsForm } from "./create-shipping-option-details-form" import { CreateShippingOptionsPricesForm } from "./create-shipping-options-prices-form" @@ -31,12 +34,14 @@ type CreateShippingOptionFormProps = { zone: HttpTypes.AdminServiceZone locationId: string isReturn?: boolean + type: FulfillmentSetType } export function CreateShippingOptionsForm({ zone, isReturn, locationId, + type, }: CreateShippingOptionFormProps) { const [activeTab, setActiveTab] = useState(Tab.DETAILS) const [validDetails, setValidDetails] = useState(false) @@ -309,13 +314,14 @@ export function CreateShippingOptionsForm({ form={form} zone={zone} isReturn={isReturn} + type={type} locationId={locationId} fulfillmentProviderOptions={fulfillmentProviderOptions || []} selectedProviderId={selectedProviderId} /> - + diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx index 6d00a42c42..2840c094f3 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx @@ -1,4 +1,4 @@ -import { useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { UseFormReturn, useWatch } from "react-hook-form" import { DataGrid } from "../../../../../components/data-grid" @@ -12,18 +12,24 @@ import { useRegions } from "../../../../../hooks/api/regions" import { useStore } from "../../../../../hooks/api/store" import { ConditionalPriceForm } from "../../../common/components/conditional-price-form" import { ShippingOptionPriceProvider } from "../../../common/components/shipping-option-price-provider" -import { CONDITIONAL_PRICES_STACKED_MODAL_ID } from "../../../common/constants" +import { + FulfillmentSetType, + CONDITIONAL_PRICES_STACKED_MODAL_ID, +} from "../../../common/constants" import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns" import { ConditionalPriceInfo } from "../../../common/types" import { CreateShippingOptionSchema } from "./schema" type PricingPricesFormProps = { form: UseFormReturn + type: FulfillmentSetType } export const CreateShippingOptionsPricesForm = ({ form, + type, }: PricingPricesFormProps) => { + const isPickup = type === FulfillmentSetType.Pickup const { getIsOpen, setIsOpen } = useStackedModal() const [selectedPrice, setSelectedPrice] = useState(null) @@ -80,6 +86,25 @@ export const CreateShippingOptionsPricesForm = ({ [currencies, regions] ) + /** + * Prefill prices with 0 if createing a pickup (shipping) option + */ + useEffect(() => { + if (!isLoading && isPickup) { + if (currencies.length > 0) { + currencies.forEach((currency) => { + form.setValue(`currency_prices.${currency}`, "0") + }) + } + + if (regions.length > 0) { + regions.forEach((region) => { + form.setValue(`region_prices.${region.id}`, "0") + }) + } + } + }, [isLoading, isPickup]) + if (isStoreError) { throw storeError } diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/location-service-zone-shipping-option-create.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/location-service-zone-shipping-option-create.tsx index f878ac9d0d..c40d49a89b 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/location-service-zone-shipping-option-create.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/location-service-zone-shipping-option-create.tsx @@ -4,6 +4,7 @@ import { RouteFocusModal } from "../../../components/modals" import { useStockLocation } from "../../../hooks/api/stock-locations" import { CreateShippingOptionsForm } from "./components/create-shipping-options-form" import { LOC_CREATE_SHIPPING_OPTION_FIELDS } from "./constants" +import { FulfillmentSetType } from "../common/constants" export function LocationServiceZoneShippingOptionCreate() { const { location_id, fset_id, zone_id } = useParams() @@ -15,9 +16,18 @@ export function LocationServiceZoneShippingOptionCreate() { fields: LOC_CREATE_SHIPPING_OPTION_FIELDS, }) - const zone = stock_location?.fulfillment_sets - ?.find((f) => f.id === fset_id) - ?.service_zones?.find((z) => z.id === zone_id) + const fulfillmentSet = stock_location?.fulfillment_sets?.find( + (f) => f.id === fset_id + ) + + if (!isPending && !isFetching && !fulfillmentSet) { + throw json( + { message: `Fulfillment set with ID ${fset_id} was not found` }, + 404 + ) + } + + const zone = fulfillmentSet?.service_zones?.find((z) => z.id === zone_id) if (!isPending && !isFetching && !zone) { throw json( @@ -37,6 +47,7 @@ export function LocationServiceZoneShippingOptionCreate() { zone={zone} isReturn={isReturn} locationId={location_id!} + type={fulfillmentSet!.type as FulfillmentSetType} /> )} diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx index bed53bbe99..c33fe0b6dd 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/components/edit-region-form/edit-shipping-option-form.tsx @@ -15,11 +15,15 @@ import { useComboboxData } from "../../../../../hooks/use-combobox-data" import { sdk } from "../../../../../lib/client" import { pick } from "../../../../../lib/common" import { isOptionEnabledInStore } from "../../../../../lib/shipping-options" -import { ShippingOptionPriceType } from "../../../common/constants" +import { + FulfillmentSetType, + ShippingOptionPriceType, +} from "../../../common/constants" type EditShippingOptionFormProps = { locationId: string shippingOption: HttpTypes.AdminShippingOption + type: FulfillmentSetType } const EditShippingOptionSchema = zod.object({ @@ -32,10 +36,13 @@ const EditShippingOptionSchema = zod.object({ export const EditShippingOptionForm = ({ locationId, shippingOption, + type, }: EditShippingOptionFormProps) => { const { t } = useTranslation() const { handleSuccess } = useRouteModal() + const isPickup = type === FulfillmentSetType.Pickup + const shippingProfiles = useComboboxData({ queryFn: (params) => sdk.admin.shippingProfile.list(params), queryKey: ["shipping_profiles"], @@ -108,46 +115,48 @@ export const EditShippingOptionForm = ({
- { - return ( - - - {t( - "stockLocations.shippingOptions.fields.priceType.label" - )} - - - - - - - - - - ) - }} - /> + {!isPickup && ( + { + return ( + + + {t( + "stockLocations.shippingOptions.fields.priceType.label" + )} + + + + + + + + + + ) + }} + /> + )}
- - - + {!isPickup && ( + <> + + + + )}
diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/location-service-zone-shipping-option-edit.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/location-service-zone-shipping-option-edit.tsx index 5845c6a891..90c9bd1fce 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/location-service-zone-shipping-option-edit.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-edit/location-service-zone-shipping-option-edit.tsx @@ -5,6 +5,7 @@ import { json, useParams } from "react-router-dom" import { RouteDrawer } from "../../../components/modals" import { useShippingOptions } from "../../../hooks/api/shipping-options" import { EditShippingOptionForm } from "./components/edit-region-form" +import { FulfillmentSetType } from "../common/constants" export const LocationServiceZoneShippingOptionEdit = () => { const { t } = useTranslation() @@ -14,6 +15,7 @@ export const LocationServiceZoneShippingOptionEdit = () => { const { shipping_options, isPending, isFetching, isError, error } = useShippingOptions({ id: so_id, + fields: "+service_zone.fulfillment_set.type", }) const shippingOption = shipping_options?.find((so) => so.id === so_id) @@ -29,15 +31,27 @@ export const LocationServiceZoneShippingOptionEdit = () => { throw error } + const isPickup = + shippingOption?.service_zone.fulfillment_set.type === + FulfillmentSetType.Pickup + return ( - {t("stockLocations.shippingOptions.edit.header")} + + {t( + `stockLocations.${isPickup ? "pickupOptions" : "shippingOptions"}.edit.header` + )} + {shippingOption && ( )} diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx index 30e5d8f4f7..8e2054ef7a 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx @@ -30,6 +30,7 @@ import { import { useStockLocation } from "../../../../../hooks/api/stock-locations" import { formatProvider } from "../../../../../lib/format-provider" import { getLocaleAmount } from "../../../../../lib/money-amount-helpers" +import { FulfillmentSetType } from "../../../../locations/common/constants" type OrderFulfillmentSectionProps = { order: AdminOrder @@ -213,6 +214,10 @@ const Fulfillment = ({ const showLocation = !!fulfillment.location_id + const isPickUpFulfillment = + fulfillment.shipping_option?.service_zone.fulfillment_set.type === + FulfillmentSetType.Pickup + const { stock_location, isError, error } = useStockLocation( fulfillment.location_id!, undefined, @@ -222,7 +227,9 @@ const Fulfillment = ({ ) let statusText = fulfillment.requires_shipping - ? "Awaiting shipping" + ? isPickUpFulfillment + ? "Awaiting pickup" + : "Awaiting shipping" : "Awaiting delivery" let statusColor: "blue" | "green" | "red" = "blue" let statusTimestamp = fulfillment.created_at @@ -251,7 +258,9 @@ const Fulfillment = ({ !fulfillment.canceled_at && !fulfillment.shipped_at && !fulfillment.delivered_at && - fulfillment.requires_shipping + fulfillment.requires_shipping && + !isPickUpFulfillment + const showDeliveryButton = !fulfillment.canceled_at && !fulfillment.delivered_at @@ -267,7 +276,13 @@ const Fulfillment = ({ if (res) { await markAsDelivered(undefined, { onSuccess: () => { - toast.success(t("orders.fulfillment.toast.fulfillmentDelivered")) + toast.success( + t( + isPickUpFulfillment + ? "orders.fulfillment.toast.fulfillmentPickedUp" + : "orders.fulfillment.toast.fulfillmentDelivered" + ) + ) }, onError: (e) => { toast.error(e.message) @@ -431,7 +446,11 @@ const Fulfillment = ({
{showDeliveryButton && ( )} diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/constants.ts b/packages/admin/dashboard/src/routes/orders/order-detail/constants.ts index 446ff7e218..a4a1519659 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/constants.ts +++ b/packages/admin/dashboard/src/routes/orders/order-detail/constants.ts @@ -37,6 +37,7 @@ const DEFAULT_RELATIONS = [ "*promotion", "*shipping_methods", "*fulfillments", + "+fulfillments.shipping_option.service_zone.fulfillment_set.type", "*fulfillments.items", "*fulfillments.labels", "*fulfillments.labels", diff --git a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart-with-pricing.ts b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart-with-pricing.ts index 1fc550029e..600138fd90 100644 --- a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart-with-pricing.ts +++ b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart-with-pricing.ts @@ -22,6 +22,8 @@ const COMMON_OPTIONS_FIELDS = [ "price_type", "service_zone_id", "service_zone.fulfillment_set_id", + "service_zone.fulfillment_set.type", + "service_zone.fulfillment_set.location.address.*", "shipping_profile_id", "provider_id", "data", diff --git a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts index 2a177673f6..255eda18c8 100644 --- a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts @@ -131,6 +131,8 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( "provider_id", "data", "service_zone.fulfillment_set_id", + "service_zone.fulfillment_set.type", + "service_zone.fulfillment_set.location.address.*", "type.id", "type.label",