@asteby/metacore-runtime-react 49.1.0 → 49.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/CHANGELOG.md +26 -0
- package/dist/action-modal-dispatcher.d.ts +1 -0
- package/dist/action-modal-dispatcher.d.ts.map +1 -1
- package/dist/action-modal-dispatcher.js +34 -5
- package/dist/business/document-editor-model.d.ts +6 -1
- package/dist/business/document-editor-model.d.ts.map +1 -1
- package/dist/business/document-editor-model.js +48 -7
- package/dist/business/document-editor-panels.d.ts +18 -0
- package/dist/business/document-editor-panels.d.ts.map +1 -1
- package/dist/business/document-editor-panels.js +9 -0
- package/dist/business/document-editor.d.ts +7 -1
- package/dist/business/document-editor.d.ts.map +1 -1
- package/dist/business/document-editor.js +69 -24
- package/dist/document-form-dialog.d.ts.map +1 -1
- package/dist/document-form-dialog.js +8 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/record-mutation-events.d.ts +12 -0
- package/dist/record-mutation-events.d.ts.map +1 -1
- package/dist/record-mutation-events.js +18 -0
- package/package.json +1 -1
- package/src/__tests__/document-editor-model.test.ts +27 -0
- package/src/__tests__/document-editor.test.tsx +60 -0
- package/src/__tests__/invoice-document-editor.test.tsx +267 -0
- package/src/__tests__/r5-self-label-settled.test.ts +57 -0
- package/src/action-modal-dispatcher.tsx +34 -5
- package/src/business/document-editor-model.ts +50 -7
- package/src/business/document-editor-panels.tsx +49 -0
- package/src/business/document-editor.tsx +246 -164
- package/src/document-form-dialog.tsx +13 -4
- package/src/index.ts +2 -0
- package/src/record-mutation-events.ts +23 -0
|
@@ -54,7 +54,9 @@ function declaredDefault(f: ActionFieldDef): unknown {
|
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
56
|
* Divulgación progresiva: ¿el campo va plegado en «Opciones fiscales»?
|
|
57
|
-
* - campos de extensión (`fiscal_data.*`) que aportan otros addons
|
|
57
|
+
* - campos de extensión (`fiscal_data.*`) que aportan otros addons (los que
|
|
58
|
+
* el propio tipo declara obligatorios, p. ej. método y forma de pago de la
|
|
59
|
+
* factura, se marcan esenciales en splitEditorFields);
|
|
58
60
|
* - `section: "fiscal" | "advanced"`;
|
|
59
61
|
* - catálogos opcionales que ya traen un default (método, serie, relación…):
|
|
60
62
|
* el usuario no necesita tocarlos para guardar.
|
|
@@ -84,7 +86,10 @@ export function splitEditorFields(
|
|
|
84
86
|
fields: readonly ActionFieldDef[],
|
|
85
87
|
opts: { partyField?: string; essentialKeys?: readonly string[]; extensionFields?: readonly ActionFieldDef[] } = {},
|
|
86
88
|
): EditorFieldGroups {
|
|
87
|
-
|
|
89
|
+
// Lo obligatorio que el tipo declara siempre está a la vista, aunque sea de
|
|
90
|
+
// extensión (`fiscal_data.metodo_pago`): es lo que el usuario debe decidir.
|
|
91
|
+
// Las extensiones que llegan del metadata (no declaradas) siguen plegadas.
|
|
92
|
+
const essentialKeys = new Set([...(opts.essentialKeys ?? []), ...fields.filter((f) => f.required).map((f) => f.key)])
|
|
88
93
|
const out: EditorFieldGroups = { essential: [], advanced: [], notes: [] }
|
|
89
94
|
const seen = new Set<string>()
|
|
90
95
|
for (const f of [...fields, ...(opts.extensionFields ?? [])]) {
|
|
@@ -200,7 +205,7 @@ export function creditStatus(
|
|
|
200
205
|
export function linesFromSource(
|
|
201
206
|
rows: unknown,
|
|
202
207
|
map: Record<string, string> = {},
|
|
203
|
-
opts: { kind?: DocumentFormLines['kind']; discountMode?: DocumentFormLines['discount_mode'] } = {},
|
|
208
|
+
opts: { kind?: DocumentFormLines['kind']; discountMode?: DocumentFormLines['discount_mode']; defaultTaxRate?: number } = {},
|
|
204
209
|
): LineItem[] {
|
|
205
210
|
if (!Array.isArray(rows)) return []
|
|
206
211
|
return rows.filter((raw) => !isFullyConsumed(raw)).map((raw) => {
|
|
@@ -212,13 +217,18 @@ export function linesFromSource(
|
|
|
212
217
|
if (!hasValue(rate)) {
|
|
213
218
|
const sub = toAmount(get('subtotal'))
|
|
214
219
|
const tax = toAmount(get('tax_amount'))
|
|
215
|
-
|
|
220
|
+
// Un origen sin dato de impuesto (p. ej. una OT de taller: solo
|
|
221
|
+
// cantidad y precio) toma la tasa de la org, como un renglón nuevo.
|
|
222
|
+
rate = hasValue(get('tax_amount'))
|
|
223
|
+
? sub > 0 ? Math.round((tax / sub) * 10000) / 10000 : 0
|
|
224
|
+
: opts.defaultTaxRate ?? 0
|
|
216
225
|
}
|
|
217
226
|
const r2 = toAmount(rate)
|
|
227
|
+
const product = get('product_id')
|
|
218
228
|
const line = makeLine({
|
|
219
|
-
product_id:
|
|
229
|
+
product_id: refValue(product),
|
|
220
230
|
sku: get('sku') ?? undefined,
|
|
221
|
-
description: String(get('description') ?? get('product_name') ?? ''),
|
|
231
|
+
description: String(get('description') ?? get('product_name') ?? refLabel(product) ?? refLabel(r.product) ?? ''),
|
|
222
232
|
quantity: qty,
|
|
223
233
|
unit_price: toAmount(get('unit_price')),
|
|
224
234
|
discount: toAmount(get('discount') ?? 0),
|
|
@@ -236,11 +246,40 @@ export function linesFromSource(
|
|
|
236
246
|
})
|
|
237
247
|
}
|
|
238
248
|
|
|
249
|
+
/** Id de una celda de relación (`{value,label}`, `{id,name}` o el id plano). */
|
|
250
|
+
function refValue(v: unknown): string | undefined {
|
|
251
|
+
if (v == null || v === '') return undefined
|
|
252
|
+
if (typeof v === 'object') {
|
|
253
|
+
const o = v as Record<string, unknown>
|
|
254
|
+
const id = o.value ?? o.id
|
|
255
|
+
return id == null || id === '' ? undefined : String(id)
|
|
256
|
+
}
|
|
257
|
+
return String(v)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Nombre de una celda de relación ya resuelta por el host (`{label}`/`{name}`).
|
|
262
|
+
* Un renglón de origen sin descripción propia (InvoiceItem solo lleva el
|
|
263
|
+
* producto) se nombra con su producto en vez de entrar en blanco.
|
|
264
|
+
*/
|
|
265
|
+
function refLabel(v: unknown): string | undefined {
|
|
266
|
+
if (!v || typeof v !== 'object') return undefined
|
|
267
|
+
const o = v as Record<string, unknown>
|
|
268
|
+
const l = o.label ?? o.name
|
|
269
|
+
return typeof l === 'string' && l.trim() ? l : undefined
|
|
270
|
+
}
|
|
271
|
+
|
|
239
272
|
function isFullyConsumed(raw: unknown): boolean {
|
|
240
273
|
const r = (raw ?? {}) as Record<string, any>
|
|
241
274
|
return hasValue(r.remaining_quantity) && toAmount(r.remaining_quantity) <= 0
|
|
242
275
|
}
|
|
243
276
|
|
|
277
|
+
/** El editor ocupa toda la pantalla: documentos comerciales con renglones (factura, cotización, pedido, OC). */
|
|
278
|
+
export function isFullscreenEditor(type: DocumentFormType, forms: Pick<DocumentFormsManifest, 'lines_field'>): boolean {
|
|
279
|
+
const kind = editorLinesConfig(type, forms)?.kind ?? 'sale'
|
|
280
|
+
return !!type.lines && (kind === 'sale' || kind === 'purchase')
|
|
281
|
+
}
|
|
282
|
+
|
|
244
283
|
/** Cuántos renglones del origen se cargaron y cuántos se omitieron por estar ya cubiertos. */
|
|
245
284
|
export function sourceLoadSummary(rows: unknown): { loaded: number; covered: number } {
|
|
246
285
|
if (!Array.isArray(rows)) return { loaded: 0, covered: 0 }
|
|
@@ -267,7 +306,11 @@ export function localIssues(lines: LineItem[], opts: { requireLines?: boolean; r
|
|
|
267
306
|
issues.push({ field: 'lines', severity: 'error', message: 'Agrega al menos un renglón.' })
|
|
268
307
|
}
|
|
269
308
|
items.forEach((l, i) => {
|
|
270
|
-
|
|
309
|
+
// Un renglón con producto se describe con él (el servidor lo nombra
|
|
310
|
+
// desde el catálogo): solo un renglón libre exige texto, como en el
|
|
311
|
+
// editor de renglones (validateLineItems).
|
|
312
|
+
if (!l.description.trim() && !l.product_id)
|
|
313
|
+
issues.push({ field: `lines.${i}`, severity: 'error', message: `Renglón ${i + 1}: elige un producto o escribe la descripción.` })
|
|
271
314
|
if (toAmount(l.unit_price) <= 0) issues.push({ field: `lines.${i}`, severity: 'warning', message: `Renglón ${i + 1}: precio en cero.` })
|
|
272
315
|
if (l.max_quantity != null && toAmount(l.quantity) > toAmount(l.max_quantity)) {
|
|
273
316
|
issues.push({
|
|
@@ -7,6 +7,7 @@ import { Badge } from '@asteby/metacore-ui/primitives'
|
|
|
7
7
|
import { AlertTriangle, ChevronDown, XCircle } from 'lucide-react'
|
|
8
8
|
import type { CreditStatus, EditorIssue } from './document-editor-model'
|
|
9
9
|
import type { Formatter } from './format'
|
|
10
|
+
import { computeLine, type LineItem } from './line-items'
|
|
10
11
|
|
|
11
12
|
/** Bloque con título discreto y espaciado generoso (sin bordes pesados). */
|
|
12
13
|
export function EditorSection({ title, hint, children, slot }: { title?: ReactNode; hint?: ReactNode; children: ReactNode; slot?: string }) {
|
|
@@ -162,3 +163,51 @@ export function PreviewPanel({ preview }: { preview: PreviewContent | null }) {
|
|
|
162
163
|
if (preview.xml) return <pre className="max-h-96 overflow-auto rounded bg-muted p-3 text-[11px] leading-tight" data-slot="preview-xml">{preview.xml}</pre>
|
|
163
164
|
return null
|
|
164
165
|
}
|
|
166
|
+
|
|
167
|
+
export interface DraftPreviewProps {
|
|
168
|
+
title: string
|
|
169
|
+
party?: string
|
|
170
|
+
/** Datos del encabezado ya con su etiqueta y valor legibles. */
|
|
171
|
+
header: Array<{ label: string; value: string }>
|
|
172
|
+
lines: LineItem[]
|
|
173
|
+
totals: TotalsRow[]
|
|
174
|
+
fmt: Formatter
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Vista previa local del documento (sin servidor ni borrador): cómo queda con
|
|
179
|
+
* lo capturado. Para tipos sin acción `preview`; la del servidor (PDF/XML) gana.
|
|
180
|
+
*/
|
|
181
|
+
export function DraftPreview({ title, party, header, lines, totals, fmt }: DraftPreviewProps) {
|
|
182
|
+
const items = lines.filter((l) => l.kind === 'item')
|
|
183
|
+
return (
|
|
184
|
+
<article className="space-y-3 rounded border bg-background p-4 text-xs" data-slot="preview-draft">
|
|
185
|
+
<header className="flex flex-wrap items-baseline justify-between gap-2 border-b pb-2">
|
|
186
|
+
<span className="text-sm font-semibold">{title}</span>
|
|
187
|
+
{party && <span className="font-medium">{party}</span>}
|
|
188
|
+
</header>
|
|
189
|
+
{header.length > 0 && (
|
|
190
|
+
<dl className="grid grid-cols-2 gap-x-4 gap-y-1">
|
|
191
|
+
{header.map((h) => (
|
|
192
|
+
<div key={h.label} className="min-w-0">
|
|
193
|
+
<dt className="text-muted-foreground">{h.label}</dt>
|
|
194
|
+
<dd className="truncate">{h.value}</dd>
|
|
195
|
+
</div>
|
|
196
|
+
))}
|
|
197
|
+
</dl>
|
|
198
|
+
)}
|
|
199
|
+
<table className="w-full tabular-nums">
|
|
200
|
+
<tbody>
|
|
201
|
+
{items.map((l) => (
|
|
202
|
+
<tr key={l.key} className="border-t align-top">
|
|
203
|
+
<td className="py-1 pr-2">{l.description}</td>
|
|
204
|
+
<td className="py-1 pr-2 text-right">{l.quantity}</td>
|
|
205
|
+
<td className="py-1 text-right">{fmt.money(computeLine(l).net)}</td>
|
|
206
|
+
</tr>
|
|
207
|
+
))}
|
|
208
|
+
</tbody>
|
|
209
|
+
</table>
|
|
210
|
+
<TotalsPanel rows={totals} />
|
|
211
|
+
</article>
|
|
212
|
+
)
|
|
213
|
+
}
|