feat(dashboard): add customers domain (#6093)
This commit is contained in:
1 parent
e28fa7fbdf
commit
6315a61189
34 files changed
+2073
-52
No files matched your search
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"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",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"create": "Create",
|
"create": "Create",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
|
"confirm": "Confirm",
|
||||||
"add": "Add",
|
"add": "Add",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
"start": "Start",
|
"start": "Start",
|
||||||
@@ -66,7 +67,17 @@
|
|||||||
"domain": "Gift Cards"
|
"domain": "Gift Cards"
|
||||||
},
|
},
|
||||||
"customers": {
|
"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": {
|
"customerGroups": {
|
||||||
"domain": "Customer Groups"
|
"domain": "Customer Groups"
|
||||||
@@ -161,6 +172,7 @@
|
|||||||
"description": "Description",
|
"description": "Description",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
|
"confirmPassword": "Confirm Password",
|
||||||
"categories": "Categories",
|
"categories": "Categories",
|
||||||
"category": "Category",
|
"category": "Category",
|
||||||
"collection": "Collection",
|
"collection": "Collection",
|
||||||
@@ -194,6 +206,9 @@
|
|||||||
"phone": "Phone",
|
"phone": "Phone",
|
||||||
"metadata": "Metadata",
|
"metadata": "Metadata",
|
||||||
"selectCountry": "Select country",
|
"selectCountry": "Select country",
|
||||||
"variants": "Variants"
|
"variants": "Variants",
|
||||||
|
"orders": "Orders",
|
||||||
|
"account": "Account",
|
||||||
|
"total": "Total"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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"
|
||||||
+97
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 {
|
import {
|
||||||
Outlet,
|
Outlet,
|
||||||
RouterProvider as Provider,
|
RouterProvider as Provider,
|
||||||
|
RouteObject,
|
||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
} from "react-router-dom"
|
} from "react-router-dom"
|
||||||
|
|
||||||
@@ -11,25 +16,28 @@ import { MainLayout } from "../../components/layout/main-layout"
|
|||||||
import { PublicLayout } from "../../components/layout/public-layout"
|
import { PublicLayout } from "../../components/layout/public-layout"
|
||||||
import { SettingsLayout } from "../../components/layout/settings-layout"
|
import { SettingsLayout } from "../../components/layout/settings-layout"
|
||||||
|
|
||||||
// const routeExtensions: RouteObject[] = routes.pages.map((ext) => {
|
import routes from "medusa-admin:routes/pages"
|
||||||
// return {
|
import settings from "medusa-admin:settings/pages"
|
||||||
// path: ext.path,
|
|
||||||
// async lazy() {
|
|
||||||
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
|
||||||
// return { Component }
|
|
||||||
// },
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
// const settingsExtensions: RouteObject[] = settings.pages.map((ext) => {
|
const routeExtensions: RouteObject[] = routes.pages.map((ext) => {
|
||||||
// return {
|
return {
|
||||||
// path: `/settings${ext.path}`,
|
path: ext.path,
|
||||||
// async lazy() {
|
async lazy() {
|
||||||
// const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
const { default: Component } = await import(/* @vite-ignore */ ext.file)
|
||||||
// return { Component }
|
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([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -143,12 +151,28 @@ const router = createBrowserRouter([
|
|||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
index: true,
|
path: "",
|
||||||
lazy: () => import("../../routes/customers/list"),
|
lazy: () => import("../../routes/customers/customer-list"),
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "create",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/customers/customer-create"),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ":id",
|
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,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+221
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./create-customer-form"
|
||||||
+15
@@ -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"
|
||||||
+73
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-general-section"
|
||||||
+52
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-group-section"
|
||||||
+247
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./customer-order-section"
|
||||||
+39
@@ -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))
|
||||||
|
)
|
||||||
|
}
|
||||||
+154
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -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"
|
||||||
+228
@@ -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]
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
@@ -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>
|
</div>
|
||||||
</FocusModal.Header>
|
</FocusModal.Header>
|
||||||
<FocusModal.Body className="flex flex-col items-center pt-[72px]">
|
<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>
|
<Heading></Heading>
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
|||||||
Reference in new issue
Block a user