feat(dashboard): add customers domain (#6093)

This commit is contained in:
Kasper Fabricius Kristensen
2024-01-16 15:23:45 +01:00
committed by GitHub
parent e28fa7fbdf
commit 6315a61189
34 changed files with 2073 additions and 52 deletions

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -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<string, CurrencyInfo> = ${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)
}
})()

View File

@@ -0,0 +1 @@
export * from "./order-table-cells"

View File

@@ -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 <span>#{id}</span>
}
export const OrderDateCell = ({
date,
}: {
date: Order["created_at"] | string
}) => {
const value = new Date(date)
return <span>{format(value, "dd MMM, yyyy")}</span>
}
export const OrderFulfillmentStatusCell = ({
status,
}: {
status: Order["fulfillment_status"]
}) => {
switch (status) {
case "not_fulfilled":
return <StatusBadge color="grey">Not fulfilled</StatusBadge>
case "partially_fulfilled":
return <StatusBadge color="orange">Partially fulfilled</StatusBadge>
case "fulfilled":
return <StatusBadge color="green">Fulfilled</StatusBadge>
case "partially_shipped":
return <StatusBadge color="orange">Partially shipped</StatusBadge>
case "shipped":
return <StatusBadge color="green">Shipped</StatusBadge>
case "partially_returned":
return <StatusBadge color="orange">Partially returned</StatusBadge>
case "returned":
return <StatusBadge color="green">Returned</StatusBadge>
case "canceled":
return <StatusBadge color="red">Canceled</StatusBadge>
case "requires_action":
return <StatusBadge color="orange">Requires action</StatusBadge>
}
}
export const OrderPaymentStatusCell = ({
status,
}: {
status: Order["payment_status"]
}) => {
switch (status) {
case "not_paid":
return <StatusBadge color="grey">Not paid</StatusBadge>
case "awaiting":
return <StatusBadge color="orange">Awaiting</StatusBadge>
case "captured":
return <StatusBadge color="green">Captured</StatusBadge>
case "partially_refunded":
return <StatusBadge color="orange">Partially refunded</StatusBadge>
case "refunded":
return <StatusBadge color="green">Refunded</StatusBadge>
case "canceled":
return <StatusBadge color="red">Canceled</StatusBadge>
case "requires_action":
return <StatusBadge color="orange">Requires action</StatusBadge>
}
}
// 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 (
<span>
{symbol} {formattedTotal} {currencyCode.toUpperCase()}
</span>
)
}

View File

@@ -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<string, CurrencyInfo> = {
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,
},
}

View File

@@ -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
}

View File

