@asteby/metacore-runtime-react 47.1.0 → 49.0.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/CHANGELOG.md +51 -0
- package/dist/action-modal-dispatcher.d.ts +17 -3
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +80 -13
- package/dist/action-priority.d.ts +34 -0
- package/dist/action-priority.d.ts.map +1 -0
- package/dist/action-priority.js +60 -0
- package/dist/business/catalog-product-search.d.ts +32 -0
- package/dist/business/catalog-product-search.d.ts.map +1 -0
- package/dist/business/catalog-product-search.js +71 -0
- package/dist/business/catalog.d.ts.map +1 -1
- package/dist/business/catalog.js +2 -0
- package/dist/business/document-editor-model.d.ts +119 -0
- package/dist/business/document-editor-model.d.ts.map +1 -0
- package/dist/business/document-editor-model.js +317 -0
- package/dist/business/document-editor-panels.d.ts +64 -0
- package/dist/business/document-editor-panels.d.ts.map +1 -0
- package/dist/business/document-editor-panels.js +58 -0
- package/dist/business/document-editor.d.ts +27 -0
- package/dist/business/document-editor.d.ts.map +1 -0
- package/dist/business/document-editor.js +493 -0
- package/dist/business/document-lines.d.ts.map +1 -1
- package/dist/business/document-lines.js +5 -1
- package/dist/business/index.d.ts +5 -0
- package/dist/business/index.d.ts.map +1 -1
- package/dist/business/index.js +5 -0
- package/dist/business/line-items.d.ts +16 -1
- package/dist/business/line-items.d.ts.map +1 -1
- package/dist/business/line-items.js +14 -2
- package/dist/business/payment-allocator.d.ts +19 -0
- package/dist/business/payment-allocator.d.ts.map +1 -0
- package/dist/business/payment-allocator.js +31 -0
- package/dist/business/product-search.d.ts +4 -0
- package/dist/business/product-search.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.d.ts.map +1 -1
- package/dist/dialogs/dynamic-record.js +14 -7
- package/dist/document/document-page.d.ts.map +1 -1
- package/dist/document/document-page.js +8 -1
- package/dist/document/evaluate.d.ts.map +1 -1
- package/dist/document/evaluate.js +17 -1
- package/dist/document/index.d.ts +1 -0
- package/dist/document/index.d.ts.map +1 -1
- package/dist/document/index.js +1 -0
- package/dist/document/secondary-bar.d.ts +20 -0
- package/dist/document/secondary-bar.d.ts.map +1 -0
- package/dist/document/secondary-bar.js +44 -0
- package/dist/document/types.d.ts +8 -0
- package/dist/document/types.d.ts.map +1 -1
- package/dist/document-form-dialog.d.ts +31 -2
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +46 -11
- package/dist/dynamic-columns-helpers.d.ts +9 -0
- package/dist/dynamic-columns-helpers.d.ts.map +1 -1
- package/dist/dynamic-columns-helpers.js +19 -0
- package/dist/dynamic-columns.d.ts +9 -13
- package/dist/dynamic-columns.d.ts.map +1 -1
- package/dist/dynamic-columns.js +27 -21
- package/dist/dynamic-crud-page.d.ts +18 -0
- package/dist/dynamic-crud-page.d.ts.map +1 -1
- package/dist/dynamic-crud-page.js +15 -6
- package/dist/dynamic-form-schema.d.ts.map +1 -1
- package/dist/dynamic-form-schema.js +15 -3
- package/dist/dynamic-form.d.ts +16 -1
- package/dist/dynamic-form.d.ts.map +1 -1
- package/dist/dynamic-form.js +78 -20
- package/dist/dynamic-kanban.d.ts +9 -3
- package/dist/dynamic-kanban.d.ts.map +1 -1
- package/dist/dynamic-kanban.js +31 -12
- package/dist/dynamic-relation-helpers.d.ts.map +1 -1
- package/dist/dynamic-relation-helpers.js +8 -0
- package/dist/dynamic-relation.js +1 -1
- package/dist/dynamic-row-actions.d.ts +8 -0
- package/dist/dynamic-row-actions.d.ts.map +1 -1
- package/dist/dynamic-row-actions.js +12 -2
- package/dist/dynamic-table.d.ts.map +1 -1
- package/dist/dynamic-table.js +64 -63
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -2
- package/dist/installed-addons-context.d.ts +15 -0
- package/dist/installed-addons-context.d.ts.map +1 -0
- package/dist/installed-addons-context.js +39 -0
- package/dist/org-runtime-context.d.ts +8 -0
- package/dist/org-runtime-context.d.ts.map +1 -1
- package/dist/org-runtime-context.js +8 -0
- package/dist/org-runtime-provider.d.ts +3 -1
- package/dist/org-runtime-provider.d.ts.map +1 -1
- package/dist/org-runtime-provider.js +3 -3
- package/dist/primitives/aging.d.ts +34 -0
- package/dist/primitives/aging.d.ts.map +1 -0
- package/dist/primitives/aging.js +48 -0
- package/dist/primitives/allocation.d.ts +42 -0
- package/dist/primitives/allocation.d.ts.map +1 -0
- package/dist/primitives/allocation.js +59 -0
- package/dist/primitives/approvals.d.ts +71 -0
- package/dist/primitives/approvals.d.ts.map +1 -0
- package/dist/primitives/approvals.js +48 -0
- package/dist/primitives/contributions.d.ts +93 -0
- package/dist/primitives/contributions.d.ts.map +1 -0
- package/dist/primitives/contributions.js +163 -0
- package/dist/primitives/document-kinds.d.ts +25 -0
- package/dist/primitives/document-kinds.d.ts.map +1 -0
- package/dist/primitives/document-kinds.js +26 -0
- package/dist/primitives/index.d.ts +8 -0
- package/dist/primitives/index.d.ts.map +1 -0
- package/dist/primitives/index.js +23 -0
- package/dist/primitives/panels.d.ts +68 -0
- package/dist/primitives/panels.d.ts.map +1 -0
- package/dist/primitives/panels.js +3 -0
- package/dist/primitives/use-contributions.d.ts +15 -0
- package/dist/primitives/use-contributions.d.ts.map +1 -0
- package/dist/primitives/use-contributions.js +30 -0
- package/dist/record-actions.d.ts +21 -0
- package/dist/record-actions.d.ts.map +1 -0
- package/dist/record-actions.js +76 -0
- package/dist/record-prefill-bar.d.ts +11 -0
- package/dist/record-prefill-bar.d.ts.map +1 -0
- package/dist/record-prefill-bar.js +33 -0
- package/dist/row-actions-menu.d.ts +32 -0
- package/dist/row-actions-menu.d.ts.map +1 -0
- package/dist/row-actions-menu.js +59 -0
- package/dist/slot.d.ts +4 -21
- package/dist/slot.d.ts.map +1 -1
- package/dist/slot.js +14 -36
- package/dist/types.d.ts +134 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/__tests__/catalog-product-search.test.ts +61 -0
- package/src/__tests__/document-editor-model.test.ts +221 -0
- package/src/__tests__/document-editor.test.tsx +340 -0
- package/src/__tests__/document-form-dialog.test.tsx +158 -3
- package/src/__tests__/dynamic-form-select-edit.test.tsx +155 -0
- package/src/__tests__/dynamic-kanban.test.tsx +24 -3
- package/src/__tests__/dynamic-record-extension-label.test.tsx +88 -0
- package/src/__tests__/installed-addons-context.test.tsx +50 -0
- package/src/__tests__/line-item-totals.test.ts +28 -0
- package/src/__tests__/prefill-scalar-from-record.test.ts +52 -1
- package/src/__tests__/primitives-federated.test.tsx +156 -0
- package/src/__tests__/primitives.test.ts +224 -0
- package/src/__tests__/record-prefill-bar.test.tsx +50 -0
- package/src/__tests__/row-action-link-navigation.test.ts +17 -0
- package/src/__tests__/secondary-actions.test.tsx +261 -0
- package/src/action-modal-dispatcher.tsx +97 -12
- package/src/action-priority.ts +82 -0
- package/src/business/business.test.ts +1 -1
- package/src/business/catalog-product-search.ts +105 -0
- package/src/business/catalog.ts +2 -0
- package/src/business/document-editor-model.ts +367 -0
- package/src/business/document-editor-panels.tsx +164 -0
- package/src/business/document-editor.tsx +781 -0
- package/src/business/document-lines.ts +5 -1
- package/src/business/index.ts +5 -0
- package/src/business/line-items.ts +31 -2
- package/src/business/payment-allocator.tsx +110 -0
- package/src/business/product-search.ts +4 -0
- package/src/dialogs/dynamic-record.tsx +29 -6
- package/src/document/document-page.tsx +15 -0
- package/src/document/evaluate.ts +17 -1
- package/src/document/index.ts +1 -0
- package/src/document/secondary-bar.tsx +92 -0
- package/src/document/types.ts +8 -0
- package/src/document-form-dialog.tsx +96 -12
- package/src/dynamic-columns-helpers.ts +22 -0
- package/src/dynamic-columns.tsx +35 -62
- package/src/dynamic-crud-page.tsx +33 -5
- package/src/dynamic-form-schema.ts +15 -2
- package/src/dynamic-form.tsx +117 -39
- package/src/dynamic-kanban.tsx +34 -7
- package/src/dynamic-relation-helpers.ts +9 -0
- package/src/dynamic-relation.tsx +7 -2
- package/src/dynamic-row-actions.tsx +12 -2
- package/src/dynamic-table.tsx +3 -0
- package/src/index.ts +46 -1
- package/src/installed-addons-context.tsx +58 -0
- package/src/org-runtime-context.ts +10 -0
- package/src/org-runtime-provider.tsx +7 -2
- package/src/primitives/aging.ts +76 -0
- package/src/primitives/allocation.ts +104 -0
- package/src/primitives/approvals.ts +103 -0
- package/src/primitives/contributions.ts +255 -0
- package/src/primitives/document-kinds.ts +62 -0
- package/src/primitives/index.ts +23 -0
- package/src/primitives/panels.ts +59 -0
- package/src/primitives/use-contributions.ts +42 -0
- package/src/record-actions.ts +101 -0
- package/src/record-prefill-bar.tsx +62 -0
- package/src/row-action-stage-gate.test.ts +33 -0
- package/src/row-actions-menu.tsx +159 -0
- package/src/slot.tsx +18 -53
- package/src/types.ts +129 -0
|
@@ -31,17 +31,21 @@ export function applyProductToLine(
|
|
|
31
31
|
const list = variant?.price ?? product.price
|
|
32
32
|
const cost = variant?.cost ?? product.cost
|
|
33
33
|
const catalog = source === 'cost' ? (cost ?? list) : list
|
|
34
|
+
// Nunca cantidad 0: un renglón vacío (o con la celda borrada) entra con 1.
|
|
35
|
+
const qty = opts.quantity ?? toAmount(line.quantity)
|
|
34
36
|
return {
|
|
35
37
|
...line,
|
|
36
38
|
kind: 'item',
|
|
37
39
|
product_id: variant?.id ?? product.id,
|
|
38
40
|
sku: variant?.sku ?? product.sku,
|
|
39
41
|
description: variant ? `${product.name} · ${variant.label}` : product.name,
|
|
40
|
-
quantity:
|
|
42
|
+
quantity: qty !== 0 ? qty : 1,
|
|
41
43
|
unit_price: priceFromProduct(product, variant, source),
|
|
42
44
|
catalog_price: catalog,
|
|
43
45
|
cost,
|
|
44
46
|
tax_rate: product.tax_rate ?? line.tax_rate ?? 0,
|
|
47
|
+
unit: product.unit ?? line.unit,
|
|
48
|
+
extensions: product.extensions ? { ...(line.extensions ?? {}), ...product.extensions } : line.extensions,
|
|
45
49
|
available: availableStock(src, opts.warehouseId) ?? availableStock(product, opts.warehouseId),
|
|
46
50
|
}
|
|
47
51
|
}
|
package/src/business/index.ts
CHANGED
|
@@ -19,3 +19,8 @@ export * from './inspection-checklist'
|
|
|
19
19
|
export * from './vehicle-picker'
|
|
20
20
|
export * from './relate-documents'
|
|
21
21
|
export * from './print-send-dialog'
|
|
22
|
+
export * from './catalog-product-search'
|
|
23
|
+
export * from './document-editor-model'
|
|
24
|
+
export * from './document-editor-panels'
|
|
25
|
+
export * from './payment-allocator'
|
|
26
|
+
export * from './document-editor'
|
|
@@ -32,11 +32,24 @@ export interface LineItem {
|
|
|
32
32
|
cost?: number
|
|
33
33
|
/** Tope de cantidad (NC: facturado − acreditado; devolución: vendido − devuelto). */
|
|
34
34
|
max_quantity?: number
|
|
35
|
+
/**
|
|
36
|
+
* Id del renglón del documento origen («crear desde»). Viaja en la columna
|
|
37
|
+
* `line_link_field` de la fuente para que el servidor descuente y valide lo
|
|
38
|
+
* pendiente; un renglón libre no lo lleva.
|
|
39
|
+
*/
|
|
40
|
+
source_line_id?: string
|
|
35
41
|
technician_id?: string
|
|
36
42
|
lot?: string
|
|
37
43
|
dot?: string
|
|
38
44
|
/** Existencia disponible conocida (para la política de sobreventa). */
|
|
39
45
|
available?: number
|
|
46
|
+
/**
|
|
47
|
+
* Campos de extensión del renglón que aportan OTROS addons (p. ej.
|
|
48
|
+
* fiscal_mexico: `mx_clave_prod_serv`, `mx_clave_unidad`), copiados del
|
|
49
|
+
* producto al elegirlo. Viajan en `fiscal_data` (la bolsa de ModelExtension
|
|
50
|
+
* del host) — el SDK no conoce sus claves.
|
|
51
|
+
*/
|
|
52
|
+
extensions?: Record<string, string>
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
/** Qué hacer si `quantity > available`. */
|
|
@@ -196,7 +209,10 @@ export function validateLineItems(lines: LineItem[], policy: LineItemsPolicy = {
|
|
|
196
209
|
* Forma que se manda al backend: números reales (nunca `""`), sin claves de UI,
|
|
197
210
|
* opcionales vacíos omitidos y `position` 1-based conservando el orden.
|
|
198
211
|
*/
|
|
199
|
-
export function serializeLineItems(
|
|
212
|
+
export function serializeLineItems(
|
|
213
|
+
lines: LineItem[],
|
|
214
|
+
opts: { /** Columna que recibe `source_line_id` (la `line_link_field` de la fuente). */ sourceLineField?: string } = {},
|
|
215
|
+
): Array<Record<string, unknown>> {
|
|
200
216
|
return lines.map((l, i) => {
|
|
201
217
|
const out: Record<string, unknown> = {
|
|
202
218
|
position: i + 1,
|
|
@@ -214,6 +230,13 @@ export function serializeLineItems(lines: LineItem[]): Array<Record<string, unkn
|
|
|
214
230
|
const v = l[k]
|
|
215
231
|
if (typeof v === 'string' && v.trim()) out[k] = v.trim()
|
|
216
232
|
}
|
|
233
|
+
if (l.extensions && Object.keys(l.extensions).length > 0) out.fiscal_data = { ...l.extensions }
|
|
234
|
+
if (opts.sourceLineField && l.source_line_id) out[opts.sourceLineField] = l.source_line_id
|
|
235
|
+
// Importes ya calculados: un modelo de renglón sin tasa (solo
|
|
236
|
+
// `tax_amount`/`subtotal`) los guarda tal cual en vez de perder el IVA.
|
|
237
|
+
const a = computeLine(l)
|
|
238
|
+
out.subtotal = a.net
|
|
239
|
+
out.tax_amount = a.tax
|
|
217
240
|
}
|
|
218
241
|
return out
|
|
219
242
|
})
|
|
@@ -231,7 +254,7 @@ export function parseLineItems(rows: unknown): LineItem[] {
|
|
|
231
254
|
product_id: str(o.product_id),
|
|
232
255
|
sku: str(o.sku),
|
|
233
256
|
description: typeof o.description === 'string' ? o.description : '',
|
|
234
|
-
quantity: o.quantity == null ? 1 : toAmount(o.quantity),
|
|
257
|
+
quantity: o.quantity == null || o.quantity === '' ? 1 : toAmount(o.quantity),
|
|
235
258
|
unit_price: toAmount(o.unit_price),
|
|
236
259
|
discount: toAmount(o.discount),
|
|
237
260
|
discount_kind: o.discount_kind === 'amount' ? 'amount' : undefined,
|
|
@@ -243,6 +266,12 @@ export function parseLineItems(rows: unknown): LineItem[] {
|
|
|
243
266
|
technician_id: str(o.technician_id),
|
|
244
267
|
lot: str(o.lot),
|
|
245
268
|
dot: str(o.dot),
|
|
269
|
+
extensions:
|
|
270
|
+
o.fiscal_data && typeof o.fiscal_data === 'object'
|
|
271
|
+
? (Object.fromEntries(
|
|
272
|
+
Object.entries(o.fiscal_data as Record<string, unknown>).filter(([, v]) => typeof v === 'string'),
|
|
273
|
+
) as Record<string, string>)
|
|
274
|
+
: undefined,
|
|
246
275
|
})
|
|
247
276
|
})
|
|
248
277
|
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// PaymentAllocator — UI del reparto de un pago entre los documentos abiertos de
|
|
2
|
+
// la contraparte (cobro multi-factura / REP, CxP «Aplicar pago»). Las reglas
|
|
3
|
+
// son las de primitives/allocation (allocatePayment / validateAllocation); aquí
|
|
4
|
+
// solo se pintan y se editan. Por defecto reparte solo, del más vencido al más
|
|
5
|
+
// reciente; al teclear un monto pasa a manual sin perder lo ya repartido.
|
|
6
|
+
import { useTranslation } from 'react-i18next'
|
|
7
|
+
import { Badge, Button } from '@asteby/metacore-ui/primitives'
|
|
8
|
+
import { Input } from '@asteby/metacore-ui'
|
|
9
|
+
import type { AllocationResult, AllocationStrategy, OpenDocument } from '../primitives/allocation'
|
|
10
|
+
import { manualFromAllocations, overdueDays } from './document-editor-model'
|
|
11
|
+
import { useFormatter } from './format'
|
|
12
|
+
|
|
13
|
+
export interface PaymentAllocatorValue {
|
|
14
|
+
strategy: AllocationStrategy
|
|
15
|
+
manual: Record<string, number>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface PaymentAllocatorProps {
|
|
19
|
+
documents: OpenDocument[]
|
|
20
|
+
/** Resultado de `allocatePayment(amount, documents, value.strategy, value.manual)`. */
|
|
21
|
+
result: AllocationResult
|
|
22
|
+
value: PaymentAllocatorValue
|
|
23
|
+
onChange: (next: PaymentAllocatorValue) => void
|
|
24
|
+
currency?: string
|
|
25
|
+
loading?: boolean
|
|
26
|
+
readOnly?: boolean
|
|
27
|
+
/** Hoy (para los días de atraso; inyectable en tests). */
|
|
28
|
+
today?: Date
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const num = (s: string): number => (s.trim() === '' ? 0 : Number(s.replace(',', '.')) || 0)
|
|
32
|
+
|
|
33
|
+
export function PaymentAllocator({ documents, result, value, onChange, currency, loading, readOnly, today }: PaymentAllocatorProps) {
|
|
34
|
+
const { t } = useTranslation()
|
|
35
|
+
const fmt = useFormatter({ currency })
|
|
36
|
+
const byDoc = new Map(result.allocations.map((a) => [a.document_id, a]))
|
|
37
|
+
|
|
38
|
+
if (loading) {
|
|
39
|
+
return <p className="text-sm text-muted-foreground">{t('paymentAllocator.loading', { defaultValue: 'Buscando documentos con saldo…' })}</p>
|
|
40
|
+
}
|
|
41
|
+
if (documents.length === 0) {
|
|
42
|
+
return (
|
|
43
|
+
<p className="text-sm text-muted-foreground" data-slot="payment-allocator-empty">
|
|
44
|
+
{t('paymentAllocator.empty', { defaultValue: 'No hay documentos con saldo pendiente.' })}
|
|
45
|
+
</p>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const setAmount = (id: string, amount: number) =>
|
|
50
|
+
onChange({ strategy: 'manual', manual: { ...manualFromAllocations(result.allocations), [id]: amount } })
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="space-y-2" data-slot="payment-allocator">
|
|
54
|
+
<div className="flex items-center justify-between gap-3 text-xs text-muted-foreground">
|
|
55
|
+
<span>
|
|
56
|
+
{value.strategy === 'manual'
|
|
57
|
+
? t('paymentAllocator.manual', { defaultValue: 'Reparto manual' })
|
|
58
|
+
: t('paymentAllocator.auto', { defaultValue: 'Se aplica primero a lo más vencido' })}
|
|
59
|
+
</span>
|
|
60
|
+
{!readOnly && value.strategy === 'manual' && (
|
|
61
|
+
<Button type="button" variant="ghost" size="sm" onClick={() => onChange({ strategy: 'oldest_due_first', manual: {} })}>
|
|
62
|
+
{t('paymentAllocator.autoapply', { defaultValue: 'Autoaplicar' })}
|
|
63
|
+
</Button>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<ul className="divide-y rounded-md border">
|
|
67
|
+
{documents.map((d) => {
|
|
68
|
+
const a = byDoc.get(d.id)
|
|
69
|
+
const late = overdueDays(d, today)
|
|
70
|
+
return (
|
|
71
|
+
<li key={d.id} className="flex flex-wrap items-center gap-3 px-3 py-2 text-sm" data-document={d.id}>
|
|
72
|
+
<div className="min-w-0 flex-1">
|
|
73
|
+
<div className="flex items-center gap-2">
|
|
74
|
+
<span className="font-medium">{d.number}</span>
|
|
75
|
+
{late > 0 && (
|
|
76
|
+
<Badge variant="secondary">{t('paymentAllocator.overdue', { defaultValue: 'Vencida · {{days}} d', days: late })}</Badge>
|
|
77
|
+
)}
|
|
78
|
+
{d.payment_method && <span className="text-xs text-muted-foreground">{d.payment_method}</span>}
|
|
79
|
+
</div>
|
|
80
|
+
<div className="text-xs text-muted-foreground">
|
|
81
|
+
{t('paymentAllocator.balance', { defaultValue: 'Saldo' })} {fmt.money(d.balance, d.currency)}
|
|
82
|
+
{a && a.balance_after > 0 && (
|
|
83
|
+
<>
|
|
84
|
+
{' · '}
|
|
85
|
+
{t('paymentAllocator.installment', { defaultValue: 'Parcialidad {{n}}', n: a.installment })}
|
|
86
|
+
{' · '}
|
|
87
|
+
{t('paymentAllocator.remaining', { defaultValue: 'Queda' })} {fmt.money(a.balance_after, d.currency)}
|
|
88
|
+
</>
|
|
89
|
+
)}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<Input
|
|
93
|
+
type="number"
|
|
94
|
+
inputMode="decimal"
|
|
95
|
+
min={0}
|
|
96
|
+
step="0.01"
|
|
97
|
+
aria-label={t('paymentAllocator.apply_to', { defaultValue: 'Aplicar a {{number}}', number: d.number })}
|
|
98
|
+
className="h-9 w-32 text-right tabular-nums"
|
|
99
|
+
value={a ? String(a.amount) : ''}
|
|
100
|
+
placeholder="0.00"
|
|
101
|
+
disabled={readOnly}
|
|
102
|
+
onChange={(e) => setAmount(d.id, num(e.target.value))}
|
|
103
|
+
/>
|
|
104
|
+
</li>
|
|
105
|
+
)
|
|
106
|
+
})}
|
|
107
|
+
</ul>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
@@ -80,6 +80,10 @@ export interface ProductResult {
|
|
|
80
80
|
stock?: StockByWarehouse[]
|
|
81
81
|
/** Paquetes automáticos: ids/nombres que se sugieren junto al producto. */
|
|
82
82
|
bundle_items?: Array<{ product_id: string; name: string; quantity: number }>
|
|
83
|
+
/** Unidad de venta del catálogo (pza, juego, kg…). */
|
|
84
|
+
unit?: string
|
|
85
|
+
/** Campos de extensión del producto (fiscal_data.*) que se copian al renglón. */
|
|
86
|
+
extensions?: Record<string, string>
|
|
83
87
|
}
|
|
84
88
|
|
|
85
89
|
/** Existencia disponible: de la variante o del producto, en un almacén o en total. */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// the image-url resolver and the org IANA timezone — are passed as props so the
|
|
11
11
|
// SDK stays transport- and host-agnostic.
|
|
12
12
|
import { emitRecordMutation } from '../record-mutation-events'
|
|
13
|
-
import { createContext, useCallback, useContext, useEffect, useId, useRef, useState } from 'react'
|
|
13
|
+
import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from 'react'
|
|
14
14
|
import { useTranslation } from 'react-i18next'
|
|
15
15
|
import type { ModelSchema } from './types'
|
|
16
16
|
|
|
@@ -69,13 +69,14 @@ import type { VisibleWhen } from '../types'
|
|
|
69
69
|
import { groupFieldsBySection, type FormLayout } from '../form-layout'
|
|
70
70
|
import { FieldSection, WizardProgress } from '../form-layout-ui'
|
|
71
71
|
import { AssistInterview } from '../assist-interview'
|
|
72
|
+
import { RecordPrefillBar } from '../record-prefill-bar'
|
|
72
73
|
import { FieldCell } from '../field-grid'
|
|
73
74
|
import { isNilUuid, normalizeNilUuid } from '../nil-uuid'
|
|
74
75
|
import { normalizeRefFieldsForSubmit } from './normalize-submit'
|
|
75
76
|
import { validateValues, bagHasErrors, exemptUnchangedRuleIssues } from '../validator'
|
|
76
77
|
import { DynamicIcon, isLucideIconName } from '../dynamic-icon'
|
|
77
78
|
import { IconPickerField } from '../icon-picker-field'
|
|
78
|
-
import { humanizeToken } from '../dynamic-columns-helpers'
|
|
79
|
+
import { humanizeToken, localizeFieldLabel } from '../dynamic-columns-helpers'
|
|
79
80
|
import { formatDateCell } from '../dynamic-columns'
|
|
80
81
|
import {
|
|
81
82
|
ImageStack,
|
|
@@ -667,6 +668,17 @@ export function DynamicRecordDialog({
|
|
|
667
668
|
const isCreate = mode === 'create'
|
|
668
669
|
const isView = mode === 'view'
|
|
669
670
|
const isEditable = mode === 'create' || mode === 'edit'
|
|
671
|
+
// Claves que un ayudante de captura (RecordPrefillBar) puede escribir: sólo
|
|
672
|
+
// los campos que este formulario declara (incluye fiscal_data.* de las
|
|
673
|
+
// ModelExtension). Readonly fuera: un prefill no salta un candado.
|
|
674
|
+
const prefillAllowedKeys = useMemo(
|
|
675
|
+
() => new Set<string>((modalMeta?.fields ?? []).filter((f: any) => !f.readonly).map((f: any) => f.key)),
|
|
676
|
+
[modalMeta],
|
|
677
|
+
)
|
|
678
|
+
const applyPrefill = useCallback(
|
|
679
|
+
(patch: Record<string, any>) => setFormValues(prev => ({ ...prev, ...patch })),
|
|
680
|
+
[],
|
|
681
|
+
)
|
|
670
682
|
const config = MODE_CONFIG[mode]
|
|
671
683
|
|
|
672
684
|
// ── Fetch metadata + record when dialog opens ──────────────────────────
|
|
@@ -851,7 +863,7 @@ export function DynamicRecordDialog({
|
|
|
851
863
|
// with no matching form field).
|
|
852
864
|
const labelForKey = (key: string): string => {
|
|
853
865
|
const f = (modalMeta?.fields ?? []).find(x => x.key === key)
|
|
854
|
-
if (f?.label) return f.label
|
|
866
|
+
if (f?.label) return localizeFieldLabel(f.label, t)
|
|
855
867
|
return key.replace(/[._-]+/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
|
856
868
|
}
|
|
857
869
|
|
|
@@ -903,7 +915,7 @@ export function DynamicRecordDialog({
|
|
|
903
915
|
}
|
|
904
916
|
if (bagHasErrors(bag)) {
|
|
905
917
|
const labels: Record<string, string> = {}
|
|
906
|
-
for (const f of visible) labels[f.key] = f.label
|
|
918
|
+
for (const f of visible) labels[f.key] = localizeFieldLabel(f.label, t)
|
|
907
919
|
const next = localizeFieldErrorMap(bag, t, { labels })
|
|
908
920
|
setFieldErrors(next)
|
|
909
921
|
const description = Object.entries(next)
|
|
@@ -1077,7 +1089,7 @@ export function DynamicRecordDialog({
|
|
|
1077
1089
|
const bag = validateValues(stepFields as ActionFieldDef[], formValues)
|
|
1078
1090
|
if (bagHasErrors(bag)) {
|
|
1079
1091
|
const labels: Record<string, string> = {}
|
|
1080
|
-
for (const f of stepFields) labels[f.key] = f.label
|
|
1092
|
+
for (const f of stepFields) labels[f.key] = localizeFieldLabel(f.label, t)
|
|
1081
1093
|
setFieldErrors(localizeFieldErrorMap(bag, t, { labels }))
|
|
1082
1094
|
toast.error(t('dynamic.validation_failed', { defaultValue: 'Revisa los campos marcados' }))
|
|
1083
1095
|
return
|
|
@@ -1111,6 +1123,16 @@ export function DynamicRecordDialog({
|
|
|
1111
1123
|
two-column grid, unchanged. FieldCell gives each
|
|
1112
1124
|
cell `min-w-0` so a long select/input value can't
|
|
1113
1125
|
blow the two columns past the dialog width. */}
|
|
1126
|
+
{isEditable && (
|
|
1127
|
+
<RecordPrefillBar
|
|
1128
|
+
model={model}
|
|
1129
|
+
mode={isCreate ? 'create' : 'edit'}
|
|
1130
|
+
recordId={recordId}
|
|
1131
|
+
values={formValues}
|
|
1132
|
+
allowedKeys={prefillAllowedKeys}
|
|
1133
|
+
onApply={applyPrefill}
|
|
1134
|
+
/>
|
|
1135
|
+
)}
|
|
1114
1136
|
<form
|
|
1115
1137
|
id={formId}
|
|
1116
1138
|
// Stable Aby / guidance contract: unique id={formId}
|
|
@@ -1313,11 +1335,12 @@ function FieldRow({ field, record, value, mode, onChange, error, locked }: Field
|
|
|
1313
1335
|
// the rich read-only renderer. `locked` forces the same disabled rendering on
|
|
1314
1336
|
// CREATE for a caller-specified field (see `lockedFields`).
|
|
1315
1337
|
const isEditReadonly = (mode === 'edit' && !!field.readonly) || !!locked
|
|
1338
|
+
const { t } = useTranslation()
|
|
1316
1339
|
|
|
1317
1340
|
return (
|
|
1318
1341
|
<div className="flex flex-col gap-1.5" data-aby-field={field.key}>
|
|
1319
1342
|
<Label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
1320
|
-
{field.label}
|
|
1343
|
+
{localizeFieldLabel(field.label, t)}
|
|
1321
1344
|
{field.required && mode !== 'view' && !isEditReadonly && (
|
|
1322
1345
|
<span className="text-destructive ml-0.5">*</span>
|
|
1323
1346
|
)}
|
|
@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'
|
|
|
8
8
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@asteby/metacore-ui/primitives'
|
|
9
9
|
import { cn } from '@asteby/metacore-ui/lib'
|
|
10
10
|
import { ActionBar } from './action-bar'
|
|
11
|
+
import { DocumentSecondaryBar } from './secondary-bar'
|
|
11
12
|
import { DocumentHeader, type HeaderBadge, type HeaderMetric } from './document-header'
|
|
12
13
|
import { SmartButtons, type SmartButtonItem } from './smart-buttons'
|
|
13
14
|
import { StatusBadge } from './status-badge'
|
|
@@ -175,6 +176,20 @@ export function DocumentPage(props: DocumentPageProps) {
|
|
|
175
176
|
</TabsContent>
|
|
176
177
|
))}
|
|
177
178
|
</Tabs>
|
|
179
|
+
|
|
180
|
+
{/* Compartir / imprimir / correo / PDF-XML y lo que aporten los
|
|
181
|
+
addons instalados: al pie, no compitiendo con la primaria. */}
|
|
182
|
+
<DocumentSecondaryBar
|
|
183
|
+
items={layout.footer.map((a) => ({
|
|
184
|
+
key: a.def.key,
|
|
185
|
+
label: a.def.label,
|
|
186
|
+
icon: a.def.icon,
|
|
187
|
+
blockedReason: a.blockedReason,
|
|
188
|
+
run: () => handleAction(a),
|
|
189
|
+
}))}
|
|
190
|
+
model={spec.model}
|
|
191
|
+
record={record}
|
|
192
|
+
/>
|
|
178
193
|
</div>
|
|
179
194
|
)
|
|
180
195
|
}
|
package/src/document/evaluate.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { classifyActionPriority } from '../action-priority'
|
|
1
2
|
import type {
|
|
2
3
|
ActionLayout,
|
|
3
4
|
DocumentActionDef,
|
|
@@ -130,12 +131,15 @@ export function resolveActions(spec: DocumentSpec, ctx: DocumentContext): Resolv
|
|
|
130
131
|
const destructive = many(layout.destructive)
|
|
131
132
|
const secondary: ResolvedAction[] = []
|
|
132
133
|
const more: ResolvedAction[] = []
|
|
134
|
+
const footer: ResolvedAction[] = []
|
|
133
135
|
for (const a of [...many(layout.secondary)]) {
|
|
134
136
|
if (a.def.destructive) destructive.push(a)
|
|
137
|
+
else if (isFooterAction(a.def)) footer.push(a)
|
|
135
138
|
else secondary.push(a)
|
|
136
139
|
}
|
|
137
140
|
for (const a of many(layout.more)) {
|
|
138
141
|
if (a.def.destructive) destructive.push(a)
|
|
142
|
+
else if (isFooterAction(a.def)) footer.push(a)
|
|
139
143
|
else more.push(a)
|
|
140
144
|
}
|
|
141
145
|
const overflow = secondary.splice(MAX_VISIBLE_SECONDARY)
|
|
@@ -146,7 +150,19 @@ export function resolveActions(spec: DocumentSpec, ctx: DocumentContext): Resolv
|
|
|
146
150
|
destructive.unshift(primary)
|
|
147
151
|
primary = undefined
|
|
148
152
|
}
|
|
149
|
-
return { primary, secondary, more, destructive }
|
|
153
|
+
return { primary, secondary, more, destructive, footer }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* ¿Va al pie? `priority: secondary`, o la convención por clave sobre la acción
|
|
158
|
+
* del modelo (`modelAction ?? key`): compartir, imprimir, correo, chat, PDF/XML.
|
|
159
|
+
*/
|
|
160
|
+
function isFooterAction(def: DocumentActionDef): boolean {
|
|
161
|
+
if (def.priority) return def.priority === 'secondary'
|
|
162
|
+
return (
|
|
163
|
+
classifyActionPriority({ key: def.key }) === 'secondary' ||
|
|
164
|
+
(!!def.modelAction && classifyActionPriority({ key: def.modelAction }) === 'secondary')
|
|
165
|
+
)
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
/** Interpola `{{campo}}` con valores del registro (sin evaluar código). */
|
package/src/document/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ export {
|
|
|
13
13
|
export { formatFieldValue, type FieldFormatOptions } from './format'
|
|
14
14
|
export { StatusBadge, STATUS_TONE_STYLE, type StatusBadgeProps } from './status-badge'
|
|
15
15
|
export { ActionBar, type ActionBarProps } from './action-bar'
|
|
16
|
+
export { DocumentSecondaryBar, type DocumentSecondaryBarProps, type SecondaryBarItem } from './secondary-bar'
|
|
16
17
|
export { DocumentHeader, type DocumentHeaderProps, type HeaderBadge, type HeaderMetric } from './document-header'
|
|
17
18
|
export { SmartButtons, type SmartButtonItem, type SmartButtonsProps } from './smart-buttons'
|
|
18
19
|
export { DocumentTimeline, type DocumentTimelineProps, type TimelineEntry } from './document-timeline'
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// DocumentSecondaryBar — pie del documento con las acciones secundarias:
|
|
2
|
+
// compartir, imprimir/descargar PDF o XML, enviar por correo, enviar al chat…
|
|
3
|
+
// La cabecera se queda con UNA primaria (y sus principales); esto va abajo,
|
|
4
|
+
// discreto. Junta dos fuentes:
|
|
5
|
+
// - `items`: acciones del propio documento/modelo ya clasificadas como
|
|
6
|
+
// secundarias (resolveActions → `footer`, o splitActionsByPriority);
|
|
7
|
+
// - las aportadas por addons/host con registerRecordAction, sólo si su
|
|
8
|
+
// proveedor está instalado y activo (useRecordActions).
|
|
9
|
+
// Sin nada que mostrar no pinta nada.
|
|
10
|
+
import { useState, type ReactNode } from 'react'
|
|
11
|
+
import { useTranslation } from 'react-i18next'
|
|
12
|
+
import { Loader2 } from 'lucide-react'
|
|
13
|
+
import { Button } from '@asteby/metacore-ui/primitives'
|
|
14
|
+
import { cn } from '@asteby/metacore-ui/lib'
|
|
15
|
+
import { DynamicIcon } from '../dynamic-icon'
|
|
16
|
+
import { useRecordActions } from '../record-actions'
|
|
17
|
+
|
|
18
|
+
export interface SecondaryBarItem {
|
|
19
|
+
key: string
|
|
20
|
+
label: string
|
|
21
|
+
icon?: string
|
|
22
|
+
/** Deshabilitada con su motivo (no desaparece). */
|
|
23
|
+
blockedReason?: string
|
|
24
|
+
run: () => unknown
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DocumentSecondaryBarProps {
|
|
28
|
+
items?: SecondaryBarItem[]
|
|
29
|
+
/** Modelo + registro: resuelven las acciones aportadas por addons. */
|
|
30
|
+
model?: string
|
|
31
|
+
record?: Record<string, unknown> | null
|
|
32
|
+
/** Controles extra del host (al final). */
|
|
33
|
+
children?: ReactNode
|
|
34
|
+
className?: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function DocumentSecondaryBar({ items = [], model, record, children, className }: DocumentSecondaryBarProps) {
|
|
38
|
+
const { t } = useTranslation()
|
|
39
|
+
const contributed = useRecordActions(model ?? '', model ? record : null)
|
|
40
|
+
const [busy, setBusy] = useState<string | null>(null)
|
|
41
|
+
|
|
42
|
+
const run = async (key: string, fn?: () => unknown) => {
|
|
43
|
+
if (!fn || busy) return
|
|
44
|
+
setBusy(key)
|
|
45
|
+
try {
|
|
46
|
+
await fn()
|
|
47
|
+
} finally {
|
|
48
|
+
setBusy(null)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const all: SecondaryBarItem[] = [
|
|
53
|
+
...items,
|
|
54
|
+
...contributed.filter((c) => c.run).map((c) => ({ key: c.key, label: c.label, icon: c.icon, run: c.run! })),
|
|
55
|
+
]
|
|
56
|
+
const rendered = contributed.filter((c) => c.render && !c.run)
|
|
57
|
+
if (all.length === 0 && rendered.length === 0 && !children) return null
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<footer
|
|
61
|
+
className={cn('flex flex-wrap items-center gap-1.5 border-t pt-3', className)}
|
|
62
|
+
data-document-secondary=''
|
|
63
|
+
aria-label={t('datatable.secondary_actions', { defaultValue: 'Compartir y documentos' })}
|
|
64
|
+
>
|
|
65
|
+
{all.map((a) => (
|
|
66
|
+
<Button
|
|
67
|
+
key={a.key}
|
|
68
|
+
type='button'
|
|
69
|
+
size='sm'
|
|
70
|
+
variant='ghost'
|
|
71
|
+
className='text-muted-foreground'
|
|
72
|
+
data-action={a.key}
|
|
73
|
+
disabled={!!a.blockedReason || (!!busy && busy !== a.key)}
|
|
74
|
+
title={a.blockedReason ? t(a.blockedReason, { defaultValue: a.blockedReason }) : undefined}
|
|
75
|
+
onClick={() => void run(a.key, a.run)}
|
|
76
|
+
>
|
|
77
|
+
{busy === a.key ? (
|
|
78
|
+
<Loader2 className='mr-1.5 h-4 w-4 animate-spin' />
|
|
79
|
+
) : a.icon ? (
|
|
80
|
+
<DynamicIcon name={a.icon} className='mr-1.5 h-4 w-4' />
|
|
81
|
+
) : null}
|
|
82
|
+
{t(a.label, { defaultValue: a.label })}
|
|
83
|
+
</Button>
|
|
84
|
+
))}
|
|
85
|
+
{rendered.map((c) => {
|
|
86
|
+
const C = c.render!
|
|
87
|
+
return <C key={c.key} model={model!} record={record!} />
|
|
88
|
+
})}
|
|
89
|
+
{children}
|
|
90
|
+
</footer>
|
|
91
|
+
)
|
|
92
|
+
}
|
package/src/document/types.ts
CHANGED
|
@@ -64,6 +64,12 @@ export interface DocumentActionDef {
|
|
|
64
64
|
href?: string
|
|
65
65
|
openUrl?: string
|
|
66
66
|
tab?: string
|
|
67
|
+
/**
|
|
68
|
+
* "secondary" (o, sin valor, una clave de compartir/imprimir/correo/chat/
|
|
69
|
+
* PDF/XML por convención) saca la acción de la cabecera y la pone en la
|
|
70
|
+
* barra secundaria del pie — salvo que el layout la haga primaria.
|
|
71
|
+
*/
|
|
72
|
+
priority?: 'primary' | 'secondary'
|
|
67
73
|
/** Estilo crítico: siempre va en la zona destructiva. */
|
|
68
74
|
destructive?: boolean
|
|
69
75
|
/** Se muestra deshabilitada con el motivo en lugar de desaparecer. */
|
|
@@ -200,6 +206,8 @@ export interface ResolvedActionLayout {
|
|
|
200
206
|
secondary: ResolvedAction[]
|
|
201
207
|
more: ResolvedAction[]
|
|
202
208
|
destructive: ResolvedAction[]
|
|
209
|
+
/** Secundarias (compartir, imprimir, correo…) → DocumentSecondaryBar, no la cabecera. */
|
|
210
|
+
footer: ResolvedAction[]
|
|
203
211
|
}
|
|
204
212
|
|
|
205
213
|
export type DocumentSlotRenderer = (ctx: DocumentContext) => ReactNode
|