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
This commit is contained in:
Frane Polić
2025-02-14 15:14:58 +01:00
committed by GitHub
parent 271337eb23
commit 5dc8a403ef
13 changed files with 310 additions and 126 deletions
@@ -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({
<div className="flex flex-col gap-y-4 px-6 py-4">
<div className="item-center flex justify-between">
<span className="text-ui-fg-subtle txt-small self-center font-medium">
{t("stockLocations.shippingOptions.create.shipping.label")}
{t(`stockLocations.shippingOptions.create.${type}.label`)}
</span>
<LinkButton
to={`/settings/locations/${locationId}/fulfillment-set/${fulfillmentSetId}/service-zone/${zone.id}/shipping-option/create`}
@@ -274,9 +276,15 @@ type ServiceZoneProps = {
zone: HttpTypes.AdminServiceZone
locationId: string
fulfillmentSetId: string
type: FulfillmentSetType
}
function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
function ServiceZone({
zone,
locationId,
fulfillmentSetId,
type,
}: ServiceZoneProps) {
const { t } = useTranslation()
const prompt = usePrompt()
const [open, setOpen] = useState(true)
@@ -368,7 +376,7 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
/>
<span>·</span>
<Text className="text-ui-fg-subtle txt-small">
{t("stockLocations.shippingOptions.fields.count.shipping", {
{t(`stockLocations.shippingOptions.fields.count.${type}`, {
count: shippingOptionsCount,
})}
</Text>
@@ -427,6 +435,7 @@ function ServiceZone({ zone, locationId, fulfillmentSetId }: ServiceZoneProps) {
<ServiceZoneOptions
fulfillmentSetId={fulfillmentSetId}
locationId={locationId}
type={type}
zone={zone}
/>
)}
@@ -570,6 +579,7 @@ function FulfillmentSet(props: FulfillmentSetProps) {
{fulfillmentSet?.service_zones.map((zone) => (
<ServiceZone
zone={zone}
type={type}
key={zone.id}
locationId={locationId}
fulfillmentSetId={fulfillmentSet.id}
@@ -10,7 +10,10 @@ import { Combobox } from "../../../../../components/inputs/combobox"
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
import { sdk } from "../../../../../lib/client"
import { formatProvider } from "../../../../../lib/format-provider"
import { ShippingOptionPriceType } from "../../../common/constants"
import {
FulfillmentSetType,
ShippingOptionPriceType,
} from "../../../common/constants"
import { CreateShippingOptionSchema } from "./schema"
type CreateShippingOptionDetailsFormProps = {
@@ -20,6 +23,7 @@ type CreateShippingOptionDetailsFormProps = {
locationId: string
fulfillmentProviderOptions: HttpTypes.AdminFulfillmentProviderOption[]
selectedProviderId?: string
type: FulfillmentSetType
}
export const CreateShippingOptionDetailsForm = ({
@@ -29,9 +33,12 @@ export const CreateShippingOptionDetailsForm = ({
locationId,
fulfillmentProviderOptions,
selectedProviderId,
type,
}: CreateShippingOptionDetailsFormProps) => {
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 = ({
<Heading>
{t(
`stockLocations.shippingOptions.create.${
isReturn ? "returns" : "shipping"
isPickup ? "pickup" : isReturn ? "returns" : "shipping"
}.header`,
{
zone: zone.name,
@@ -73,54 +80,56 @@ export const CreateShippingOptionDetailsForm = ({
<Text size="small" className="text-ui-fg-subtle">
{t(
`stockLocations.shippingOptions.create.${
isReturn ? "returns" : "shipping"
isReturn ? "returns" : isPickup ? "pickup" : "shipping"
}.hint`
)}
</Text>
</div>
<Form.Field
control={form.control}
name="price_type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("stockLocations.shippingOptions.fields.priceType.label")}
</Form.Label>
<Form.Control>
<RadioGroup
className="grid grid-cols-1 gap-4 md:grid-cols-2"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
className="flex-1"
value={ShippingOptionPriceType.FlatRate}
label={t(
"stockLocations.shippingOptions.fields.priceType.options.fixed.label"
)}
description={t(
"stockLocations.shippingOptions.fields.priceType.options.fixed.hint"
)}
/>
<RadioGroup.ChoiceBox
className="flex-1"
value={ShippingOptionPriceType.Calculated}
label={t(
"stockLocations.shippingOptions.fields.priceType.options.calculated.label"
)}
description={t(
"stockLocations.shippingOptions.fields.priceType.options.calculated.hint"
)}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
{!isPickup && (
<Form.Field
control={form.control}
name="price_type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t("stockLocations.shippingOptions.fields.priceType.label")}
</Form.Label>
<Form.Control>
<RadioGroup
className="grid grid-cols-1 gap-4 md:grid-cols-2"
{...field}
onValueChange={field.onChange}
>
<RadioGroup.ChoiceBox
className="flex-1"
value={ShippingOptionPriceType.FlatRate}
label={t(
"stockLocations.shippingOptions.fields.priceType.options.fixed.label"
)}
description={t(
"stockLocations.shippingOptions.fields.priceType.options.fixed.hint"
)}
/>
<RadioGroup.ChoiceBox
className="flex-1"
value={ShippingOptionPriceType.Calculated}
label={t(
"stockLocations.shippingOptions.fields.priceType.options.calculated.label"
)}
description={t(
"stockLocations.shippingOptions.fields.priceType.options.calculated.hint"
)}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Form.Field
@@ -238,16 +247,21 @@ export const CreateShippingOptionDetailsForm = ({
/>
</div>
<Divider />
<SwitchBox
control={form.control}
name="enabled_in_store"
label={t("stockLocations.shippingOptions.fields.enableInStore.label")}
description={t(
"stockLocations.shippingOptions.fields.enableInStore.hint"
)}
/>
{!isPickup && (
<>
<Divider />
<SwitchBox
control={form.control}
name="enabled_in_store"
label={t(
"stockLocations.shippingOptions.fields.enableInStore.label"
)}
description={t(
"stockLocations.shippingOptions.fields.enableInStore.hint"
)}
/>
</>
)}
</div>
</div>
)
@@ -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>(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}
/>
</ProgressTabs.Content>
<ProgressTabs.Content value={Tab.PRICING} className="size-full">
<CreateShippingOptionsPricesForm form={form} />
<CreateShippingOptionsPricesForm form={form} type={type} />
</ProgressTabs.Content>
</RouteFocusModal.Body>
<RouteFocusModal.Footer>
@@ -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<CreateShippingOptionSchema>
type: FulfillmentSetType
}
export const CreateShippingOptionsPricesForm = ({
form,
type,
}: PricingPricesFormProps) => {
const isPickup = type === FulfillmentSetType.Pickup
const { getIsOpen, setIsOpen } = useStackedModal()
const [selectedPrice, setSelectedPrice] =
useState<ConditionalPriceInfo | null>(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
}
@@ -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}
/>
)}
</RouteFocusModal>
@@ -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 = ({
<RouteDrawer.Body>
<div className="flex flex-col gap-y-8">
<div className="flex flex-col gap-y-8">
<Form.Field
control={form.control}
name="price_type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t(
"stockLocations.shippingOptions.fields.priceType.label"
)}
</Form.Label>
<Form.Control>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup.ChoiceBox
className="flex-1"
value={ShippingOptionPriceType.FlatRate}
label={t(
"stockLocations.shippingOptions.fields.priceType.options.fixed.label"
)}
description={t(
"stockLocations.shippingOptions.fields.priceType.options.fixed.hint"
)}
/>
<RadioGroup.ChoiceBox
className="flex-1"
value={ShippingOptionPriceType.Calculated}
label={t(
"stockLocations.shippingOptions.fields.priceType.options.calculated.label"
)}
description={t(
"stockLocations.shippingOptions.fields.priceType.options.calculated.hint"
)}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
{!isPickup && (
<Form.Field
control={form.control}
name="price_type"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>
{t(
"stockLocations.shippingOptions.fields.priceType.label"
)}
</Form.Label>
<Form.Control>
<RadioGroup {...field} onValueChange={field.onChange}>
<RadioGroup.ChoiceBox
className="flex-1"
value={ShippingOptionPriceType.FlatRate}
label={t(
"stockLocations.shippingOptions.fields.priceType.options.fixed.label"
)}
description={t(
"stockLocations.shippingOptions.fields.priceType.options.fixed.hint"
)}
/>
<RadioGroup.ChoiceBox
className="flex-1"
value={ShippingOptionPriceType.Calculated}
label={t(
"stockLocations.shippingOptions.fields.priceType.options.calculated.label"
)}
description={t(
"stockLocations.shippingOptions.fields.priceType.options.calculated.hint"
)}
/>
</RadioGroup>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
)}
<div className="grid gap-y-4">
<Form.Field
@@ -193,18 +202,21 @@ export const EditShippingOptionForm = ({
/>
</div>
<Divider />
<SwitchBox
control={form.control}
name="enabled_in_store"
label={t(
"stockLocations.shippingOptions.fields.enableInStore.label"
)}
description={t(
"stockLocations.shippingOptions.fields.enableInStore.hint"
)}
/>
{!isPickup && (
<>
<Divider />
<SwitchBox
control={form.control}
name="enabled_in_store"
label={t(
"stockLocations.shippingOptions.fields.enableInStore.label"
)}
description={t(
"stockLocations.shippingOptions.fields.enableInStore.hint"
)}
/>
</>
)}
</div>
</div>
</RouteDrawer.Body>
@@ -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 (
<RouteDrawer>
<RouteDrawer.Header>
<Heading>{t("stockLocations.shippingOptions.edit.header")}</Heading>
<Heading>
{t(
`stockLocations.${isPickup ? "pickupOptions" : "shippingOptions"}.edit.header`
)}
</Heading>
</RouteDrawer.Header>
{shippingOption && (
<EditShippingOptionForm
shippingOption={shippingOption}
locationId={location_id!}
type={
shippingOption.service_zone.fulfillment_set
.type as FulfillmentSetType
}
/>
)}
</RouteDrawer>
@@ -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 = ({
<div className="bg-ui-bg-subtle flex items-center justify-end gap-x-2 rounded-b-xl px-4 py-4">
{showDeliveryButton && (
<Button onClick={handleMarkAsDelivered} variant="secondary">
{t("orders.fulfillment.markAsDelivered")}
{t(
isPickUpFulfillment
? "orders.fulfillment.markAsPickedUp"
: "orders.fulfillment.markAsDelivered"
)}
</Button>
)}
@@ -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",