feat(dashboard): add customers domain (#6093)
This commit is contained in:
committed by
GitHub
parent
e28fa7fbdf
commit
6315a61189
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
46
packages/admin-next/dashboard/scripts/generate-currencies.js
Normal file
46
packages/admin-next/dashboard/scripts/generate-currencies.js
Normal 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)
|
||||
}
|
||||
})()
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./order-table-cells"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
730
packages/admin-next/dashboard/src/lib/currencies.ts
Normal file
730
packages/admin-next/dashboard/src/lib/currencies.ts
Normal 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,
|
||||
},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-customer-form"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CustomerCreate as Component } from "./customer-create"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./customer-general-section"
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./customer-group-section"
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./customer-order-section"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { CustomerDetail as Component } from "./customer-detail"
|
||||
export { customerLoader as loader } from "./loader"
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./edit-customer-form"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CustomerEdit as Component } from "./customer-edit"
|
||||
@@ -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]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./customer-list-table"
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CustomersList as Component } from "./customer-list"
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const CustomerDetails = () => {
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<Heading>Customers</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export { CustomerDetails as Component } from "./details";
|
||||
@@ -1 +0,0 @@
|
||||
export { CustomersList as Component } from "./list";
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Container, Heading } from "@medusajs/ui";
|
||||
|
||||
export const CustomersList = () => {
|
||||
return (
|
||||
<div>
|
||||
<Container>
|
||||
<Heading>Customers</Heading>
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user