@@ -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,
],
},
{

View File

@@ -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<zod.infer<typeof CreateCustomerSchema>>({
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 (
<Form {...form}>
<form onSubmit={handleSubmit}>
<FocusModal.Header>
<div className="flex items-center justify-end gap-x-2">
<FocusModal.Close asChild>
<Button size="small" variant="secondary">
{t("general.cancel")}
</Button>
</FocusModal.Close>
<Button
size="small"
variant="primary"
type="submit"
isLoading={isLoading}
>
{t("general.create")}
</Button>
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-col items-center pt-[72px]">
<div className="w-full max-w-[720px] flex flex-col gap-y-10">
<div>
<Heading>{t("customers.createCustomer")}</Heading>
<Text size="small" className="text-ui-fg-subtle">
{t("customers.createCustomerHint")}
</Text>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Field
control={form.control}
name="first_name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.firstName")}</Form.Label>
<Form.Control>
<Input autoComplete="off" {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="last_name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.lastName")}</Form.Label>
<Form.Control>
<Input autoComplete="off" {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="email"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.email")}</Form.Label>
<Form.Control>
<Input autoComplete="off" {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="phone"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>{t("fields.phone")}</Form.Label>
<Form.Control>
<Input autoComplete="off" {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<div className="flex flex-col gap-y-4">
<div>
<Text size="small" leading="compact" weight="plus">
{t("fields.password")}
</Text>
<Text size="small" className="text-ui-fg-subtle">
{t("customers.passwordHint")}
</Text>
</div>
<div className="grid grid-cols-2 gap-4">
<Form.Field
control={form.control}
name="password"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.password")}</Form.Label>
<Form.Control>
<Input
autoComplete="off"
type="password"
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="password_confirmation"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.confirmPassword")}</Form.Label>
<Form.Control>
<Input
autoComplete="off"
type="password"
{...field}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</div>
</div>
</FocusModal.Body>
</form>
</Form>
)
}

View File

@@ -0,0 +1 @@
export * from "./create-customer-form"

View File

@@ -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 (
<FocusModal open={open} onOpenChange={onOpenChange}>
<FocusModal.Content>
<CreateCustomerForm subscribe={subscribe} />
</FocusModal.Content>
</FocusModal>
)
}

View File

@@ -0,0 +1 @@
export { CustomerCreate as Component } from "./customer-create"

View File

@@ -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 (
<Container className="px-6 py-4 flex flex-col gap-y-3">
<div className="flex items-center justify-between">
<Heading>{customer.email}</Heading>
<Link to={`/customers/${customer.id}/edit`}>
<Button size="small" variant="secondary">
{t("general.edit")}
</Button>
</Link>
</div>
<div className="grid grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-4">
<Bulletpoint
title={t("fields.name")}
value={
customer.first_name && customer.last_name
? `${customer.first_name} ${customer.last_name}`
: customer.last_name
? customer.last_name
: customer.first_name
? customer.first_name
: null
}
/>
<Bulletpoint
title={t("customers.firstSeen")}
value={format(new Date(customer.created_at), "MMM d, yyyy")}
/>
<Bulletpoint title="Phone" value={customer.phone} />
<Bulletpoint title="Orders" value={customer.orders.length} />
<Bulletpoint
title={t("fields.account")}
value={
customer.has_account
? t("customers.registered")
: t("customers.guest")
}
/>
</div>
</Container>
)
}
const Bulletpoint = ({ title, value }: { title: string; value: ReactNode }) => {
return (
<div className="flex flex-col flex-1">
<Text
size="small"
weight="plus"
leading="compact"
className="text-ui-fg-muted"
>
{title}
</Text>
<div className="text-ui-fg-subtle txt-small-plus">{value ?? "-"}</div>
</div>
)
}

View File

@@ -0,0 +1 @@
export * from "./customer-general-section"

View File

@@ -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 (
<Container className="p-0 divide-y">
<div className="px-6 py-4">
<Heading level="h2">Groups</Heading>
</div>
</Container>
)
}
const columnHelper = createColumnHelper<CustomerGroup>()
const useColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.display({
id: "select",
}),
columnHelper.accessor("name", {
header: t("fields.name"),
cell: ({ getValue }) => getValue(),
}),
],
[t]
)
}

View File

@@ -0,0 +1 @@
export * from "./customer-group-section"

View File

@@ -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<PaginationState>({
pageIndex: 0,
pageSize: PAGE_SIZE,
})
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
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 (
<Container className="p-0 divide-y">
<div className="flex items-center justify-between py-4 px-6">
<Heading level="h2">{t("orders.domain")}</Heading>
<div className="flex items-center gap-x-2">
<Button size="small" variant="secondary">
{t("general.create")}
</Button>
</div>
</div>
{!noRecords && (
<div className="flex items-center justify-between py-4 px-6">
<div></div>
<div className="flex items-center gap-x-2">
<Query />
</div>
</div>
)}
{noRecords ? (
<NoRecords />
) : (
<div>
<Table>
<Table.Header className="border-t-0">
{table.getHeaderGroups().map((headerGroup) => {
return (
<Table.Row
key={headerGroup.id}
className=" [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/5"
>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.HeaderCell>
)
})}
</Table.Row>
)
})}
</Table.Header>
<Table.Body className="border-b-0">
{table.getRowModel().rows.map((row) => (
<Table.Row
key={row.id}
className={clx(
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
{
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
row.getIsSelected(),
}
)}
onClick={() => navigate(`/orders/${row.original.id}`)}
>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
<LocalizedTablePagination
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
nextPage={table.nextPage}
previousPage={table.previousPage}
count={count ?? 0}
pageIndex={pageIndex}
pageCount={Math.ceil((count ?? 0) / PAGE_SIZE)}
pageSize={PAGE_SIZE}
/>
</div>
)}
</Container>
)
}
const OrderActions = ({ order }: { order: Order }) => {
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<IconButton size="small" variant="transparent">
<EllipsisHorizontal />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<Link to={`/orders/${order.id}`}>
<DropdownMenu.Item className="flex items-center gap-x-2">
<ReceiptPercent />
<span>Go to order</span>
</DropdownMenu.Item>
</Link>
</DropdownMenu.Content>
</DropdownMenu>
)
}
const columnHelper = createColumnHelper<any>()
const useColumns = () => {
const { t } = useTranslation()
return useMemo(
() => [
columnHelper.accessor("display_id", {
header: "Order",
cell: ({ getValue }) => <OrderDisplayIdCell id={getValue()} />,
}),
columnHelper.accessor("created_at", {
header: "Date",
cell: ({ getValue }) => <OrderDateCell date={getValue()} />,
}),
columnHelper.accessor("fulfillment_status", {
header: "Fulfillment Status",
cell: ({ getValue }) => (
<OrderFulfillmentStatusCell status={getValue()} />
),
}),
columnHelper.accessor("payment_status", {
header: "Payment Status",
cell: ({ getValue }) => <OrderPaymentStatusCell status={getValue()} />,
}),
columnHelper.accessor("total", {
header: () => t("fields.total"),
cell: ({ getValue, row }) => (
<OrderTotalCell
total={getValue()}
currencyCode={row.original.currency_code}
/>
),
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => <OrderActions order={row.original} />,
}),
],
[t]
)
}

