From 87375db9ef14953cd146426e1f1eab2d100a2656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Tue, 2 Jul 2024 16:50:15 +0200 Subject: [PATCH] feat(dashboard): create shipment flow (#7898) **What** - add "Mark shipped" to Fulfillment section --- CLOSES CORE-2427 --- .../dashboard/src/hooks/api/fulfillment.tsx | 24 ++- .../dashboard/src/i18n/translations/en.json | 10 +- .../providers/router-provider/route-map.tsx | 5 + .../order-create-shipment-form/constants.ts | 15 ++ .../order-create-shipment-form/index.ts | 1 + .../order-create-shipment-form.tsx | 169 ++++++++++++++++++ .../orders/order-create-shipment/index.ts | 1 + .../order-create-shipment.tsx | 30 ++++ .../order-fulfillment-section.tsx | 28 ++- .../routes/orders/order-detail/constants.ts | 1 + packages/core/js-sdk/src/admin/fulfillment.ts | 4 +- packages/core/js-sdk/src/admin/order.ts | 1 - 12 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/constants.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/order-create-shipment-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-create-shipment/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-create-shipment/order-create-shipment.tsx diff --git a/packages/admin-next/dashboard/src/hooks/api/fulfillment.tsx b/packages/admin-next/dashboard/src/hooks/api/fulfillment.tsx index 9eaab956ba..d3e7821ad8 100644 --- a/packages/admin-next/dashboard/src/hooks/api/fulfillment.tsx +++ b/packages/admin-next/dashboard/src/hooks/api/fulfillment.tsx @@ -2,9 +2,10 @@ import { useMutation, UseMutationOptions } from "@tanstack/react-query" import { queryKeysFactory } from "../../lib/query-key-factory" -import { client } from "../../lib/client" +import { client, sdk } from "../../lib/client" import { queryClient } from "../../lib/query-client" import { ordersQueryKeys } from "./orders" +import { HttpTypes } from "@medusajs/types" const FULFILLMENTS_QUERY_KEY = "fulfillments" as const export const fulfillmentsQueryKeys = queryKeysFactory(FULFILLMENTS_QUERY_KEY) @@ -41,3 +42,24 @@ export const useCancelFulfillment = ( ...options, }) } + +export const useCreateShipment = ( + fulfillmentId: string, + options?: UseMutationOptions< + { order: HttpTypes.AdminOrder }, + Error, + HttpTypes.AdminCreateFulfillmentShipment + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminCreateFulfillmentShipment) => + sdk.admin.fulfillment.createShipment(fulfillmentId, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.details(), + }) + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index 007b385204..d734d29c78 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -676,6 +676,14 @@ "allocatedLabel": "Allocated", "notAllocatedLabel": "Not allocated" }, + "shipment": { + "title": "Mark fulfillment shipped", + "trackingNumber": "Tracking number", + "addTracking": "Add additional tracking number", + "sendNotification": "Send notification", + "sendNotificationHint": "Notify the customer about this shipment.", + "toastCreated": "Shipment created successfully." + }, "fulfillment": { "cancelWarning": "You are about to cancel a fulfillment. This action cannot be undone.", "unfulfilledItems": "Unfulfilled Items", @@ -683,12 +691,12 @@ "statusTitle": "Fulfillment Status", "fulfillItems": "Fulfill items", "awaitingFulfillmentBadge": "Awaiting fulfillment", - "awaitingFullfillmentBadge": "Awaiting fulfillment", "number": "Fulfillment #{{number}}", "itemsToFulfill": "Items to fulfill", "create": "Create Fulfillment", "available": "Available", "inStock": "In stock", + "markAsShipped": "Mark as shipped", "itemsToFulfillDesc": "Choose items and quantities to fulfill", "locationDescription": "Choose which location you want to fulfill items from.", "sendNotificationHint": "Notify customers about the created fulfillment.", diff --git a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx index 2577816017..295a81bf2c 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/route-map.tsx @@ -208,6 +208,11 @@ export const RouteMap: RouteObject[] = [ lazy: () => import("../../routes/orders/order-create-fulfillment"), }, + { + path: ":f_id/create-shipment", + lazy: () => + import("../../routes/orders/order-create-shipment"), + }, ], }, ], diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/constants.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/constants.ts new file mode 100644 index 0000000000..45df05e20c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/constants.ts @@ -0,0 +1,15 @@ +import { z } from "zod" + +export const CreateShipmentSchema = z.object({ + labels: z + .array( + z.object({ + tracking_number: z.string(), + // TODO: this 2 are not optional in the API + tracking_url: z.string().optional(), + label_url: z.string().optional(), + }) + ) + .min(1), + send_notification: z.boolean().optional(), +}) diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/index.ts new file mode 100644 index 0000000000..65ec3c1d1a --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/index.ts @@ -0,0 +1 @@ +export * from "./order-create-shipment-form.tsx" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/order-create-shipment-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/order-create-shipment-form.tsx new file mode 100644 index 0000000000..f98ff56fcb --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/components/order-create-shipment-form/order-create-shipment-form.tsx @@ -0,0 +1,169 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useTranslation } from "react-i18next" +import * as zod from "zod" + +import { AdminFulfillment, AdminOrder } from "@medusajs/types" +import { Button, Heading, Input, Switch, toast } from "@medusajs/ui" +import { useFieldArray, useForm } from "react-hook-form" + +import { Form } from "../../../../../components/common/form" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/modals" +import { useCreateOrderShipment } from "../../../../../hooks/api/orders" +import { CreateShipmentSchema } from "./constants" +import { useCreateShipment } from "../../../../../hooks/api/fulfillment.tsx" + +type OrderCreateFulfillmentFormProps = { + order: AdminOrder + fulfillment: AdminFulfillment +} + +export function OrderCreateShipmentForm({ + order, + fulfillment, +}: OrderCreateFulfillmentFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const { mutateAsync: createShipment, isPending: isMutating } = + useCreateShipment(fulfillment.id) + + const form = useForm>({ + defaultValues: { + labels: [{ tracking_number: "" }], + send_notification: !order.no_notification, //TODO: not supported in the API + }, + resolver: zodResolver(CreateShipmentSchema), + }) + + const { fields: labels, append } = useFieldArray({ + name: "labels", + control: form.control, + }) + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await createShipment({ + labels: data.labels + .filter((l) => !!l.tracking_number) + .map((l) => ({ + tracking_number: l.tracking_number, + tracking_url: "#", + label_url: "#", + })), + // no_notification: !data.send_notification, + }) + + handleSuccess(`/orders/${order.id}`) + + toast.success(t("general.success"), { + description: t("orders.shipment.toastCreated"), + dismissLabel: t("actions.close"), + }) + } catch (e) { + toast.error(t("general.error"), { + description: e.message, + dismissLabel: t("actions.close"), + }) + } + }) + + return ( + +
+ +
+ + + + +
+
+ +
+
+
+
+ + {t("orders.shipment.title")} + + + {labels.map((label, index) => ( + { + return ( + + {index === 0 && ( + + {t("orders.shipment.trackingNumber")} + + )} + + + + + + ) + }} + /> + ))} + + +
+ +
+ { + return ( + +
+ + {t("orders.shipment.sendNotification")} + + + + + + +
+ + {t("orders.shipment.sendNotificationHint")} + + +
+ ) + }} + /> +
+
+
+
+
+
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/index.ts new file mode 100644 index 0000000000..c075a5ffce --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/index.ts @@ -0,0 +1 @@ +export { OrderCreateShipment as Component } from "./order-create-shipment" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/order-create-shipment.tsx b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/order-create-shipment.tsx new file mode 100644 index 0000000000..bf48fe68f7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-create-shipment/order-create-shipment.tsx @@ -0,0 +1,30 @@ +import { useParams } from "react-router-dom" + +import { RouteFocusModal } from "../../../components/modals" +import { useOrder } from "../../../hooks/api/orders" +import { OrderCreateShipmentForm } from "./components/order-create-shipment-form" + +export function OrderCreateShipment() { + const { id, f_id } = useParams() + + const { order, isLoading, isError, error } = useOrder(id!, { + fields: "*fulfillments", + }) + + if (isError) { + throw error + } + + const ready = !isLoading && order + + return ( + + {ready && ( + f.id === f_id)} + /> + )} + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx index ac66d3cb72..eefbc6c1fe 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-fulfillment-section/order-fulfillment-section.tsx @@ -9,10 +9,11 @@ import { Tooltip, toast, usePrompt, + Button, } from "@medusajs/ui" import { format } from "date-fns" import { useTranslation } from "react-i18next" -import { Link } from "react-router-dom" +import { Link, useNavigate } from "react-router-dom" import { ActionMenu } from "../../../../../components/common/action-menu" import { Skeleton } from "../../../../../components/common/skeleton" import { Thumbnail } from "../../../../../components/common/thumbnail" @@ -157,6 +158,7 @@ const Fulfillment = ({ }) => { const { t } = useTranslation() const prompt = usePrompt() + const navigate = useNavigate() const showLocation = !!fulfillment.location_id @@ -168,8 +170,8 @@ const Fulfillment = ({ } ) - let statusText = "Fulfilled" - let statusColor: "orange" | "green" | "red" = "orange" + let statusText = "Awaiting shipping" + let statusColor: "blue" | "green" | "red" = "blue" let statusTimestamp = fulfillment.created_at if (fulfillment.canceled_at) { @@ -184,6 +186,8 @@ const Fulfillment = ({ const { mutateAsync } = useCancelOrderFulfillment(order.id, fulfillment.id) + const showShippingButton = !fulfillment.canceled_at && !fulfillment.shipped_at + const handleCancel = async () => { if (fulfillment.shipped_at) { toast.warning(t("general.warning"), { @@ -303,11 +307,11 @@ const Fulfillment = ({ {t("orders.fulfillment.trackingLabel")}
- {fulfillment.tracking_links && - fulfillment.tracking_links.length > 0 ? ( + {fulfillment.labels && fulfillment.labels.length > 0 ? (
+ {showShippingButton && ( +
+ +
+ )} ) } diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts index 7e46b4e6d0..cc63b01d98 100644 --- a/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/constants.ts @@ -24,6 +24,7 @@ const DEFAULT_RELATIONS = [ "*promotion", "*fulfillments", "*fulfillments.items", + "*fulfillments.labels", ] export const DEFAULT_FIELDS = `${DEFAULT_PROPERTIES.join( diff --git a/packages/core/js-sdk/src/admin/fulfillment.ts b/packages/core/js-sdk/src/admin/fulfillment.ts index d98792012c..d4d648e7ad 100644 --- a/packages/core/js-sdk/src/admin/fulfillment.ts +++ b/packages/core/js-sdk/src/admin/fulfillment.ts @@ -1,4 +1,4 @@ -import { HttpTypes } from "@medusajs/types" +import { HttpTypes, SelectParams } from "@medusajs/types" import { Client } from "../client" import { ClientHeaders } from "../types" @@ -46,7 +46,7 @@ export class Fulfillment { headers?: ClientHeaders ) { return await this.client.fetch( - `/admin/fulfillments/${id}/shipments`, + `/admin/fulfillments/${id}/shipment`, { method: "POST", headers, diff --git a/packages/core/js-sdk/src/admin/order.ts b/packages/core/js-sdk/src/admin/order.ts index 4c8c2574d6..6a3ff70ce3 100644 --- a/packages/core/js-sdk/src/admin/order.ts +++ b/packages/core/js-sdk/src/admin/order.ts @@ -1,5 +1,4 @@ import { - AdminCancelOrderFulfillment, FindParams, HttpTypes, PaginatedResponse,