Adds support for configurable order views. https://github.com/user-attachments/assets/ed4a5f61-1667-4ed7-9478-423894f3eba6
372 lines
11 KiB
TypeScript
372 lines
11 KiB
TypeScript
import React from "react"
|
|
import { Badge, StatusBadge, Tooltip } from "@medusajs/ui"
|
|
import { HttpTypes } from "@medusajs/types"
|
|
import ReactCountryFlag from "react-country-flag"
|
|
import { getCountryByIso2 } from "./data/countries"
|
|
import { getStylizedAmount } from "./money-amount-helpers"
|
|
|
|
// Helper function to get nested value from object using dot notation
|
|
const getNestedValue = (obj: any, path: string) => {
|
|
return path.split('.').reduce((current, key) => current?.[key], obj)
|
|
}
|
|
|
|
// Helper function to format date
|
|
const formatDate = (date: string | Date, format: 'short' | 'long' | 'relative' = 'short') => {
|
|
const dateObj = new Date(date)
|
|
|
|
switch (format) {
|
|
case 'short':
|
|
return dateObj.toLocaleDateString('en-GB', {
|
|
day: 'numeric',
|
|
month: 'short',
|
|
year: 'numeric'
|
|
})
|
|
case 'long':
|
|
return dateObj.toLocaleDateString('en-GB', {
|
|
day: 'numeric',
|
|
month: 'long',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
})
|
|
case 'relative':
|
|
const now = new Date()
|
|
const diffInMs = now.getTime() - dateObj.getTime()
|
|
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24))
|
|
|
|
if (diffInDays === 0) return 'Today'
|
|
if (diffInDays === 1) return 'Yesterday'
|
|
if (diffInDays < 7) return `${diffInDays} days ago`
|
|
|
|
return dateObj.toLocaleDateString('en-GB', {
|
|
day: 'numeric',
|
|
month: 'short'
|
|
})
|
|
default:
|
|
return dateObj.toLocaleDateString()
|
|
}
|
|
}
|
|
|
|
// Payment status display
|
|
const PaymentStatusBadge = ({ status }: { status: string }) => {
|
|
const getStatusColor = (status: string) => {
|
|
switch (status?.toLowerCase()) {
|
|
case 'paid':
|
|
case 'captured':
|
|
return 'green'
|
|
case 'pending':
|
|
case 'awaiting':
|
|
return 'orange'
|
|
case 'failed':
|
|
case 'canceled':
|
|
return 'red'
|
|
default:
|
|
return 'grey'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<StatusBadge color={getStatusColor(status)}>
|
|
{status}
|
|
</StatusBadge>
|
|
)
|
|
}
|
|
|
|
// Fulfillment status display
|
|
const FulfillmentStatusBadge = ({ status }: { status: string }) => {
|
|
const getStatusColor = (status: string) => {
|
|
switch (status?.toLowerCase()) {
|
|
case 'fulfilled':
|
|
case 'shipped':
|
|
return 'green'
|
|
case 'partially_fulfilled':
|
|
case 'preparing':
|
|
return 'orange'
|
|
case 'canceled':
|
|
case 'returned':
|
|
return 'red'
|
|
case 'pending':
|
|
case 'not_fulfilled':
|
|
return 'grey'
|
|
default:
|
|
return 'grey'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<StatusBadge color={getStatusColor(status)}>
|
|
{status}
|
|
</StatusBadge>
|
|
)
|
|
}
|
|
|
|
// Generic status badge
|
|
const GenericStatusBadge = ({ status }: { status: string }) => {
|
|
return (
|
|
<Badge variant="outline" className="capitalize">
|
|
{status}
|
|
</Badge>
|
|
)
|
|
}
|
|
|
|
// Display strategies registry
|
|
export const DISPLAY_STRATEGIES = {
|
|
// Known semantic types with pixel-perfect display
|
|
status: {
|
|
payment: (value: any) => <PaymentStatusBadge status={value} />,
|
|
fulfillment: (value: any) => <FulfillmentStatusBadge status={value} />,
|
|
default: (value: any) => <GenericStatusBadge status={value} />
|
|
},
|
|
|
|
currency: {
|
|
default: (value: any, row: any) => {
|
|
if (value === null || value === undefined) return '-'
|
|
const currencyCode = row.currency_code || 'USD'
|
|
const formatted = getStylizedAmount(value, currencyCode)
|
|
|
|
return (
|
|
<div className="flex h-full w-full items-center justify-end text-right">
|
|
<span className="truncate">{formatted}</span>
|
|
</div>
|
|
)
|
|
}
|
|
},
|
|
|
|
timestamp: {
|
|
creation: (value: any) => value ? formatDate(value, 'short') : '-',
|
|
update: (value: any) => value ? formatDate(value, 'relative') : '-',
|
|
default: (value: any) => value ? formatDate(value, 'short') : '-'
|
|
},
|
|
|
|
identifier: {
|
|
order: (value: any) => `#${value}`,
|
|
default: (value: any) => value
|
|
},
|
|
|
|
email: {
|
|
default: (value: any) => value || '-'
|
|
},
|
|
|
|
// Generic fallbacks for custom fields
|
|
enum: {
|
|
default: (value: any) => <GenericStatusBadge status={value} />
|
|
},
|
|
|
|
// Base type fallbacks
|
|
string: {
|
|
default: (value: any) => value || '-'
|
|
},
|
|
|
|
number: {
|
|
default: (value: any) => value?.toLocaleString() || '0'
|
|
},
|
|
|
|
boolean: {
|
|
default: (value: any) => (
|
|
<Badge variant={value ? 'solid' : 'outline'}>
|
|
{value ? 'Yes' : 'No'}
|
|
</Badge>
|
|
)
|
|
},
|
|
|
|
object: {
|
|
relationship: (value: any) => {
|
|
if (!value || typeof value !== 'object') return '-'
|
|
|
|
// Try common display fields
|
|
if (value.name) return value.name
|
|
if (value.title) return value.title
|
|
if (value.email) return value.email
|
|
if (value.display_name) return value.display_name
|
|
|
|
return JSON.stringify(value)
|
|
},
|
|
default: (value: any) => {
|
|
if (!value || typeof value !== 'object') return '-'
|
|
|
|
// Try common display fields
|
|
if (value.name) return value.name
|
|
if (value.title) return value.title
|
|
if (value.email) return value.email
|
|
|
|
return JSON.stringify(value)
|
|
}
|
|
},
|
|
|
|
// Date types (in addition to timestamp)
|
|
date: {
|
|
default: (value: any) => value ? formatDate(value, 'short') : '-'
|
|
},
|
|
|
|
datetime: {
|
|
default: (value: any) => value ? formatDate(value, 'long') : '-'
|
|
},
|
|
|
|
// Computed columns
|
|
computed: {
|
|
display: (value: any) => value || '-',
|
|
default: (value: any) => value || '-'
|
|
}
|
|
}
|
|
|
|
// Strategy selection function
|
|
export const getDisplayStrategy = (column: any) => {
|
|
const semanticStrategies = DISPLAY_STRATEGIES[column.semantic_type as keyof typeof DISPLAY_STRATEGIES]
|
|
if (semanticStrategies) {
|
|
const contextStrategy = semanticStrategies[column.context as keyof typeof semanticStrategies]
|
|
if (contextStrategy) return contextStrategy
|
|
|
|
const defaultStrategy = semanticStrategies.default
|
|
if (defaultStrategy) return defaultStrategy
|
|
}
|
|
|
|
// Fallback to data type
|
|
// Map 'text' data type to 'string' strategy
|
|
const dataType = column.data_type === 'text' ? 'string' : column.data_type
|
|
const dataTypeStrategies = DISPLAY_STRATEGIES[dataType as keyof typeof DISPLAY_STRATEGIES]
|
|
if (dataTypeStrategies) {
|
|
const defaultStrategy = dataTypeStrategies.default
|
|
if (defaultStrategy) return defaultStrategy
|
|
}
|
|
|
|
// Final fallback
|
|
return (value: any) => String(value || '-')
|
|
}
|
|
|
|
// Computed column computation functions
|
|
export const COMPUTED_COLUMN_FUNCTIONS = {
|
|
customer_name: (row: any) => {
|
|
// Try customer object first
|
|
if (row.customer?.first_name || row.customer?.last_name) {
|
|
const fullName = `${row.customer.first_name || ''} ${row.customer.last_name || ''}`.trim()
|
|
if (fullName) return fullName
|
|
}
|
|
|
|
// Fall back to email
|
|
if (row.customer?.email) {
|
|
return row.customer.email
|
|
}
|
|
|
|
// Fall back to phone
|
|
if (row.customer?.phone) {
|
|
return row.customer.phone
|
|
}
|
|
|
|
return 'Guest'
|
|
},
|
|
|
|
address_summary: (row: any, column?: any) => {
|
|
// Determine which address to use based on the column field
|
|
let address = null
|
|
if (column?.field === 'shipping_address_display') {
|
|
address = row.shipping_address
|
|
} else if (column?.field === 'billing_address_display') {
|
|
address = row.billing_address
|
|
} else {
|
|
// Fallback to shipping address if no specific field
|
|
address = row.shipping_address || row.billing_address
|
|
}
|
|
|
|
if (!address) return '-'
|
|
|
|
// Build address parts in a meaningful order
|
|
const parts = []
|
|
|
|
// Include street address if available
|
|
if (address.address_1) {
|
|
parts.push(address.address_1)
|
|
}
|
|
|
|
// City, Province/State, Postal Code
|
|
const locationParts = []
|
|
if (address.city) locationParts.push(address.city)
|
|
if (address.province) locationParts.push(address.province)
|
|
if (address.postal_code) locationParts.push(address.postal_code)
|
|
|
|
if (locationParts.length > 0) {
|
|
parts.push(locationParts.join(', '))
|
|
}
|
|
|
|
// Country
|
|
if (address.country_code) {
|
|
parts.push(address.country_code.toUpperCase())
|
|
}
|
|
|
|
return parts.join(' • ') || '-'
|
|
},
|
|
|
|
country_code: (row: any) => {
|
|
// Get country code from shipping address
|
|
const countryCode = row.shipping_address?.country_code
|
|
|
|
if (!countryCode) return <div className="flex w-full justify-center">-</div>
|
|
|
|
// Get country information
|
|
const country = getCountryByIso2(countryCode)
|
|
const displayName = country?.display_name || countryCode.toUpperCase()
|
|
|
|
// Display country flag with tooltip - centered in the cell
|
|
return (
|
|
<div className="flex w-full items-center justify-center">
|
|
<Tooltip content={displayName}>
|
|
<div className="flex size-4 items-center justify-center overflow-hidden rounded-sm">
|
|
<ReactCountryFlag
|
|
countryCode={countryCode.toUpperCase()}
|
|
svg
|
|
style={{
|
|
width: "16px",
|
|
height: "16px",
|
|
}}
|
|
aria-label={displayName}
|
|
/>
|
|
</div>
|
|
</Tooltip>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
// Entity-specific column overrides
|
|
export const ENTITY_COLUMN_OVERRIDES = {
|
|
orders: {
|
|
// Override for customer column that combines multiple fields
|
|
customer: {
|
|
accessor: (row: any) => {
|
|
// Complex logic for combining fields
|
|
const shipping = row.shipping_address
|
|
const customer = row.customer
|
|
|
|
if (shipping?.first_name || shipping?.last_name) {
|
|
return `${shipping.first_name || ''} ${shipping.last_name || ''}`.trim()
|
|
}
|
|
if (customer?.first_name || customer?.last_name) {
|
|
return `${customer.first_name || ''} ${customer.last_name || ''}`.trim()
|
|
}
|
|
return customer?.email || 'Guest'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Helper function to get entity-specific accessor
|
|
export const getEntityAccessor = (entity: string, fieldName: string, column?: any) => {
|
|
// Check if this is a computed column
|
|
if (column?.computed) {
|
|
const computationFn = COMPUTED_COLUMN_FUNCTIONS[column.computed.type as keyof typeof COMPUTED_COLUMN_FUNCTIONS]
|
|
if (computationFn) {
|
|
// Return a wrapper function that passes the column info
|
|
return (row: any) => computationFn(row, column)
|
|
}
|
|
}
|
|
|
|
const entityOverrides = ENTITY_COLUMN_OVERRIDES[entity as keyof typeof ENTITY_COLUMN_OVERRIDES]
|
|
if (entityOverrides) {
|
|
const fieldOverride = entityOverrides[fieldName as keyof typeof entityOverrides]
|
|
if (fieldOverride?.accessor) {
|
|
return fieldOverride.accessor
|
|
}
|
|
}
|
|
|
|
// Default accessor using dot notation
|
|
return (row: any) => getNestedValue(row, fieldName)
|
|
} |