View File

@@ -0,0 +1 @@
export * from "./customer-order-section"

View File

@@ -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<typeof customerLoader>
>
const { customer, isLoading, isError, error } = useAdminCustomer(id!, {
initialData,
})
if (isLoading) {
return <div>Loading...</div>
}
if (isError || !customer) {
if (error) {
throw error
}
throw json("An unknown error occurred", 500)
}
return (
<div className="flex flex-col gap-y-2">
<CustomerGeneralSection customer={customer} />
<CustomerOrderSection customer={customer} />
{/* <CustomerGroupSection customer={customer} /> */}
<JsonViewSection data={customer} />
<Outlet />
</div>
)
}

View File

@@ -0,0 +1,2 @@
export { CustomerDetail as Component } from "./customer-detail"
export { customerLoader as loader } from "./loader"

View File

@@ -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<Response<AdminCustomersRes>>(query.queryKey) ??
(await queryClient.fetchQuery(query))
)
}

View File

@@ -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<zod.infer<typeof EditCustomerSchema>>({
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 (
<Form {...form}>
<form onSubmit={handleSubmit} className="flex flex-1 flex-col">
<Drawer.Body>
<div className="flex flex-col gap-y-4">
<Form.Field
control={form.control}
name="email"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.email")}</Form.Label>
<Form.Control>
<Input {...field} disabled={customer.has_account} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="first_name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.firstName")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="last_name"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.lastName")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="phone"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.phone")}</Form.Label>
<Form.Control>
<Input {...field} />
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
</Drawer.Body>
<Drawer.Footer>
<div className="flex items-center justify-end gap-x-2">
<Drawer.Close asChild>
<Button variant="secondary" size="small">
{t("general.cancel")}
</Button>
</Drawer.Close>
<Button
isLoading={isLoading}
type="submit"
variant="primary"
size="small"
>
{t("general.save")}
</Button>
</div>
</Drawer.Footer>
</form>
</Form>
)
}

View File

@@ -0,0 +1 @@
export * from "./edit-customer-form"

View File

@@ -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 (
<Drawer open={open} onOpenChange={onOpenChange}>
<Drawer.Content>
<Drawer.Header>
<Heading>{t("customers.editCustomer")}</Heading>
</Drawer.Header>
{!isLoading && customer && (
<EditCustomerForm
customer={customer}
onSuccessfulSubmit={handleSuccessfulSubmit}
subscribe={subscribe}
/>
)}
</Drawer.Content>
</Drawer>
)
}

View File

@@ -0,0 +1 @@
export { CustomerEdit as Component } from "./customer-edit"

View File

