From 6315a611896b2dbf01b0b0d23b852f28029ddf66 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Tue, 16 Jan 2024 15:23:45 +0100 Subject: [PATCH] feat(dashboard): add customers domain (#6093) --- packages/admin-next/dashboard/package.json | 2 +- .../public/locales/en/translation.json | 19 +- .../dashboard/scripts/generate-currencies.js | 46 ++ .../common/order-table-cells/index.ts | 1 + .../order-table-cells/order-table-cells.tsx | 97 +++ .../dashboard/src/lib/currencies.ts | 730 ++++++++++++++++++ .../dashboard/src/lib/money-amount-helpers.ts | 21 + .../router-provider/router-provider.tsx | 72 +- .../create-customer-form.tsx | 221 ++++++ .../components/create-customer-form/index.ts | 1 + .../customer-create/customer-create.tsx | 15 + .../routes/customers/customer-create/index.ts | 1 + .../customer-general-section.tsx | 73 ++ .../customer-general-section/index.ts | 1 + .../customer-group-section.tsx | 52 ++ .../customer-group-section/index.ts | 1 + .../customer-order-section.tsx | 247 ++++++ .../customer-order-section/index.ts | 1 + .../customer-detail/customer-detail.tsx | 39 + .../routes/customers/customer-detail/index.ts | 2 + .../customers/customer-detail/loader.ts | 21 + .../edit-customer-form/edit-customer-form.tsx | 154 ++++ .../components/edit-customer-form/index.ts | 1 + .../customers/customer-edit/customer-edit.tsx | 39 + .../routes/customers/customer-edit/index.ts | 1 + .../customer-list-table.tsx | 228 ++++++ .../components/customer-list-table/index.ts | 1 + .../customers/customer-list/customer-list.tsx | 11 + .../routes/customers/customer-list/index.ts | 1 + .../src/routes/customers/details/details.tsx | 11 - .../src/routes/customers/details/index.ts | 1 - .../src/routes/customers/list/index.ts | 1 - .../src/routes/customers/list/list.tsx | 11 - .../regions/region-create/region-create.tsx | 2 +- 34 files changed, 2073 insertions(+), 52 deletions(-) create mode 100644 packages/admin-next/dashboard/scripts/generate-currencies.js create mode 100644 packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts create mode 100644 packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx create mode 100644 packages/admin-next/dashboard/src/lib/currencies.ts create mode 100644 packages/admin-next/dashboard/src/lib/money-amount-helpers.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx create mode 100644 packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/customers/details/details.tsx delete mode 100644 packages/admin-next/dashboard/src/routes/customers/details/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/customers/list/index.ts delete mode 100644 packages/admin-next/dashboard/src/routes/customers/list/list.tsx diff --git a/packages/admin-next/dashboard/package.json b/packages/admin-next/dashboard/package.json index 68f7d41826..559d2a4a41 100644 --- a/packages/admin-next/dashboard/package.json +++ b/packages/admin-next/dashboard/package.json @@ -3,7 +3,7 @@ "private": true, "version": "0.0.0", "scripts": { - "generate:countries": "node ./scripts/generate-countries.js && prettier --write ./src/lib/countries.ts", + "generate:static": "node ./scripts/generate-countries.js && prettier --write ./src/lib/countries.ts && node ./scripts/generate-currencies.js && prettier --write ./src/lib/currencies.ts", "dev": "vite", "build": "vite build", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0" diff --git a/packages/admin-next/dashboard/public/locales/en/translation.json b/packages/admin-next/dashboard/public/locales/en/translation.json index 85ab3191e9..c1642e35c4 100644 --- a/packages/admin-next/dashboard/public/locales/en/translation.json +++ b/packages/admin-next/dashboard/public/locales/en/translation.json @@ -8,6 +8,7 @@ "create": "Create", "delete": "Delete", "edit": "Edit", + "confirm": "Confirm", "add": "Add", "continue": "Continue", "start": "Start", @@ -66,7 +67,17 @@ "domain": "Gift Cards" }, "customers": { - "domain": "Customers" + "domain": "Customers", + "editCustomer": "Edit Customer", + "createCustomer": "Create Customer", + "createCustomerHint": "Create a new customer to manage their details.", + "passwordHint": "Create a password for the customer to use when logging in to the storefront. Make sure that you communicate the password to the customer.", + "changePassword": "Change password", + "changePasswordPromptTitle": "Change password", + "changePasswordPromptDescription": "You are about to change the password for {{email}}. Make sure that you have communicated the new password to the customer before proceeding.", + "guest": "Guest", + "registered": "Registered", + "firstSeen": "First seen" }, "customerGroups": { "domain": "Customer Groups" @@ -161,6 +172,7 @@ "description": "Description", "email": "Email", "password": "Password", + "confirmPassword": "Confirm Password", "categories": "Categories", "category": "Category", "collection": "Collection", @@ -194,6 +206,9 @@ "phone": "Phone", "metadata": "Metadata", "selectCountry": "Select country", - "variants": "Variants" + "variants": "Variants", + "orders": "Orders", + "account": "Account", + "total": "Total" } } diff --git a/packages/admin-next/dashboard/scripts/generate-currencies.js b/packages/admin-next/dashboard/scripts/generate-currencies.js new file mode 100644 index 0000000000..bf6608f5eb --- /dev/null +++ b/packages/admin-next/dashboard/scripts/generate-currencies.js @@ -0,0 +1,46 @@ +async function generateCurrencies() { + const { currencies } = await import( + "@medusajs/medusa/dist/utils/currencies.js" + ) + const fs = await import("fs") + const path = await import("path") + + const record = Object.entries(currencies).reduce((acc, [key, values]) => { + const code = values.code + const symbol_native = values.symbol_native + const name = values.name + const decimal_digits = values.decimal_digits + + acc[key] = { + code, + name, + symbol_native, + decimal_digits, + } + + return acc + }, {}) + + const json = JSON.stringify(record, null, 2) + + const dest = path.join(__dirname, "../src/lib/currencies.ts") + const destDir = path.dirname(dest) + + const fileContent = `/** This file is auto-generated. Do not modify it manually. */\ntype CurrencyInfo = { code: string; name: string; symbol_native: string; decimal_digits: number }\n\nexport const currencies: Record = ${json}` + + if (!fs.existsSync(destDir)) { + fs.mkdirSync(destDir, { recursive: true }) + } + + fs.writeFileSync(dest, fileContent) +} + +;(async () => { + console.log("Generating currency info") + try { + await generateCurrencies() + console.log("Currency info generated") + } catch (e) { + console.error(e) + } +})() diff --git a/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts b/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts new file mode 100644 index 0000000000..20b29f6c85 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/order-table-cells/index.ts @@ -0,0 +1 @@ +export * from "./order-table-cells" diff --git a/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx b/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx new file mode 100644 index 0000000000..9026e45590 --- /dev/null +++ b/packages/admin-next/dashboard/src/components/common/order-table-cells/order-table-cells.tsx @@ -0,0 +1,97 @@ +import type { Order } from "@medusajs/medusa" +import { StatusBadge } from "@medusajs/ui" +import { format } from "date-fns" +import { getPresentationalAmount } from "../../../lib/money-amount-helpers" + +export const OrderDisplayIdCell = ({ id }: { id: Order["display_id"] }) => { + return #{id} +} + +export const OrderDateCell = ({ + date, +}: { + date: Order["created_at"] | string +}) => { + const value = new Date(date) + + return {format(value, "dd MMM, yyyy")} +} + +export const OrderFulfillmentStatusCell = ({ + status, +}: { + status: Order["fulfillment_status"] +}) => { + switch (status) { + case "not_fulfilled": + return Not fulfilled + case "partially_fulfilled": + return Partially fulfilled + case "fulfilled": + return Fulfilled + case "partially_shipped": + return Partially shipped + case "shipped": + return Shipped + case "partially_returned": + return Partially returned + case "returned": + return Returned + case "canceled": + return Canceled + case "requires_action": + return Requires action + } +} + +export const OrderPaymentStatusCell = ({ + status, +}: { + status: Order["payment_status"] +}) => { + switch (status) { + case "not_paid": + return Not paid + case "awaiting": + return Awaiting + case "captured": + return Captured + case "partially_refunded": + return Partially refunded + case "refunded": + return Refunded + case "canceled": + return Canceled + case "requires_action": + return Requires action + } +} + +// TODO: Fix formatting amount with correct division eg. EUR 1000 -> EUR 10.00 +// Source currency info from `@medusajs/medusa` definition +export const OrderTotalCell = ({ + total, + currencyCode, +}: { + total: Order["total"] + currencyCode: Order["currency_code"] +}) => { + const formatted = new Intl.NumberFormat(undefined, { + style: "currency", + currency: currencyCode, + currencyDisplay: "narrowSymbol", + }).format(0) + + const symbol = formatted.replace(/\d/g, "").replace(/[.,]/g, "").trim() + + const presentationAmount = getPresentationalAmount(total, currencyCode) + const formattedTotal = new Intl.NumberFormat(undefined, { + style: "decimal", + }).format(presentationAmount) + + return ( + + {symbol} {formattedTotal} {currencyCode.toUpperCase()} + + ) +} diff --git a/packages/admin-next/dashboard/src/lib/currencies.ts b/packages/admin-next/dashboard/src/lib/currencies.ts new file mode 100644 index 0000000000..ae22a07fbb --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/currencies.ts @@ -0,0 +1,730 @@ +/** This file is auto-generated. Do not modify it manually. */ +type CurrencyInfo = { + code: string + name: string + symbol_native: string + decimal_digits: number +} + +export const currencies: Record = { + USD: { + code: "USD", + name: "US Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + CAD: { + code: "CAD", + name: "Canadian Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + EUR: { + code: "EUR", + name: "Euro", + symbol_native: "€", + decimal_digits: 2, + }, + AED: { + code: "AED", + name: "United Arab Emirates Dirham", + symbol_native: "د.إ.‏", + decimal_digits: 2, + }, + AFN: { + code: "AFN", + name: "Afghan Afghani", + symbol_native: "؋", + decimal_digits: 0, + }, + ALL: { + code: "ALL", + name: "Albanian Lek", + symbol_native: "Lek", + decimal_digits: 0, + }, + AMD: { + code: "AMD", + name: "Armenian Dram", + symbol_native: "դր.", + decimal_digits: 0, + }, + ARS: { + code: "ARS", + name: "Argentine Peso", + symbol_native: "$", + decimal_digits: 2, + }, + AUD: { + code: "AUD", + name: "Australian Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + AZN: { + code: "AZN", + name: "Azerbaijani Manat", + symbol_native: "ман.", + decimal_digits: 2, + }, + BAM: { + code: "BAM", + name: "Bosnia-Herzegovina Convertible Mark", + symbol_native: "KM", + decimal_digits: 2, + }, + BDT: { + code: "BDT", + name: "Bangladeshi Taka", + symbol_native: "৳", + decimal_digits: 2, + }, + BGN: { + code: "BGN", + name: "Bulgarian Lev", + symbol_native: "лв.", + decimal_digits: 2, + }, + BHD: { + code: "BHD", + name: "Bahraini Dinar", + symbol_native: "د.ب.‏", + decimal_digits: 3, + }, + BIF: { + code: "BIF", + name: "Burundian Franc", + symbol_native: "FBu", + decimal_digits: 0, + }, + BND: { + code: "BND", + name: "Brunei Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + BOB: { + code: "BOB", + name: "Bolivian Boliviano", + symbol_native: "Bs", + decimal_digits: 2, + }, + BRL: { + code: "BRL", + name: "Brazilian Real", + symbol_native: "R$", + decimal_digits: 2, + }, + BWP: { + code: "BWP", + name: "Botswanan Pula", + symbol_native: "P", + decimal_digits: 2, + }, + BYN: { + code: "BYN", + name: "Belarusian Ruble", + symbol_native: "руб.", + decimal_digits: 2, + }, + BZD: { + code: "BZD", + name: "Belize Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + CDF: { + code: "CDF", + name: "Congolese Franc", + symbol_native: "FrCD", + decimal_digits: 2, + }, + CHF: { + code: "CHF", + name: "Swiss Franc", + symbol_native: "CHF", + decimal_digits: 2, + }, + CLP: { + code: "CLP", + name: "Chilean Peso", + symbol_native: "$", + decimal_digits: 0, + }, + CNY: { + code: "CNY", + name: "Chinese Yuan", + symbol_native: "CN¥", + decimal_digits: 2, + }, + COP: { + code: "COP", + name: "Colombian Peso", + symbol_native: "$", + decimal_digits: 0, + }, + CRC: { + code: "CRC", + name: "Costa Rican Colón", + symbol_native: "₡", + decimal_digits: 0, + }, + CVE: { + code: "CVE", + name: "Cape Verdean Escudo", + symbol_native: "CV$", + decimal_digits: 2, + }, + CZK: { + code: "CZK", + name: "Czech Republic Koruna", + symbol_native: "Kč", + decimal_digits: 2, + }, + DJF: { + code: "DJF", + name: "Djiboutian Franc", + symbol_native: "Fdj", + decimal_digits: 0, + }, + DKK: { + code: "DKK", + name: "Danish Krone", + symbol_native: "kr", + decimal_digits: 2, + }, + DOP: { + code: "DOP", + name: "Dominican Peso", + symbol_native: "RD$", + decimal_digits: 2, + }, + DZD: { + code: "DZD", + name: "Algerian Dinar", + symbol_native: "د.ج.‏", + decimal_digits: 2, + }, + EEK: { + code: "EEK", + name: "Estonian Kroon", + symbol_native: "kr", + decimal_digits: 2, + }, + EGP: { + code: "EGP", + name: "Egyptian Pound", + symbol_native: "ج.م.‏", + decimal_digits: 2, + }, + ERN: { + code: "ERN", + name: "Eritrean Nakfa", + symbol_native: "Nfk", + decimal_digits: 2, + }, + ETB: { + code: "ETB", + name: "Ethiopian Birr", + symbol_native: "Br", + decimal_digits: 2, + }, + GBP: { + code: "GBP", + name: "British Pound Sterling", + symbol_native: "£", + decimal_digits: 2, + }, + GEL: { + code: "GEL", + name: "Georgian Lari", + symbol_native: "GEL", + decimal_digits: 2, + }, + GHS: { + code: "GHS", + name: "Ghanaian Cedi", + symbol_native: "GH₵", + decimal_digits: 2, + }, + GNF: { + code: "GNF", + name: "Guinean Franc", + symbol_native: "FG", + decimal_digits: 0, + }, + GTQ: { + code: "GTQ", + name: "Guatemalan Quetzal", + symbol_native: "Q", + decimal_digits: 2, + }, + HKD: { + code: "HKD", + name: "Hong Kong Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + HNL: { + code: "HNL", + name: "Honduran Lempira", + symbol_native: "L", + decimal_digits: 2, + }, + HRK: { + code: "HRK", + name: "Croatian Kuna", + symbol_native: "kn", + decimal_digits: 2, + }, + HUF: { + code: "HUF", + name: "Hungarian Forint", + symbol_native: "Ft", + decimal_digits: 0, + }, + IDR: { + code: "IDR", + name: "Indonesian Rupiah", + symbol_native: "Rp", + decimal_digits: 0, + }, + ILS: { + code: "ILS", + name: "Israeli New Sheqel", + symbol_native: "₪", + decimal_digits: 2, + }, + INR: { + code: "INR", + name: "Indian Rupee", + symbol_native: "টকা", + decimal_digits: 2, + }, + IQD: { + code: "IQD", + name: "Iraqi Dinar", + symbol_native: "د.ع.‏", + decimal_digits: 0, + }, + IRR: { + code: "IRR", + name: "Iranian Rial", + symbol_native: "﷼", + decimal_digits: 0, + }, + ISK: { + code: "ISK", + name: "Icelandic Króna", + symbol_native: "kr", + decimal_digits: 0, + }, + JMD: { + code: "JMD", + name: "Jamaican Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + JOD: { + code: "JOD", + name: "Jordanian Dinar", + symbol_native: "د.أ.‏", + decimal_digits: 3, + }, + JPY: { + code: "JPY", + name: "Japanese Yen", + symbol_native: "¥", + decimal_digits: 0, + }, + KES: { + code: "KES", + name: "Kenyan Shilling", + symbol_native: "Ksh", + decimal_digits: 2, + }, + KHR: { + code: "KHR", + name: "Cambodian Riel", + symbol_native: "៛", + decimal_digits: 2, + }, + KMF: { + code: "KMF", + name: "Comorian Franc", + symbol_native: "FC", + decimal_digits: 0, + }, + KRW: { + code: "KRW", + name: "South Korean Won", + symbol_native: "₩", + decimal_digits: 0, + }, + KWD: { + code: "KWD", + name: "Kuwaiti Dinar", + symbol_native: "د.ك.‏", + decimal_digits: 3, + }, + KZT: { + code: "KZT", + name: "Kazakhstani Tenge", + symbol_native: "тңг.", + decimal_digits: 2, + }, + LBP: { + code: "LBP", + name: "Lebanese Pound", + symbol_native: "ل.ل.‏", + decimal_digits: 0, + }, + LKR: { + code: "LKR", + name: "Sri Lankan Rupee", + symbol_native: "SL Re", + decimal_digits: 2, + }, + LTL: { + code: "LTL", + name: "Lithuanian Litas", + symbol_native: "Lt", + decimal_digits: 2, + }, + LVL: { + code: "LVL", + name: "Latvian Lats", + symbol_native: "Ls", + decimal_digits: 2, + }, + LYD: { + code: "LYD", + name: "Libyan Dinar", + symbol_native: "د.ل.‏", + decimal_digits: 3, + }, + MAD: { + code: "MAD", + name: "Moroccan Dirham", + symbol_native: "د.م.‏", + decimal_digits: 2, + }, + MDL: { + code: "MDL", + name: "Moldovan Leu", + symbol_native: "MDL", + decimal_digits: 2, + }, + MGA: { + code: "MGA", + name: "Malagasy Ariary", + symbol_native: "MGA", + decimal_digits: 0, + }, + MKD: { + code: "MKD", + name: "Macedonian Denar", + symbol_native: "MKD", + decimal_digits: 2, + }, + MMK: { + code: "MMK", + name: "Myanma Kyat", + symbol_native: "K", + decimal_digits: 0, + }, + MNT: { + code: "MNT", + name: "Mongolian Tugrig", + symbol_native: "₮", + decimal_digits: 0, + }, + MOP: { + code: "MOP", + name: "Macanese Pataca", + symbol_native: "MOP$", + decimal_digits: 2, + }, + MUR: { + code: "MUR", + name: "Mauritian Rupee", + symbol_native: "MURs", + decimal_digits: 0, + }, + MXN: { + code: "MXN", + name: "Mexican Peso", + symbol_native: "$", + decimal_digits: 2, + }, + MYR: { + code: "MYR", + name: "Malaysian Ringgit", + symbol_native: "RM", + decimal_digits: 2, + }, + MZN: { + code: "MZN", + name: "Mozambican Metical", + symbol_native: "MTn", + decimal_digits: 2, + }, + NAD: { + code: "NAD", + name: "Namibian Dollar", + symbol_native: "N$", + decimal_digits: 2, + }, + NGN: { + code: "NGN", + name: "Nigerian Naira", + symbol_native: "₦", + decimal_digits: 2, + }, + NIO: { + code: "NIO", + name: "Nicaraguan Córdoba", + symbol_native: "C$", + decimal_digits: 2, + }, + NOK: { + code: "NOK", + name: "Norwegian Krone", + symbol_native: "kr", + decimal_digits: 2, + }, + NPR: { + code: "NPR", + name: "Nepalese Rupee", + symbol_native: "नेरू", + decimal_digits: 2, + }, + NZD: { + code: "NZD", + name: "New Zealand Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + OMR: { + code: "OMR", + name: "Omani Rial", + symbol_native: "ر.ع.‏", + decimal_digits: 3, + }, + PAB: { + code: "PAB", + name: "Panamanian Balboa", + symbol_native: "B/.", + decimal_digits: 2, + }, + PEN: { + code: "PEN", + name: "Peruvian Nuevo Sol", + symbol_native: "S/.", + decimal_digits: 2, + }, + PHP: { + code: "PHP", + name: "Philippine Peso", + symbol_native: "₱", + decimal_digits: 2, + }, + PKR: { + code: "PKR", + name: "Pakistani Rupee", + symbol_native: "₨", + decimal_digits: 0, + }, + PLN: { + code: "PLN", + name: "Polish Zloty", + symbol_native: "zł", + decimal_digits: 2, + }, + PYG: { + code: "PYG", + name: "Paraguayan Guarani", + symbol_native: "₲", + decimal_digits: 0, + }, + QAR: { + code: "QAR", + name: "Qatari Rial", + symbol_native: "ر.ق.‏", + decimal_digits: 2, + }, + RON: { + code: "RON", + name: "Romanian Leu", + symbol_native: "RON", + decimal_digits: 2, + }, + RSD: { + code: "RSD", + name: "Serbian Dinar", + symbol_native: "дин.", + decimal_digits: 0, + }, + RUB: { + code: "RUB", + name: "Russian Ruble", + symbol_native: "₽.", + decimal_digits: 2, + }, + RWF: { + code: "RWF", + name: "Rwandan Franc", + symbol_native: "FR", + decimal_digits: 0, + }, + SAR: { + code: "SAR", + name: "Saudi Riyal", + symbol_native: "ر.س.‏", + decimal_digits: 2, + }, + SDG: { + code: "SDG", + name: "Sudanese Pound", + symbol_native: "SDG", + decimal_digits: 2, + }, + SEK: { + code: "SEK", + name: "Swedish Krona", + symbol_native: "kr", + decimal_digits: 2, + }, + SGD: { + code: "SGD", + name: "Singapore Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + SOS: { + code: "SOS", + name: "Somali Shilling", + symbol_native: "Ssh", + decimal_digits: 0, + }, + SYP: { + code: "SYP", + name: "Syrian Pound", + symbol_native: "ل.س.‏", + decimal_digits: 0, + }, + THB: { + code: "THB", + name: "Thai Baht", + symbol_native: "฿", + decimal_digits: 2, + }, + TND: { + code: "TND", + name: "Tunisian Dinar", + symbol_native: "د.ت.‏", + decimal_digits: 3, + }, + TOP: { + code: "TOP", + name: "Tongan Paʻanga", + symbol_native: "T$", + decimal_digits: 2, + }, + TRY: { + code: "TRY", + name: "Turkish Lira", + symbol_native: "TL", + decimal_digits: 2, + }, + TTD: { + code: "TTD", + name: "Trinidad and Tobago Dollar", + symbol_native: "$", + decimal_digits: 2, + }, + TWD: { + code: "TWD", + name: "New Taiwan Dollar", + symbol_native: "NT$", + decimal_digits: 2, + }, + TZS: { + code: "TZS", + name: "Tanzanian Shilling", + symbol_native: "TSh", + decimal_digits: 0, + }, + UAH: { + code: "UAH", + name: "Ukrainian Hryvnia", + symbol_native: "₴", + decimal_digits: 2, + }, + UGX: { + code: "UGX", + name: "Ugandan Shilling", + symbol_native: "USh", + decimal_digits: 0, + }, + UYU: { + code: "UYU", + name: "Uruguayan Peso", + symbol_native: "$", + decimal_digits: 2, + }, + UZS: { + code: "UZS", + name: "Uzbekistan Som", + symbol_native: "UZS", + decimal_digits: 0, + }, + VEF: { + code: "VEF", + name: "Venezuelan Bolívar", + symbol_native: "Bs.F.", + decimal_digits: 2, + }, + VND: { + code: "VND", + name: "Vietnamese Dong", + symbol_native: "₫", + decimal_digits: 0, + }, + XAF: { + code: "XAF", + name: "CFA Franc BEAC", + symbol_native: "FCFA", + decimal_digits: 0, + }, + XOF: { + code: "XOF", + name: "CFA Franc BCEAO", + symbol_native: "CFA", + decimal_digits: 0, + }, + YER: { + code: "YER", + name: "Yemeni Rial", + symbol_native: "ر.ي.‏", + decimal_digits: 0, + }, + ZAR: { + code: "ZAR", + name: "South African Rand", + symbol_native: "R", + decimal_digits: 2, + }, + ZMK: { + code: "ZMK", + name: "Zambian Kwacha", + symbol_native: "ZK", + decimal_digits: 0, + }, + ZWL: { + code: "ZWL", + name: "Zimbabwean Dollar", + symbol_native: "ZWL$", + decimal_digits: 0, + }, +} diff --git a/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts b/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts new file mode 100644 index 0000000000..d043af4f37 --- /dev/null +++ b/packages/admin-next/dashboard/src/lib/money-amount-helpers.ts @@ -0,0 +1,21 @@ +import { currencies } from "./currencies" + +export const getPresentationalAmount = (amount: number, currency: string) => { + const decimalDigits = currencies[currency.toUpperCase()].decimal_digits + + if (decimalDigits === 0) { + throw new Error("Currency has no decimal digits") + } + + return amount / 10 ** decimalDigits +} + +export const getDbAmount = (amount: number, currency: string) => { + const decimalDigits = currencies[currency.toUpperCase()].decimal_digits + + if (decimalDigits === 0) { + throw new Error("Currency has no decimal digits") + } + + return amount * 10 ** decimalDigits +} diff --git a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx index 34918b0f9c..4c30503aaf 100644 --- a/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx +++ b/packages/admin-next/dashboard/src/providers/router-provider/router-provider.tsx @@ -1,7 +1,12 @@ -import type { AdminProductsRes, AdminRegionsRes } from "@medusajs/medusa" +import type { + AdminCustomersRes, + AdminProductsRes, + AdminRegionsRes, +} from "@medusajs/medusa" import { Outlet, RouterProvider as Provider, + RouteObject, createBrowserRouter, } from "react-router-dom" @@ -11,25 +16,28 @@ import { MainLayout } from "../../components/layout/main-layout" import { PublicLayout } from "../../components/layout/public-layout" import { SettingsLayout } from "../../components/layout/settings-layout" -// const routeExtensions: RouteObject[] = routes.pages.map((ext) => { -// return { -// path: ext.path, -// async lazy() { -// const { default: Component } = await import(/* @vite-ignore */ ext.file) -// return { Component } -// }, -// } -// }) +import routes from "medusa-admin:routes/pages" +import settings from "medusa-admin:settings/pages" -// const settingsExtensions: RouteObject[] = settings.pages.map((ext) => { -// return { -// path: `/settings${ext.path}`, -// async lazy() { -// const { default: Component } = await import(/* @vite-ignore */ ext.file) -// return { Component } -// }, -// } -// }) +const routeExtensions: RouteObject[] = routes.pages.map((ext) => { + return { + path: ext.path, + async lazy() { + const { default: Component } = await import(/* @vite-ignore */ ext.file) + return { Component } + }, + } +}) + +const settingsExtensions: RouteObject[] = settings.pages.map((ext) => { + return { + path: `/settings${ext.path}`, + async lazy() { + const { default: Component } = await import(/* @vite-ignore */ ext.file) + return { Component } + }, + } +}) const router = createBrowserRouter([ { @@ -143,12 +151,28 @@ const router = createBrowserRouter([ }, children: [ { - index: true, - lazy: () => import("../../routes/customers/list"), + path: "", + lazy: () => import("../../routes/customers/customer-list"), + children: [ + { + path: "create", + lazy: () => + import("../../routes/customers/customer-create"), + }, + ], }, { path: ":id", - lazy: () => import("../../routes/customers/details"), + lazy: () => import("../../routes/customers/customer-detail"), + handle: { + crumb: (data: AdminCustomersRes) => data.customer.email, + }, + children: [ + { + path: "edit", + lazy: () => import("../../routes/customers/customer-edit"), + }, + ], }, ], }, @@ -463,10 +487,10 @@ const router = createBrowserRouter([ }, ], }, - // ...settingsExtensions, + ...settingsExtensions, ], }, - // ...routeExtensions, + ...routeExtensions, ], }, { diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx new file mode 100644 index 0000000000..067da3b031 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/create-customer-form.tsx @@ -0,0 +1,221 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, FocusModal, Heading, Input, Text } from "@medusajs/ui" +import { useAdminCreateCustomer } from "medusa-react" +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { useNavigate } from "react-router-dom" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" + +type CreateCustomerFormProps = { + subscribe: (state: boolean) => void +} + +const CreateCustomerSchema = zod + .object({ + email: zod.string().email(), + first_name: zod.string().min(1), + last_name: zod.string().min(1), + phone: zod.string().min(1).optional(), + password: zod.string().min(8), + password_confirmation: zod.string().min(8), + }) + .superRefine(({ password, password_confirmation }, ctx) => { + if (password !== password_confirmation) { + return ctx.addIssue({ + code: zod.ZodIssueCode.custom, + message: "Passwords do not match", + path: ["password_confirmation"], + }) + } + }) + +export const CreateCustomerForm = ({ subscribe }: CreateCustomerFormProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + + const form = useForm>({ + defaultValues: { + email: "", + first_name: "", + last_name: "", + phone: "", + password: "", + password_confirmation: "", + }, + resolver: zodResolver(CreateCustomerSchema), + }) + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { mutateAsync, isLoading } = useAdminCreateCustomer() + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + email: data.email, + first_name: data.first_name, + last_name: data.last_name, + phone: data.phone, + password: data.password, + }, + { + onSuccess: ({ customer }) => { + navigate(`/customers/${customer.id}`, { replace: true }) + }, + } + ) + }) + + return ( +
+ + +
+ + + + +
+
+ +
+
+ {t("customers.createCustomer")} + + {t("customers.createCustomerHint")} + +
+
+ { + return ( + + {t("fields.firstName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.lastName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.email")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+
+ + {t("fields.password")} + + + {t("customers.passwordHint")} + +
+
+ { + return ( + + {t("fields.password")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.confirmPassword")} + + + + + + ) + }} + /> +
+
+
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts new file mode 100644 index 0000000000..e3a418be98 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/components/create-customer-form/index.ts @@ -0,0 +1 @@ +export * from "./create-customer-form" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx new file mode 100644 index 0000000000..9d28afb2b2 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/customer-create.tsx @@ -0,0 +1,15 @@ +import { FocusModal } from "@medusajs/ui" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { CreateCustomerForm } from "./components/create-customer-form" + +export const CustomerCreate = () => { + const [open, onOpenChange, subscribe] = useRouteModalState() + + return ( + + + + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts new file mode 100644 index 0000000000..78c115ec15 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-create/index.ts @@ -0,0 +1 @@ +export { CustomerCreate as Component } from "./customer-create" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx new file mode 100644 index 0000000000..1fc3b5af2b --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/customer-general-section.tsx @@ -0,0 +1,73 @@ +import { Customer } from "@medusajs/medusa" +import { Button, Container, Heading, Text } from "@medusajs/ui" +import format from "date-fns/format" +import { ReactNode } from "react" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" + +type CustomerGeneralSectionProps = { + customer: Customer +} + +export const CustomerGeneralSection = ({ + customer, +}: CustomerGeneralSectionProps) => { + const { t } = useTranslation() + + return ( + +
+ {customer.email} + + + +
+
+ + + + + +
+
+ ) +} + +const Bulletpoint = ({ title, value }: { title: string; value: ReactNode }) => { + return ( +
+ + {title} + +
{value ?? "-"}
+
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts new file mode 100644 index 0000000000..ef9c4e73b9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-general-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-general-section" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx new file mode 100644 index 0000000000..db845763c0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/customer-group-section.tsx @@ -0,0 +1,52 @@ +import { Customer, CustomerGroup } from "@medusajs/medusa" +import { Container, Heading } from "@medusajs/ui" +import { createColumnHelper } from "@tanstack/react-table" +import { useAdminCustomerGroups } from "medusa-react" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +// TODO: Continue working on this when there is a natural way to get customer groups related to a customer. +type CustomerGroupSectionProps = { + customer: Customer +} + +export const CustomerGroupSection = ({ + customer, +}: CustomerGroupSectionProps) => { + const { customer_groups, isLoading, isError, error } = useAdminCustomerGroups( + { + id: customer.groups.map((g) => g.id).join(","), + } + ) + + if (isError) { + throw error + } + + return ( + +
+ Groups +
+
+ ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "select", + }), + columnHelper.accessor("name", { + header: t("fields.name"), + cell: ({ getValue }) => getValue(), + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts new file mode 100644 index 0000000000..421addaae6 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-group-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-group-section" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx new file mode 100644 index 0000000000..adc3b72ec9 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/customer-order-section.tsx @@ -0,0 +1,247 @@ +import { EllipsisHorizontal, ReceiptPercent } from "@medusajs/icons" +import { Customer, Order } from "@medusajs/medusa" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + Table, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminOrders } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" +import { NoRecords } from "../../../../../components/common/empty-table-content" +import { + OrderDateCell, + OrderDisplayIdCell, + OrderFulfillmentStatusCell, + OrderPaymentStatusCell, + OrderTotalCell, +} from "../../../../../components/common/order-table-cells" +import { Query } from "../../../../../components/filtering/query" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +type CustomerGeneralSectionProps = { + customer: Customer +} + +const PAGE_SIZE = 10 + +export const CustomerOrderSection = ({ + customer, +}: CustomerGeneralSectionProps) => { + const { t } = useTranslation() + const navigate = useNavigate() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const params = useQueryParams(["q"]) + const { orders, count, isLoading, isError, error } = useAdminOrders( + { + customer_id: customer.id, + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + fields: + "id,status,display_id,created_at,email,fulfillment_status,payment_status,total,currency_code", + ...params, + }, + { + keepPreviousData: true, + } + ) + + const columns = useColumns() + + const table = useReactTable({ + data: orders ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + const noRecords = + Object.values(params).every((v) => !v) && !isLoading && !orders?.length + + if (isError) { + throw error + } + + return ( + +
+ {t("orders.domain")} +
+ +
+
+ {!noRecords && ( +
+
+
+ +
+
+ )} + {noRecords ? ( + + ) : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/orders/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + ))} + +
+ +
+ )} +
+ ) +} + +const OrderActions = ({ order }: { order: Order }) => { + return ( + + + + + + + + + + + Go to order + + + + + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("display_id", { + header: "Order", + cell: ({ getValue }) => , + }), + columnHelper.accessor("created_at", { + header: "Date", + cell: ({ getValue }) => , + }), + columnHelper.accessor("fulfillment_status", { + header: "Fulfillment Status", + cell: ({ getValue }) => ( + + ), + }), + columnHelper.accessor("payment_status", { + header: "Payment Status", + cell: ({ getValue }) => , + }), + columnHelper.accessor("total", { + header: () => t("fields.total"), + cell: ({ getValue, row }) => ( + + ), + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts new file mode 100644 index 0000000000..bd72160c52 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/components/customer-order-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-order-section" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx new file mode 100644 index 0000000000..a2e9fd338c --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/customer-detail.tsx @@ -0,0 +1,39 @@ +import { useAdminCustomer } from "medusa-react" +import { Outlet, json, useLoaderData, useParams } from "react-router-dom" +import { JsonViewSection } from "../../../components/common/json-view-section" +import { CustomerGeneralSection } from "./components/customer-general-section" +import { CustomerOrderSection } from "./components/customer-order-section" +import { customerLoader } from "./loader" + +export const CustomerDetail = () => { + const { id } = useParams() + + const initialData = useLoaderData() as Awaited< + ReturnType + > + const { customer, isLoading, isError, error } = useAdminCustomer(id!, { + initialData, + }) + + if (isLoading) { + return
Loading...
+ } + + if (isError || !customer) { + if (error) { + throw error + } + + throw json("An unknown error occurred", 500) + } + + return ( +
+ + + {/* */} + + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts new file mode 100644 index 0000000000..bed23b93e7 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/index.ts @@ -0,0 +1,2 @@ +export { CustomerDetail as Component } from "./customer-detail" +export { customerLoader as loader } from "./loader" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts b/packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts new file mode 100644 index 0000000000..25d7f0c284 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-detail/loader.ts @@ -0,0 +1,21 @@ +import { AdminCustomersRes } from "@medusajs/medusa" +import { Response } from "@medusajs/medusa-js" +import { adminProductKeys } from "medusa-react" +import { LoaderFunctionArgs } from "react-router-dom" + +import { medusa, queryClient } from "../../../lib/medusa" + +const customerDetailQuery = (id: string) => ({ + queryKey: adminProductKeys.detail(id), + queryFn: async () => medusa.admin.customers.retrieve(id), +}) + +export const customerLoader = async ({ params }: LoaderFunctionArgs) => { + const id = params.id + const query = customerDetailQuery(id!) + + return ( + queryClient.getQueryData>(query.queryKey) ?? + (await queryClient.fetchQuery(query)) + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx new file mode 100644 index 0000000000..8b1cbc21d1 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/edit-customer-form.tsx @@ -0,0 +1,154 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Customer } from "@medusajs/medusa" +import { Button, Drawer, Input } from "@medusajs/ui" +import { useAdminUpdateCustomer } from "medusa-react" +import { useEffect } from "react" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" + +type EditCustomerFormProps = { + customer: Customer + subscribe: (state: boolean) => void + onSuccessfulSubmit: () => void +} + +const EditCustomerSchema = zod.object({ + email: zod.string().email(), + first_name: zod.string().min(1).optional(), + last_name: zod.string().min(1).optional(), + phone: zod.string().optional(), +}) + +export const EditCustomerForm = ({ + customer, + subscribe, + onSuccessfulSubmit, +}: EditCustomerFormProps) => { + const { t } = useTranslation() + + const form = useForm>({ + defaultValues: { + email: customer.email || "", + first_name: customer.first_name || "", + last_name: customer.last_name || "", + phone: customer.phone || "", + }, + resolver: zodResolver(EditCustomerSchema), + }) + + const { + formState: { isDirty }, + } = form + + useEffect(() => { + subscribe(isDirty) + }, [isDirty]) + + const { mutateAsync, isLoading } = useAdminUpdateCustomer(customer.id) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + email: customer.has_account ? undefined : data.email, + first_name: data.first_name, + last_name: data.last_name, + phone: data.phone, + }, + { + onSuccess: () => { + onSuccessfulSubmit() + }, + } + ) + }) + + return ( +
+ + +
+ { + return ( + + {t("fields.email")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.firstName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.lastName")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+ +
+ + + + +
+
+
+ + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts new file mode 100644 index 0000000000..a8ded89ad0 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/components/edit-customer-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-customer-form" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx new file mode 100644 index 0000000000..a453f1152e --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/customer-edit.tsx @@ -0,0 +1,39 @@ +import { Drawer, Heading } from "@medusajs/ui" +import { useAdminCustomer } from "medusa-react" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { useRouteModalState } from "../../../hooks/use-route-modal-state" +import { EditCustomerForm } from "./components/edit-customer-form" + +export const CustomerEdit = () => { + const [open, onOpenChange, subscribe] = useRouteModalState() + const { t } = useTranslation() + + const { id } = useParams() + const { customer, isLoading, isError, error } = useAdminCustomer(id!) + + const handleSuccessfulSubmit = () => { + onOpenChange(false, true) + } + + if (isError) { + throw error + } + + return ( + + + + {t("customers.editCustomer")} + + {!isLoading && customer && ( + + )} + + + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts new file mode 100644 index 0000000000..d70310f5ae --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-edit/index.ts @@ -0,0 +1 @@ +export { CustomerEdit as Component } from "./customer-edit" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx new file mode 100644 index 0000000000..1d0a8137ad --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/customer-list-table.tsx @@ -0,0 +1,228 @@ +import { EllipsisHorizontal, PencilSquare } from "@medusajs/icons" +import { Customer } from "@medusajs/medusa" +import { + Button, + Container, + DropdownMenu, + Heading, + IconButton, + StatusBadge, + Table, + clx, +} from "@medusajs/ui" +import { + PaginationState, + RowSelectionState, + createColumnHelper, + flexRender, + getCoreRowModel, + useReactTable, +} from "@tanstack/react-table" +import { useAdminCustomers } from "medusa-react" +import { useMemo, useState } from "react" +import { useTranslation } from "react-i18next" +import { Link, useNavigate } from "react-router-dom" +import { Query } from "../../../../../components/filtering/query" +import { LocalizedTablePagination } from "../../../../../components/localization/localized-table-pagination" +import { useQueryParams } from "../../../../../hooks/use-query-params" + +const PAGE_SIZE = 50 + +export const CustomerListTable = () => { + const { t } = useTranslation() + const navigate = useNavigate() + + const [{ pageIndex, pageSize }, setPagination] = useState({ + pageIndex: 0, + pageSize: PAGE_SIZE, + }) + + const pagination = useMemo( + () => ({ + pageIndex, + pageSize, + }), + [pageIndex, pageSize] + ) + + const [rowSelection, setRowSelection] = useState({}) + + const { q } = useQueryParams(["q"]) + const { customers, count, isLoading, isError, error } = useAdminCustomers({ + q, + limit: PAGE_SIZE, + offset: pageIndex * PAGE_SIZE, + }) + + const columns = useColumns() + + const table = useReactTable({ + data: customers ?? [], + columns, + pageCount: Math.ceil((count ?? 0) / PAGE_SIZE), + state: { + pagination, + rowSelection, + }, + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + manualPagination: true, + }) + + if (isError) { + throw error + } + + return ( + +
+ {t("customers.domain")} + + + +
+
+
+
+ +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => { + return ( + + {headerGroup.headers.map((header) => { + return ( + + {flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ) + })} + + + {table.getRowModel().rows.map((row) => ( + navigate(`/customers/${row.original.id}`)} + > + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + ))} + +
+ +
+
+ ) +} + +const CustomerActions = ({ customer }: { customer: Customer }) => { + const { t } = useTranslation() + + return ( + + + + + + + + + e.stopPropagation()} + > + + {t("general.edit")} + + + + + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.display({ + id: "name", + header: t("fields.name"), + cell: ({ row }) => { + const firstName = row.original.first_name + const lastName = row.original.last_name + + let value = "-" + + if (firstName && lastName) { + value = `${firstName} ${lastName}` + } else if (firstName) { + value = firstName + } else if (lastName) { + value = lastName + } + + return {value} + }, + }), + columnHelper.accessor("email", { + header: t("fields.email"), + cell: ({ getValue }) => {getValue()}, + }), + columnHelper.accessor("has_account", { + header: t("fields.account"), + cell: ({ getValue }) => { + const hasAccount = getValue() + + return ( + + {hasAccount ? t("customers.registered") : t("customers.guest")} + + ) + }, + }), + columnHelper.display({ + id: "actions", + cell: ({ row }) => , + }), + ], + [t] + ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts new file mode 100644 index 0000000000..c2e0ca7b09 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/components/customer-list-table/index.ts @@ -0,0 +1 @@ +export * from "./customer-list-table" diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx b/packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx new file mode 100644 index 0000000000..4a475d2450 --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/customer-list.tsx @@ -0,0 +1,11 @@ +import { Outlet } from "react-router-dom" +import { CustomerListTable } from "./components/customer-list-table" + +export const CustomersList = () => { + return ( +
+ + +
+ ) +} diff --git a/packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts b/packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts new file mode 100644 index 0000000000..f945dbca5d --- /dev/null +++ b/packages/admin-next/dashboard/src/routes/customers/customer-list/index.ts @@ -0,0 +1 @@ +export { CustomersList as Component } from "./customer-list" diff --git a/packages/admin-next/dashboard/src/routes/customers/details/details.tsx b/packages/admin-next/dashboard/src/routes/customers/details/details.tsx deleted file mode 100644 index 542e709a0b..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/details/details.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const CustomerDetails = () => { - return ( -
- - Customers - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/customers/details/index.ts b/packages/admin-next/dashboard/src/routes/customers/details/index.ts deleted file mode 100644 index 884ed5286c..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/details/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomerDetails as Component } from "./details"; diff --git a/packages/admin-next/dashboard/src/routes/customers/list/index.ts b/packages/admin-next/dashboard/src/routes/customers/list/index.ts deleted file mode 100644 index a4826e8aba..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/list/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { CustomersList as Component } from "./list"; diff --git a/packages/admin-next/dashboard/src/routes/customers/list/list.tsx b/packages/admin-next/dashboard/src/routes/customers/list/list.tsx deleted file mode 100644 index 7fdc230dd9..0000000000 --- a/packages/admin-next/dashboard/src/routes/customers/list/list.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { Container, Heading } from "@medusajs/ui"; - -export const CustomersList = () => { - return ( -
- - Customers - -
- ); -}; diff --git a/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx b/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx index e900d160a9..191e96b1d4 100644 --- a/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx +++ b/packages/admin-next/dashboard/src/routes/regions/region-create/region-create.tsx @@ -79,7 +79,7 @@ export const RegionCreate = () => { -
+