@fayz-ai/plugin-orders 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/OrdersContext.d.ts +40 -0
- package/dist/OrdersContext.d.ts.map +1 -0
- package/dist/OrdersPage-2IXARHDR.cjs +157 -0
- package/dist/OrdersPage-2IXARHDR.cjs.map +1 -0
- package/dist/OrdersPage-LJIEIZYE.js +155 -0
- package/dist/OrdersPage-LJIEIZYE.js.map +1 -0
- package/dist/OrdersPage.d.ts +13 -0
- package/dist/OrdersPage.d.ts.map +1 -0
- package/dist/chunk-7LFVFZKZ.js +23 -0
- package/dist/chunk-7LFVFZKZ.js.map +1 -0
- package/dist/chunk-WBGECNYU.cjs +28 -0
- package/dist/chunk-WBGECNYU.cjs.map +1 -0
- package/dist/data/fayz.d.ts +18 -0
- package/dist/data/fayz.d.ts.map +1 -0
- package/dist/data/mock.d.ts +3 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/types.d.ts +17 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/index.cjs +1185 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1178 -0
- package/dist/index.js.map +1 -0
- package/dist/locales/en.d.ts +2 -0
- package/dist/locales/en.d.ts.map +1 -0
- package/dist/locales/index.d.ts +2 -0
- package/dist/locales/index.d.ts.map +1 -0
- package/dist/locales/pt-BR.d.ts +2 -0
- package/dist/locales/pt-BR.d.ts.map +1 -0
- package/dist/registries.d.ts +3 -0
- package/dist/registries.d.ts.map +1 -0
- package/dist/store.d.ts +29 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/views/OrderKanbanView.d.ts +3 -0
- package/dist/views/OrderKanbanView.d.ts.map +1 -0
- package/package.json +54 -0
- package/src/OrdersContext.tsx +47 -0
- package/src/OrdersPage.tsx +20 -0
- package/src/data/fayz.ts +456 -0
- package/src/data/mock.ts +512 -0
- package/src/data/types.ts +23 -0
- package/src/index.ts +217 -0
- package/src/locales/en.ts +53 -0
- package/src/locales/index.ts +4 -0
- package/src/locales/pt-BR.ts +53 -0
- package/src/registries.ts +32 -0
- package/src/store.ts +191 -0
- package/src/types.ts +190 -0
- package/src/views/OrderKanbanView.tsx +240 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Orders Plugin — Pure TypeScript types
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// ENUMS / LITERALS
|
|
7
|
+
// ============================================================
|
|
8
|
+
|
|
9
|
+
export type OrderKind = 'dine_in' | 'takeout' | 'delivery'
|
|
10
|
+
|
|
11
|
+
export type DineInStatus = 'new' | 'confirmed' | 'preparing' | 'ready' | 'served' | 'completed' | 'cancelled'
|
|
12
|
+
export type TakeoutStatus = 'new' | 'confirmed' | 'preparing' | 'ready' | 'picked_up' | 'completed' | 'cancelled'
|
|
13
|
+
export type DeliveryStatus = 'new' | 'confirmed' | 'preparing' | 'ready' | 'dispatched' | 'delivered' | 'completed' | 'cancelled'
|
|
14
|
+
export type OrderStatus = DineInStatus | TakeoutStatus | DeliveryStatus
|
|
15
|
+
|
|
16
|
+
/** Unified columns for the kanban board */
|
|
17
|
+
export type KanbanColumn = 'new' | 'confirmed' | 'preparing' | 'ready' | 'out' | 'completed' | 'cancelled'
|
|
18
|
+
|
|
19
|
+
// ============================================================
|
|
20
|
+
// STATUS FLOW
|
|
21
|
+
// ============================================================
|
|
22
|
+
|
|
23
|
+
export const STATUS_FLOW: Record<OrderKind, OrderStatus[]> = {
|
|
24
|
+
dine_in: ['new', 'confirmed', 'preparing', 'ready', 'served', 'completed'],
|
|
25
|
+
takeout: ['new', 'confirmed', 'preparing', 'ready', 'picked_up', 'completed'],
|
|
26
|
+
delivery: ['new', 'confirmed', 'preparing', 'ready', 'dispatched', 'delivered', 'completed'],
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getNextStatus(kind: OrderKind, current: OrderStatus): OrderStatus | null {
|
|
30
|
+
const flow = STATUS_FLOW[kind]
|
|
31
|
+
const idx = flow.indexOf(current)
|
|
32
|
+
return idx >= 0 && idx < flow.length - 1 ? flow[idx + 1] : null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getPreviousStatus(kind: OrderKind, current: OrderStatus): OrderStatus | null {
|
|
36
|
+
const flow = STATUS_FLOW[kind]
|
|
37
|
+
const idx = flow.indexOf(current)
|
|
38
|
+
return idx > 0 ? flow[idx - 1] : null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mapToKanbanColumn(status: OrderStatus): KanbanColumn {
|
|
42
|
+
if (['served', 'picked_up', 'dispatched', 'delivered'].includes(status)) return 'out'
|
|
43
|
+
if (['new', 'confirmed', 'preparing', 'ready', 'completed', 'cancelled'].includes(status)) {
|
|
44
|
+
return status as KanbanColumn
|
|
45
|
+
}
|
|
46
|
+
return 'new'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const KANBAN_COLUMNS: KanbanColumn[] = ['new', 'confirmed', 'preparing', 'ready', 'out', 'completed']
|
|
50
|
+
|
|
51
|
+
// ============================================================
|
|
52
|
+
// CORE ENTITIES
|
|
53
|
+
// ============================================================
|
|
54
|
+
|
|
55
|
+
/** Restaurant order — saas_core.orders + public.orders extension */
|
|
56
|
+
export interface RestaurantOrder {
|
|
57
|
+
id: string
|
|
58
|
+
referenceNumber: string
|
|
59
|
+
kind: OrderKind
|
|
60
|
+
status: OrderStatus
|
|
61
|
+
partyId?: string
|
|
62
|
+
partyName?: string
|
|
63
|
+
assigneeId?: string
|
|
64
|
+
assigneeName?: string
|
|
65
|
+
locationId?: string
|
|
66
|
+
subtotal: number
|
|
67
|
+
discount: number
|
|
68
|
+
tax: number
|
|
69
|
+
total: number
|
|
70
|
+
currency: string
|
|
71
|
+
notes?: string
|
|
72
|
+
tags: string[]
|
|
73
|
+
// Extension fields
|
|
74
|
+
source: string
|
|
75
|
+
tableId?: string
|
|
76
|
+
tableName?: string
|
|
77
|
+
estimatedMinutes?: number
|
|
78
|
+
items: RestaurantOrderItem[]
|
|
79
|
+
metadata?: Record<string, unknown>
|
|
80
|
+
tenantId: string
|
|
81
|
+
createdAt: string
|
|
82
|
+
updatedAt: string
|
|
83
|
+
completedAt?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface RestaurantOrderItem {
|
|
87
|
+
id: string
|
|
88
|
+
orderId: string
|
|
89
|
+
productId?: string
|
|
90
|
+
name: string
|
|
91
|
+
description?: string
|
|
92
|
+
quantity: number
|
|
93
|
+
unitPrice: number
|
|
94
|
+
discount: number
|
|
95
|
+
total: number
|
|
96
|
+
sortOrder: number
|
|
97
|
+
modifiers?: string
|
|
98
|
+
kitchenNotes?: string
|
|
99
|
+
metadata?: Record<string, unknown>
|
|
100
|
+
createdAt: string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ============================================================
|
|
104
|
+
// INPUT TYPES
|
|
105
|
+
// ============================================================
|
|
106
|
+
|
|
107
|
+
export interface CreateOrderInput {
|
|
108
|
+
kind: OrderKind
|
|
109
|
+
source?: string
|
|
110
|
+
tableId?: string
|
|
111
|
+
partyId?: string
|
|
112
|
+
assigneeId?: string
|
|
113
|
+
notes?: string
|
|
114
|
+
estimatedMinutes?: number
|
|
115
|
+
items: CreateOrderItemInput[]
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface CreateOrderItemInput {
|
|
119
|
+
productId?: string
|
|
120
|
+
name: string
|
|
121
|
+
quantity: number
|
|
122
|
+
unitPrice: number
|
|
123
|
+
discount?: number
|
|
124
|
+
modifiers?: string
|
|
125
|
+
kitchenNotes?: string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface UpdateOrderStatusInput {
|
|
129
|
+
orderId: string
|
|
130
|
+
status: OrderStatus
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ============================================================
|
|
134
|
+
// QUERY TYPES
|
|
135
|
+
// ============================================================
|
|
136
|
+
|
|
137
|
+
export interface OrderQuery {
|
|
138
|
+
kind?: OrderKind | OrderKind[]
|
|
139
|
+
status?: OrderStatus | OrderStatus[]
|
|
140
|
+
source?: string
|
|
141
|
+
tableId?: string
|
|
142
|
+
assigneeId?: string
|
|
143
|
+
dateRange?: { from: string; to: string }
|
|
144
|
+
search?: string
|
|
145
|
+
page?: number
|
|
146
|
+
pageSize?: number
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface PaginatedResult<T> {
|
|
150
|
+
data: T[]
|
|
151
|
+
total: number
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ============================================================
|
|
155
|
+
// AGGREGATION
|
|
156
|
+
// ============================================================
|
|
157
|
+
|
|
158
|
+
export interface OrdersSummary {
|
|
159
|
+
todayOrderCount: number
|
|
160
|
+
todayRevenue: number
|
|
161
|
+
averageTicket: number
|
|
162
|
+
activeOrderCount: number
|
|
163
|
+
ordersByKind: Record<OrderKind, number>
|
|
164
|
+
ordersByStatus: Record<string, number>
|
|
165
|
+
topItems: Array<{ name: string; quantity: number; revenue: number }>
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ============================================================
|
|
169
|
+
// STATUS CONFIG
|
|
170
|
+
// ============================================================
|
|
171
|
+
|
|
172
|
+
export interface OrderStatusConfig {
|
|
173
|
+
value: string
|
|
174
|
+
label: string
|
|
175
|
+
color: string
|
|
176
|
+
icon?: string
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const DEFAULT_STATUS_CONFIGS: OrderStatusConfig[] = [
|
|
180
|
+
{ value: 'new', label: 'New', color: '#6366f1', icon: 'PlusCircle' },
|
|
181
|
+
{ value: 'confirmed', label: 'Confirmed', color: '#3b82f6', icon: 'CheckCircle' },
|
|
182
|
+
{ value: 'preparing', label: 'Preparing', color: '#f59e0b', icon: 'ChefHat' },
|
|
183
|
+
{ value: 'ready', label: 'Ready', color: '#10b981', icon: 'Bell' },
|
|
184
|
+
{ value: 'served', label: 'Served', color: '#06b6d4', icon: 'Utensils' },
|
|
185
|
+
{ value: 'picked_up', label: 'Picked Up', color: '#06b6d4', icon: 'ShoppingBag' },
|
|
186
|
+
{ value: 'dispatched', label: 'Dispatched', color: '#8b5cf6', icon: 'Truck' },
|
|
187
|
+
{ value: 'delivered', label: 'Delivered', color: '#06b6d4', icon: 'PackageCheck' },
|
|
188
|
+
{ value: 'completed', label: 'Completed', color: '#22c55e', icon: 'CircleCheck' },
|
|
189
|
+
{ value: 'cancelled', label: 'Cancelled', color: '#ef4444', icon: 'XCircle' },
|
|
190
|
+
]
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import React, { useEffect, useMemo } from 'react'
|
|
2
|
+
import { ChevronRight, Clock, Loader2, ClipboardList } from 'lucide-react'
|
|
3
|
+
import { useOrdersConfig, useOrdersStore, formatCurrency } from '../OrdersContext'
|
|
4
|
+
import { useTranslation } from '@fayz-ai/core'
|
|
5
|
+
import {
|
|
6
|
+
KANBAN_COLUMNS,
|
|
7
|
+
mapToKanbanColumn,
|
|
8
|
+
getNextStatus,
|
|
9
|
+
type KanbanColumn,
|
|
10
|
+
type RestaurantOrder,
|
|
11
|
+
} from '../types'
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
// Column config
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
const COLUMN_CONFIG: Record<KanbanColumn, { color: string }> = {
|
|
18
|
+
new: { color: 'bg-violet-500' },
|
|
19
|
+
confirmed: { color: 'bg-blue-500' },
|
|
20
|
+
preparing: { color: 'bg-amber-500' },
|
|
21
|
+
ready: { color: 'bg-emerald-500' },
|
|
22
|
+
out: { color: 'bg-cyan-500' },
|
|
23
|
+
completed: { color: 'bg-gray-400' },
|
|
24
|
+
cancelled: { color: 'bg-red-500' },
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Helpers
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
function timeAgo(dateStr: string): string {
|
|
32
|
+
const diff = Math.floor((Date.now() - new Date(dateStr).getTime()) / 60000)
|
|
33
|
+
if (diff < 1) return 'just now'
|
|
34
|
+
if (diff === 1) return '1min'
|
|
35
|
+
return `${diff}min`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Order Card
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
function OrderCard({
|
|
43
|
+
order,
|
|
44
|
+
onAdvance,
|
|
45
|
+
onSelect,
|
|
46
|
+
currency,
|
|
47
|
+
}: {
|
|
48
|
+
order: RestaurantOrder
|
|
49
|
+
onAdvance?: () => void
|
|
50
|
+
onSelect: () => void
|
|
51
|
+
currency: { code: string; locale: string; symbol: string }
|
|
52
|
+
}) {
|
|
53
|
+
const t = useTranslation()
|
|
54
|
+
const sourceLabel = t(`orders.source.${order.source}`) !== `orders.source.${order.source}`
|
|
55
|
+
? t(`orders.source.${order.source}`)
|
|
56
|
+
: order.source
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div
|
|
60
|
+
className="rounded-lg border bg-card p-3 hover:shadow-md transition-shadow cursor-pointer"
|
|
61
|
+
onClick={onSelect}
|
|
62
|
+
>
|
|
63
|
+
{/* Header: order number + source badge */}
|
|
64
|
+
<div className="flex items-center justify-between mb-2">
|
|
65
|
+
<span className="text-sm font-bold">{order.referenceNumber}</span>
|
|
66
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-[10px] font-medium">
|
|
67
|
+
{sourceLabel}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
{/* Customer + table */}
|
|
72
|
+
{order.partyName && (
|
|
73
|
+
<p className="text-sm font-medium truncate">{order.partyName}</p>
|
|
74
|
+
)}
|
|
75
|
+
{order.tableName && (
|
|
76
|
+
<p className="text-xs text-muted-foreground">{order.tableName}</p>
|
|
77
|
+
)}
|
|
78
|
+
|
|
79
|
+
{/* Items */}
|
|
80
|
+
{order.items.length > 0 && (
|
|
81
|
+
<div className="mt-2 space-y-0.5">
|
|
82
|
+
{order.items.map((item) => (
|
|
83
|
+
<p key={item.id} className="text-xs text-muted-foreground truncate">
|
|
84
|
+
{item.quantity}x {item.name}
|
|
85
|
+
</p>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
{/* Total + time */}
|
|
91
|
+
<div className="flex items-center justify-between mt-3 pt-2 border-t">
|
|
92
|
+
<span className="text-sm font-bold">
|
|
93
|
+
{formatCurrency(order.total, currency)}
|
|
94
|
+
</span>
|
|
95
|
+
<span className="inline-flex items-center gap-0.5 text-[10px] text-muted-foreground">
|
|
96
|
+
<Clock className="h-3 w-3" />
|
|
97
|
+
{timeAgo(order.createdAt)}
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Advance button */}
|
|
102
|
+
{onAdvance && (
|
|
103
|
+
<button
|
|
104
|
+
type="button"
|
|
105
|
+
className="mt-2 flex w-full items-center justify-center gap-1 rounded-md bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
106
|
+
onClick={(e) => {
|
|
107
|
+
e.stopPropagation()
|
|
108
|
+
onAdvance()
|
|
109
|
+
}}
|
|
110
|
+
>
|
|
111
|
+
{t('orders.kanban.advance')}
|
|
112
|
+
<ChevronRight className="h-3 w-3" />
|
|
113
|
+
</button>
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Skeleton column (loading state)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function SkeletonColumn() {
|
|
124
|
+
return (
|
|
125
|
+
<div className="flex-shrink-0 w-72">
|
|
126
|
+
<div className="h-9 rounded-lg bg-muted animate-pulse mb-3" />
|
|
127
|
+
<div className="space-y-3">
|
|
128
|
+
{[1, 2, 3].map((i) => (
|
|
129
|
+
<div key={i} className="h-32 rounded-lg bg-muted/60 animate-pulse" />
|
|
130
|
+
))}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// OrderKanbanView
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
export function OrderKanbanView() {
|
|
141
|
+
const t = useTranslation()
|
|
142
|
+
const config = useOrdersConfig()
|
|
143
|
+
|
|
144
|
+
const activeOrders = useOrdersStore((s) => s.activeOrders)
|
|
145
|
+
const activeOrdersLoading = useOrdersStore((s) => s.activeOrdersLoading)
|
|
146
|
+
const fetchActiveOrders = useOrdersStore((s) => s.fetchActiveOrders)
|
|
147
|
+
const advanceOrderStatus = useOrdersStore((s) => s.advanceOrderStatus)
|
|
148
|
+
const selectOrder = useOrdersStore((s) => s.selectOrder)
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
fetchActiveOrders()
|
|
152
|
+
}, [fetchActiveOrders])
|
|
153
|
+
|
|
154
|
+
// Group orders by kanban column
|
|
155
|
+
const grouped = useMemo(() => {
|
|
156
|
+
const map: Record<KanbanColumn, RestaurantOrder[]> = {
|
|
157
|
+
new: [],
|
|
158
|
+
confirmed: [],
|
|
159
|
+
preparing: [],
|
|
160
|
+
ready: [],
|
|
161
|
+
out: [],
|
|
162
|
+
completed: [],
|
|
163
|
+
cancelled: [],
|
|
164
|
+
}
|
|
165
|
+
for (const order of activeOrders) {
|
|
166
|
+
const col = mapToKanbanColumn(order.status)
|
|
167
|
+
map[col].push(order)
|
|
168
|
+
}
|
|
169
|
+
return map
|
|
170
|
+
}, [activeOrders])
|
|
171
|
+
|
|
172
|
+
// Loading state
|
|
173
|
+
if (activeOrdersLoading && activeOrders.length === 0) {
|
|
174
|
+
return (
|
|
175
|
+
<div className="flex gap-4 overflow-x-auto pb-4">
|
|
176
|
+
{KANBAN_COLUMNS.map((col) => (
|
|
177
|
+
<SkeletonColumn key={col} />
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Empty state
|
|
184
|
+
if (!activeOrdersLoading && activeOrders.length === 0) {
|
|
185
|
+
return (
|
|
186
|
+
<div className="flex flex-col items-center justify-center h-64 text-center">
|
|
187
|
+
<ClipboardList className="h-12 w-12 text-muted-foreground/40 mb-3" />
|
|
188
|
+
<p className="text-sm font-medium text-muted-foreground">
|
|
189
|
+
{t('orders.kanban.noOrders')}
|
|
190
|
+
</p>
|
|
191
|
+
<p className="text-xs text-muted-foreground/70 mt-1">
|
|
192
|
+
{t('orders.kanban.noOrdersDesc')}
|
|
193
|
+
</p>
|
|
194
|
+
</div>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="flex gap-4 overflow-x-auto pb-4">
|
|
200
|
+
{KANBAN_COLUMNS.map((col) => {
|
|
201
|
+
const columnOrders = grouped[col]
|
|
202
|
+
const colConfig = COLUMN_CONFIG[col]
|
|
203
|
+
const label = t(`orders.kanban.column.${col}`)
|
|
204
|
+
const hasNext = (order: RestaurantOrder) =>
|
|
205
|
+
getNextStatus(order.kind, order.status) !== null
|
|
206
|
+
|
|
207
|
+
return (
|
|
208
|
+
<div key={col} className="flex-shrink-0 w-72">
|
|
209
|
+
{/* Column header */}
|
|
210
|
+
<div className="flex items-center gap-2 px-3 py-2 rounded-lg mb-3">
|
|
211
|
+
<span className={`h-2.5 w-2.5 rounded-full ${colConfig.color}`} />
|
|
212
|
+
<span className="text-sm font-semibold">{label}</span>
|
|
213
|
+
<span className="ml-auto inline-flex items-center justify-center rounded-full bg-muted px-2 py-0.5 text-xs font-medium">
|
|
214
|
+
{columnOrders.length}
|
|
215
|
+
</span>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{/* Cards */}
|
|
219
|
+
<div className="space-y-3 max-h-[calc(100vh-16rem)] overflow-y-auto pr-1">
|
|
220
|
+
{columnOrders.map((order) => (
|
|
221
|
+
<OrderCard
|
|
222
|
+
key={order.id}
|
|
223
|
+
order={order}
|
|
224
|
+
currency={config.currency}
|
|
225
|
+
onSelect={() => selectOrder(order.id)}
|
|
226
|
+
onAdvance={hasNext(order) ? () => advanceOrderStatus(order.id) : undefined}
|
|
227
|
+
/>
|
|
228
|
+
))}
|
|
229
|
+
{columnOrders.length === 0 && (
|
|
230
|
+
<div className="text-center py-8 text-sm text-muted-foreground rounded-lg border border-dashed">
|
|
231
|
+
{t('orders.kanban.noOrders')}
|
|
232
|
+
</div>
|
|
233
|
+
)}
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
)
|
|
237
|
+
})}
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
}
|