@@ -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<PaginationState>({
pageIndex: 0,
pageSize: PAGE_SIZE,
})
const pagination = useMemo(
() => ({
pageIndex,
pageSize,
}),
[pageIndex, pageSize]
)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
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 (
<Container className="p-0 divide-y">
<div className="px-6 py-4 flex items-center justify-between">
<Heading>{t("customers.domain")}</Heading>
<Link to="/customers/create">
<Button size="small" variant="secondary">
{t("general.create")}
</Button>
</Link>
</div>
<div className="px-6 py-4 flex items-center justify-between">
<div></div>
<div className="flex items-center gap-x-2">
<Query />
</div>
</div>
<div>
<Table>
<Table.Header className="border-t-0">
{table.getHeaderGroups().map((headerGroup) => {
return (
<Table.Row
key={headerGroup.id}
className=" [&_th:last-of-type]:w-[1%] [&_th:last-of-type]:whitespace-nowrap [&_th]:w-1/3"
>
{headerGroup.headers.map((header) => {
return (
<Table.HeaderCell key={header.id}>
{flexRender(
header.column.columnDef.header,
header.getContext()
)}
</Table.HeaderCell>
)
})}
</Table.Row>
)
})}
</Table.Header>
<Table.Body className="border-b-0">
{table.getRowModel().rows.map((row) => (
<Table.Row
key={row.id}
className={clx(
"transition-fg cursor-pointer [&_td:last-of-type]:w-[1%] [&_td:last-of-type]:whitespace-nowrap",
{
"bg-ui-bg-highlight hover:bg-ui-bg-highlight-hover":
row.getIsSelected(),
}
)}
onClick={() => navigate(`/customers/${row.original.id}`)}
>
{row.getVisibleCells().map((cell) => (
<Table.Cell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</Table.Cell>
))}
</Table.Row>
))}
</Table.Body>
</Table>
<LocalizedTablePagination
canNextPage={table.getCanNextPage()}
canPreviousPage={table.getCanPreviousPage()}
nextPage={table.nextPage}
previousPage={table.previousPage}
count={count ?? 0}
pageIndex={pageIndex}
pageCount={table.getPageCount()}
pageSize={PAGE_SIZE}
/>
</div>
</Container>
)
}
const CustomerActions = ({ customer }: { customer: Customer }) => {
const { t } = useTranslation()
return (
<DropdownMenu>
<DropdownMenu.Trigger asChild>
<IconButton size="small" variant="transparent">
<EllipsisHorizontal />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content>
<Link to={`/customers/${customer.id}/edit`}>
<DropdownMenu.Item
className="flex items-center gap-x-2"
onClick={(e) => e.stopPropagation()}
>
<PencilSquare className="text-ui-fg-subtle" />
{t("general.edit")}
</DropdownMenu.Item>
</Link>
</DropdownMenu.Content>
</DropdownMenu>
)
}
const columnHelper = createColumnHelper<Customer>()
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 <span>{value}</span>
},
}),
columnHelper.accessor("email", {
header: t("fields.email"),
cell: ({ getValue }) => <span>{getValue()}</span>,
}),
columnHelper.accessor("has_account", {
header: t("fields.account"),
cell: ({ getValue }) => {
const hasAccount = getValue()
return (
<StatusBadge color={hasAccount ? "green" : "blue"}>
{hasAccount ? t("customers.registered") : t("customers.guest")}
</StatusBadge>
)
},
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => <CustomerActions customer={row.original} />,
}),
],
[t]
)
}

View File

@@ -0,0 +1 @@
export * from "./customer-list-table"

View File

@@ -0,0 +1,11 @@
import { Outlet } from "react-router-dom"
import { CustomerListTable } from "./components/customer-list-table"
export const CustomersList = () => {
return (
<div className="flex flex-col gap-y-2">
<CustomerListTable />
<Outlet />
</div>
)
}

View File

@@ -0,0 +1 @@
export { CustomersList as Component } from "./customer-list"

View File

@@ -1,11 +0,0 @@
import { Container, Heading } from "@medusajs/ui";
export const CustomerDetails = () => {
return (
<div>
<Container>
<Heading>Customers</Heading>
</Container>
</div>
);
};

View File

@@ -1 +0,0 @@
export { CustomerDetails as Component } from "./details";

View File

@@ -1 +0,0 @@
export { CustomersList as Component } from "./list";

View File

@@ -1,11 +0,0 @@
import { Container, Heading } from "@medusajs/ui";
export const CustomersList = () => {
return (
<div>
<Container>
<Heading>Customers</Heading>
</Container>
</div>
);
};

View File

@@ -79,7 +79,7 @@ export const RegionCreate = () => {
</div>
</FocusModal.Header>
<FocusModal.Body className="flex flex-col items-center pt-[72px]">
<div className="w-full max-w-[720px] flex flex-col gap-y-10">
<div className="w-full max-w-[720px] flex flex-col gap-y-8">
<Heading></Heading>
<div className="flex flex-col gap-y-4">
<div className="grid grid-cols-2 gap-4">