Initial commit: Meshkee dashboards monorepo.
Includes business, customer, and super-admin apps with shared packages and production deploy scripts. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import type { Order } from '../services/orderService'
|
||||
import { formatCellForDisplay } from '../lib/cellNumber'
|
||||
import { formatIrtPrice } from '../utils/irtPrice'
|
||||
import modalStyles from './VariationsModal.module.css'
|
||||
import styles from './OrderItemsModal.module.css'
|
||||
|
||||
interface OrderItemsModalProps {
|
||||
open: boolean
|
||||
order: Order | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const ANIMATION_MS = 220
|
||||
|
||||
function displayName(order: Order) {
|
||||
const name = [order.customer.firstName, order.customer.lastName].filter(Boolean).join(' ').trim()
|
||||
return name || '—'
|
||||
}
|
||||
|
||||
function formatVariantLabel(selections: Order['items'][number]['selections']) {
|
||||
if (!selections.length) return '—'
|
||||
return selections.map((s) => `${s.variationName}: ${s.value}`).join(' · ')
|
||||
}
|
||||
|
||||
function totalQuantity(order: Order) {
|
||||
return order.items.reduce((sum, item) => sum + item.quantity, 0)
|
||||
}
|
||||
|
||||
export function OrderItemsModal({ open, order, onClose }: OrderItemsModalProps) {
|
||||
const [mounted, setMounted] = useState(open)
|
||||
const [closing, setClosing] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setMounted(true)
|
||||
setClosing(false)
|
||||
} else if (mounted) {
|
||||
setClosing(true)
|
||||
const timer = setTimeout(() => {
|
||||
setMounted(false)
|
||||
setClosing(false)
|
||||
}, ANIMATION_MS)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [open, mounted])
|
||||
|
||||
useEffect(() => {
|
||||
if (!mounted || closing) return
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose()
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [mounted, closing, onClose])
|
||||
|
||||
if (!mounted || !order) return null
|
||||
|
||||
const itemCount = totalQuantity(order)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${modalStyles.overlay} ${closing ? modalStyles.overlayOut : modalStyles.overlayIn}`}
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className={`${modalStyles.modal} ${styles.modalWide} ${closing ? modalStyles.modalOut : modalStyles.modalIn}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="order-items-title"
|
||||
>
|
||||
<div className={modalStyles.header}>
|
||||
<div>
|
||||
<h2 id="order-items-title" className={modalStyles.title}>
|
||||
Order items
|
||||
</h2>
|
||||
<p className={modalStyles.subtitle}>{order.orderNumber}</p>
|
||||
</div>
|
||||
<button type="button" className={modalStyles.closeBtn} onClick={onClose} aria-label="Close">
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={`${modalStyles.body} ${styles.body}`}>
|
||||
<div className={styles.metaRow}>
|
||||
<span className={styles.metaItem}>
|
||||
Customer: <strong>{displayName(order)}</strong>
|
||||
</span>
|
||||
<span className={styles.metaItem}>
|
||||
Phone: <strong>{formatCellForDisplay(order.customer.cellNumber)}</strong>
|
||||
</span>
|
||||
<span className={styles.metaItem}>
|
||||
Items: <strong>{itemCount}</strong>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{order.items.length === 0 ? (
|
||||
<p className={styles.empty}>No items in this order.</p>
|
||||
) : (
|
||||
<div className={styles.tableBlock}>
|
||||
<div className={styles.tableHead}>
|
||||
<span aria-hidden="true" />
|
||||
<span>Product</span>
|
||||
<span>Qty</span>
|
||||
<span>Unit price</span>
|
||||
<span>Line total</span>
|
||||
</div>
|
||||
<ul className={styles.itemList}>
|
||||
{order.items.map((item) => (
|
||||
<li key={item.id} className={styles.itemRow}>
|
||||
<div className={styles.itemThumb}>
|
||||
{item.productImage ? (
|
||||
<img src={item.productImage} alt="" />
|
||||
) : (
|
||||
<div className={styles.itemThumbPlaceholder} />
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<div className={styles.itemTitle}>{item.productTitle}</div>
|
||||
<div className={styles.itemVariant}>{formatVariantLabel(item.selections)}</div>
|
||||
{item.variantSku && (
|
||||
<div className={styles.itemSku}>SKU: {item.variantSku}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.qtyCell}>{item.quantity}</div>
|
||||
<div className={styles.unitCell}>{formatIrtPrice(item.unitPrice)}</div>
|
||||
<div className={styles.totalCell}>{formatIrtPrice(item.lineTotal)}</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.summary}>
|
||||
<span className={styles.summaryLabel}>
|
||||
Order total · {itemCount} {itemCount === 1 ? 'item' : 'items'}
|
||||
</span>
|
||||
<span className={styles.summaryValue}>{formatIrtPrice(order.total)}</span>
|
||||
</div>
|
||||
|
||||
<div className={modalStyles.actions}>
|
||||
<button type="button" className={modalStyles.cancelBtn} onClick={onClose}>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user