From 71efa1508803837d4ade9c002a17559eaad34c0b Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Mon, 25 Mar 2024 15:50:27 +0100 Subject: [PATCH] feat(dashboard): Order timeline (#6815) **What** - Adds the initial Timeline component. - Not all events have been added, as it makes sense to add them with their respective RMA flows. - Emoji picker is omitted from initial PR as it's a nice-to-have that we can add later. --- .../public/locales/en-US/translation.json | 42 ++ .../dashboard/src/hooks/use-date.tsx | 46 ++ .../admin-next/dashboard/src/i18n/config.ts | 1 - .../order-activity-section/index.ts | 1 + .../order-activity-section.tsx | 25 ++ .../order-note-form.tsx | 112 +++++ .../order-activity-section/order-timeline.tsx | 405 ++++++++++++++++++ .../order-fulfillment-section.tsx | 10 + .../routes/orders/order-detail/constants.ts | 2 +- .../orders/order-detail/order-detail.tsx | 3 + 10 files changed, 645 insertions(+), 2 deletions(-) create mode 100644 packages/admin-next/dashboard/src/hooks/use-date.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-activity-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-note-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx diff --git a/packages/admin-next/dashboard/public/locales/en-US/translation.json b/packages/admin-next/dashboard/public/locales/en-US/translation.json index 14283bf87d..d80810f47e 100644 --- a/packages/admin-next/dashboard/public/locales/en-US/translation.json +++ b/packages/admin-next/dashboard/public/locales/en-US/translation.json @@ -335,6 +335,47 @@ "transferOwnership": "Transfer ownership", "editBillingAddress": "Edit billing address", "editShippingAddress": "Edit shipping address" + }, + "activity": { + "header": "Activity", + "showMoreActivities_one": "Show {{count}} more activity", + "showMoreActivities_other": "Show {{count}} more activities", + "comment": { + "label": "Comment", + "placeholder": "Leave a comment", + "addButtonText": "Add comment", + "deleteButtonText": "Delete comment" + }, + "events": { + "placed": { + "title": "Order placed", + "fromSalesChannel": "from {{salesChannel}}" + }, + "canceled": { + "title": "Order canceled" + }, + "payment": { + "awaiting": "Awaiting payment", + "captured": "Payment captured", + "canceled": "Payment canceled" + }, + "fulfillment": { + "created": "Fulfillment created", + "canceled": "Fulfillment canceled", + "shipped": "Fulfillment shipped", + "itemsFulfilledFrom_one": "{{count}} item fulfilled from {{location}}", + "itemsFulfilledFrom_other": "{{count}} items fulfilled from {{location}}", + "itemsFulfilled_one": "{{count}} item fulfilled", + "itemsFulfilled_other": "{{count}} items fulfilled" + }, + "return": { + "created": "Return created" + }, + "note": { + "comment": "Comment", + "byLine": "by {{author}}" + } + } } }, "draftOrders": { @@ -814,6 +855,7 @@ "date": "Date", "order": "Order", "fulfillment": "Fulfillment", + "provider": "Provider", "payment": "Payment", "items": "Items", "salesChannel": "Sales Channel", diff --git a/packages/admin-next/dashboard/src/hooks/use-date.tsx b/packages/admin-next/dashboard/src/hooks/use-date.tsx new file mode 100644 index 0000000000..6a039cac22 --- /dev/null +++ b/packages/admin-next/dashboard/src/hooks/use-date.tsx @@ -0,0 +1,46 @@ +import { format, formatDistance, sub } from "date-fns" +import { enUS } from "date-fns/locale" +import { useTranslation } from "react-i18next" + +import { languages } from "../i18n/config" + +export const useDate = () => { + const { i18n } = useTranslation() + + const locale = + languages.find((l) => l.code === i18n.language)?.date_locale || enUS + + const getFullDate = ({ + date, + includeTime = false, + }: { + date: string | Date + includeTime?: boolean + }) => { + const ensuredDate = new Date(date) + + if (isNaN(ensuredDate.getTime())) { + return "" + } + + const timeFormat = includeTime ? "p" : "" + + return format(ensuredDate, `PP ${timeFormat}`, { + locale, + }) + } + + function getRelativeDate(date: string | Date): string { + const now = new Date() + + return formatDistance(sub(new Date(date), { minutes: 0 }), now, { + addSuffix: true, + locale, + }) + } + + return { + getFullDate, + getRelativeDate, + } +} diff --git a/packages/admin-next/dashboard/src/i18n/config.ts b/packages/admin-next/dashboard/src/i18n/config.ts index f8bdd59d9e..2b561e8c81 100644 --- a/packages/admin-next/dashboard/src/i18n/config.ts +++ b/packages/admin-next/dashboard/src/i18n/config.ts @@ -18,7 +18,6 @@ void i18n escapeValue: false, }, backend: { - // for all available options read the backend's repository readme file loadPath: "/locales/{{lng}}/{{ns}}.json", }, }) diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/index.ts b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/index.ts new file mode 100644 index 0000000000..9f94b9d3ea --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/index.ts @@ -0,0 +1 @@ +export * from "./order-activity-section" diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-activity-section.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-activity-section.tsx new file mode 100644 index 0000000000..f37743d7dd --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-activity-section.tsx @@ -0,0 +1,25 @@ +import { Order } from "@medusajs/medusa" +import { Container, Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { OrderNoteForm } from "./order-note-form" +import { OrderTimeline } from "./order-timeline" + +type OrderActivityProps = { + order: Order +} + +export const OrderActivitySection = ({ order }: OrderActivityProps) => { + const { t } = useTranslation() + + return ( + +
+
+ {t("orders.activity.header")} +
+ +
+ +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-note-form.tsx b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-note-form.tsx new file mode 100644 index 0000000000..df6119b984 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-note-form.tsx @@ -0,0 +1,112 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { ArrowUpCircleSolid } from "@medusajs/icons" +import { Order } from "@medusajs/medusa" +import { IconButton } from "@medusajs/ui" +import { useAdminCreateNote } from "medusa-react" +import { useRef } from "react" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { useTranslation } from "react-i18next" +import { Form } from "../../../../../components/common/form" + +type OrderNoteFormProps = { + order: Order +} + +const OrderNoteSchema = z.object({ + value: z.string().min(1), +}) + +export const OrderNoteForm = ({ order }: OrderNoteFormProps) => { + const { t } = useTranslation() + const textareaRef = useRef(null) + + const form = useForm>({ + defaultValues: { + value: "", + }, + resolver: zodResolver(OrderNoteSchema), + }) + + const { mutateAsync, isLoading } = useAdminCreateNote() + + const handleSubmit = form.handleSubmit(async (values) => { + mutateAsync( + { + resource_id: order.id, + resource_type: "order", + value: values.value, + }, + { + onSuccess: () => { + form.reset() + handleResetSize() + }, + } + ) + }) + + const handleResize = () => { + const textarea = textareaRef.current + if (textarea) { + textarea.style.height = "auto" + textarea.style.height = textarea.scrollHeight + "px" + } + } + + const handleResetSize = () => { + const textarea = textareaRef.current + if (textarea) { + textarea.style.height = "auto" + } + } + + return ( +
+
+ +
+ { + return ( + + + +