@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
|
@@ -44,11 +44,13 @@ import {
|
|
|
44
44
|
partySummaryRows,
|
|
45
45
|
splitEditorFields,
|
|
46
46
|
toOpenDocuments,
|
|
47
|
+
friendlyOptionLabel,
|
|
47
48
|
withFriendlyOptions,
|
|
48
49
|
type EditorIssue,
|
|
49
50
|
} from './document-editor-model'
|
|
50
51
|
import {
|
|
51
52
|
CollapsibleSection,
|
|
53
|
+
DraftPreview,
|
|
52
54
|
EditorSection,
|
|
53
55
|
PartyCard,
|
|
54
56
|
PreviewPanel,
|
|
@@ -89,6 +91,12 @@ export interface DocumentEditorProps {
|
|
|
89
91
|
currency?: string
|
|
90
92
|
/** Hoy (días de atraso del reparto; inyectable en tests). */
|
|
91
93
|
today?: Date
|
|
94
|
+
/**
|
|
95
|
+
* Pantalla completa (DocumentFormDialog la pide para documentos con
|
|
96
|
+
* renglones): a dos columnas en pantallas anchas, con totales, revisión y
|
|
97
|
+
* vista previa a la derecha; en una sola columna en móvil.
|
|
98
|
+
*/
|
|
99
|
+
fullscreen?: boolean
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
type Busy = 'save' | 'preview' | 'source' | null
|
|
@@ -107,6 +115,7 @@ export function DocumentEditor({
|
|
|
107
115
|
defaultTaxRate,
|
|
108
116
|
currency,
|
|
109
117
|
today,
|
|
118
|
+
fullscreen = false,
|
|
110
119
|
}: DocumentEditorProps) {
|
|
111
120
|
const { t, i18n } = useTranslation()
|
|
112
121
|
const lang = i18n?.language
|
|
@@ -130,6 +139,7 @@ export function DocumentEditor({
|
|
|
130
139
|
const [linesFromSourceDoc, setLinesFromSourceDoc] = useState(false)
|
|
131
140
|
const [extFields, setExtFields] = useState<ActionFieldDef[]>([])
|
|
132
141
|
const [party, setParty] = useState<Record<string, any> | null>(null)
|
|
142
|
+
const [partyLabels, setPartyLabels] = useState<Record<string, string>>({})
|
|
133
143
|
const [openDocs, setOpenDocs] = useState<OpenDocument[]>([])
|
|
134
144
|
const [docsLoading, setDocsLoading] = useState(false)
|
|
135
145
|
const [alloc, setAlloc] = useState<PaymentAllocatorValue>({ strategy: 'oldest_due_first', manual: {} })
|
|
@@ -220,6 +230,25 @@ export function DocumentEditor({
|
|
|
220
230
|
}
|
|
221
231
|
}, [api, type.party, partyId])
|
|
222
232
|
|
|
233
|
+
// Etiquetas de la tarjeta (RFC, régimen…): las del formulario del modelo de
|
|
234
|
+
// la contraparte, incluidas sus extensiones; sin ellas se ve la columna cruda.
|
|
235
|
+
const partyModel = type.party?.model
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (!partyModel) return
|
|
238
|
+
let cancelled = false
|
|
239
|
+
api.get(`/metadata/modal/${partyModel}`)
|
|
240
|
+
.then((res: any) => {
|
|
241
|
+
const fields: any[] = res?.data?.data?.fields ?? res?.data?.fields ?? []
|
|
242
|
+
const out: Record<string, string> = {}
|
|
243
|
+
for (const f of fields) if (typeof f?.key === 'string' && typeof f?.label === 'string' && f.label) out[f.key] = f.label
|
|
244
|
+
if (!cancelled) setPartyLabels(out)
|
|
245
|
+
})
|
|
246
|
+
.catch(() => {})
|
|
247
|
+
return () => {
|
|
248
|
+
cancelled = true
|
|
249
|
+
}
|
|
250
|
+
}, [api, partyModel])
|
|
251
|
+
|
|
223
252
|
// Cobro: documentos abiertos de la contraparte.
|
|
224
253
|
const openCfg = lineCfg?.open_documents
|
|
225
254
|
const allocPartyId = openCfg ? header[openCfg.party_field] ?? partyId : undefined
|
|
@@ -333,8 +362,11 @@ export function DocumentEditor({
|
|
|
333
362
|
params: { source: src.key, id, ...(draftId ? { exclude: draftId } : {}) },
|
|
334
363
|
})
|
|
335
364
|
return res?.data?.data ?? []
|
|
336
|
-
} catch
|
|
337
|
-
|
|
365
|
+
} catch {
|
|
366
|
+
// Un host sin `source-lines` (404, o la ruta del registro
|
|
367
|
+
// contestando 400/500 a «source-lines» como id) o un origen que
|
|
368
|
+
// el host aún no sabe calcular: se precarga de la relación y el
|
|
369
|
+
// guardado del servidor sigue validando lo pendiente.
|
|
338
370
|
}
|
|
339
371
|
}
|
|
340
372
|
let rels = relCache.current.get(src.model)
|
|
@@ -362,7 +394,7 @@ export function DocumentEditor({
|
|
|
362
394
|
try {
|
|
363
395
|
if (!isAllocation) {
|
|
364
396
|
const rows = await fetchSourceRows(src, id)
|
|
365
|
-
const loaded = linesFromSource(rows, src.map, { kind, discountMode: lineCfg.discount_mode })
|
|
397
|
+
const loaded = linesFromSource(rows, src.map, { kind, discountMode: lineCfg.discount_mode, defaultTaxRate: taxRate })
|
|
366
398
|
const { covered } = sourceLoadSummary(rows)
|
|
367
399
|
setLines(loaded)
|
|
368
400
|
setLoadedSource(src)
|
|
@@ -400,7 +432,7 @@ export function DocumentEditor({
|
|
|
400
432
|
setBusy(null)
|
|
401
433
|
}
|
|
402
434
|
},
|
|
403
|
-
[api, lineCfg, isAllocation, kind, t, fetchSourceRows],
|
|
435
|
+
[api, lineCfg, isAllocation, kind, t, fetchSourceRows, taxRate],
|
|
404
436
|
)
|
|
405
437
|
|
|
406
438
|
// Prefill al abrir desde un documento origen (acción de fila «Crear NC», «Facturar venta»…).
|
|
@@ -528,11 +560,10 @@ export function DocumentEditor({
|
|
|
528
560
|
</FieldCell>
|
|
529
561
|
) : null
|
|
530
562
|
|
|
531
|
-
const summaryRows = partySummaryRows(
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
)
|
|
563
|
+
const summaryRows = partySummaryRows(party, type.party?.summary, {
|
|
564
|
+
...Object.fromEntries(Object.entries(partyLabels).map(([k, v]) => [k, tl(v)])),
|
|
565
|
+
...Object.fromEntries(allFields.map((f) => [f.key, tl(f.label)])),
|
|
566
|
+
})
|
|
536
567
|
const advancedSummary = groups.advanced
|
|
537
568
|
.filter(visible)
|
|
538
569
|
.map((f) => {
|
|
@@ -563,175 +594,226 @@ export function DocumentEditor({
|
|
|
563
594
|
{ label: t('documentEditor.total', { defaultValue: 'Total' }), value: fmt.money(totals.total), emphasis: true },
|
|
564
595
|
]
|
|
565
596
|
|
|
566
|
-
const activeSource = sources.find((s) => s.key === sourceKey)
|
|
597
|
+
const activeSource = sources.find((s) => s.key === sourceKey) ?? (sources.length === 1 ? sources[0] : undefined)
|
|
567
598
|
const linesTitle = lineCfg?.title
|
|
568
599
|
? tl(lineCfg.title)
|
|
569
600
|
: isAllocation
|
|
570
601
|
? t('documentEditor.allocation_title', { defaultValue: 'Documentos a pagar' })
|
|
571
602
|
: t('documentEditor.lines_title', { defaultValue: 'Conceptos' })
|
|
603
|
+
// Sin acción de vista previa del servidor, un documento con renglones ofrece
|
|
604
|
+
// la suya local (encabezado, renglones y totales): sin borrador ni red.
|
|
605
|
+
const localPreview = !previewEnabled && !type.preview && !!lineCfg && !isAllocation && kind !== 'credit'
|
|
606
|
+
|
|
607
|
+
const essentials = (
|
|
608
|
+
<EditorSection slot="editor-essentials">
|
|
609
|
+
<FieldGrid>
|
|
610
|
+
{groups.party && fieldCell(groups.party)}
|
|
611
|
+
{groups.essential.map((f) => fieldCell(f))}
|
|
612
|
+
</FieldGrid>
|
|
613
|
+
{party && (
|
|
614
|
+
<PartyCard name={party.name ?? party.legal_name} rows={summaryRows} credit={credit} fmt={fmt}>
|
|
615
|
+
{partyContribs.map(({ id, component: C }) => (
|
|
616
|
+
<C key={id} {...contribProps} />
|
|
617
|
+
))}
|
|
618
|
+
</PartyCard>
|
|
619
|
+
)}
|
|
620
|
+
</EditorSection>
|
|
621
|
+
)
|
|
572
622
|
|
|
573
|
-
|
|
574
|
-
<
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
{partyContribs.map(({ id, component: C }) => (
|
|
586
|
-
<C key={id} {...contribProps} />
|
|
587
|
-
))}
|
|
588
|
-
</PartyCard>
|
|
589
|
-
)}
|
|
590
|
-
</EditorSection>
|
|
591
|
-
|
|
592
|
-
{sources.length > 0 && !isAllocation && (
|
|
593
|
-
<EditorSection
|
|
594
|
-
slot="load-from-source"
|
|
595
|
-
title={t('documentEditor.load_from', { defaultValue: 'Cargar desde…' })}
|
|
596
|
-
hint={busy === 'source' ? t('documentEditor.loading_source', { defaultValue: 'Cargando…' }) : sourceNote}
|
|
597
|
-
>
|
|
598
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
599
|
-
{sources.length > 1 && (
|
|
600
|
-
<select
|
|
601
|
-
aria-label={t('documentEditor.source_kind', { defaultValue: 'Tipo de documento origen' })}
|
|
602
|
-
className="h-9 rounded-md border bg-background px-2 text-sm"
|
|
603
|
-
value={sourceKey}
|
|
604
|
-
onChange={(e) => {
|
|
605
|
-
setSourceKey(e.target.value)
|
|
606
|
-
setSourceId('')
|
|
607
|
-
}}
|
|
608
|
-
>
|
|
609
|
-
<option value="">{t('documentEditor.choose_source', { defaultValue: 'Elige…' })}</option>
|
|
610
|
-
{sources.map((s) => (
|
|
611
|
-
<option key={s.key} value={s.key}>
|
|
612
|
-
{tl(s.label)}
|
|
613
|
-
</option>
|
|
614
|
-
))}
|
|
615
|
-
</select>
|
|
616
|
-
)}
|
|
617
|
-
{(activeSource ?? (sources.length === 1 ? sources[0] : undefined)) && (
|
|
618
|
-
<SourcePicker
|
|
619
|
-
source={(activeSource ?? sources[0])!}
|
|
620
|
-
value={sourceId}
|
|
621
|
-
header={header}
|
|
622
|
-
label={tl((activeSource ?? sources[0])!.label)}
|
|
623
|
-
onPick={(id) => {
|
|
624
|
-
if (!activeSource) setSourceKey(sources[0].key)
|
|
625
|
-
setSourceId(id)
|
|
626
|
-
const src = activeSource ?? sources[0]
|
|
627
|
-
if (id) void loadFromSource(src, id)
|
|
628
|
-
}}
|
|
629
|
-
/>
|
|
630
|
-
)}
|
|
631
|
-
</div>
|
|
632
|
-
</EditorSection>
|
|
633
|
-
)}
|
|
634
|
-
|
|
635
|
-
{lineCfg && (
|
|
636
|
-
<EditorSection
|
|
637
|
-
slot={isAllocation ? 'editor-allocation' : 'editor-lines'}
|
|
638
|
-
title={linesTitle}
|
|
639
|
-
hint={
|
|
640
|
-
isAllocation && openBalance > 0 && amountKey ? (
|
|
641
|
-
<button
|
|
642
|
-
type="button"
|
|
643
|
-
className="underline-offset-2 hover:underline"
|
|
644
|
-
onClick={() => updateField(amountKey, openBalance)}
|
|
645
|
-
>
|
|
646
|
-
{t('documentEditor.pay_all', { defaultValue: 'Cobrar todo ({{amount}})', amount: fmt.money(openBalance) })}
|
|
647
|
-
</button>
|
|
648
|
-
) : undefined
|
|
649
|
-
}
|
|
623
|
+
const loadFrom = sources.length > 0 && !isAllocation && (
|
|
624
|
+
<EditorSection
|
|
625
|
+
slot="load-from-source"
|
|
626
|
+
title={t('documentEditor.load_from', { defaultValue: 'Cargar desde…' })}
|
|
627
|
+
hint={busy === 'source' ? t('documentEditor.loading_source', { defaultValue: 'Cargando…' }) : sourceNote}
|
|
628
|
+
>
|
|
629
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
630
|
+
{sources.length > 1 && (
|
|
631
|
+
<div
|
|
632
|
+
role="radiogroup"
|
|
633
|
+
aria-label={t('documentEditor.source_kind', { defaultValue: 'Tipo de documento origen' })}
|
|
634
|
+
className="inline-flex flex-wrap gap-1 rounded-md border p-1"
|
|
650
635
|
>
|
|
651
|
-
{
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
/>
|
|
662
|
-
) : (
|
|
663
|
-
<p className="text-sm text-muted-foreground">
|
|
664
|
-
{t('documentEditor.pick_party_first', { defaultValue: 'Elige el cliente para ver sus documentos con saldo.' })}
|
|
665
|
-
</p>
|
|
666
|
-
)
|
|
667
|
-
) : (
|
|
668
|
-
<DocumentLinesGrid
|
|
669
|
-
value={lines}
|
|
670
|
-
onChange={(next) => {
|
|
671
|
-
setLines(next)
|
|
672
|
-
setServerIssues([])
|
|
636
|
+
{sources.map((s) => (
|
|
637
|
+
<button
|
|
638
|
+
key={s.key}
|
|
639
|
+
type="button"
|
|
640
|
+
role="radio"
|
|
641
|
+
aria-checked={s.key === sourceKey}
|
|
642
|
+
data-source-kind={s.key}
|
|
643
|
+
onClick={() => {
|
|
644
|
+
setSourceKey(s.key)
|
|
645
|
+
setSourceId('')
|
|
673
646
|
}}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
currency={currency}
|
|
681
|
-
/>
|
|
682
|
-
)}
|
|
683
|
-
<TotalsPanel rows={totalsRows} />
|
|
684
|
-
</EditorSection>
|
|
685
|
-
)}
|
|
686
|
-
|
|
687
|
-
{(groups.advanced.length > 0 || headerContribs.length > 0) && (
|
|
688
|
-
<CollapsibleSection
|
|
689
|
-
slot="advanced-options"
|
|
690
|
-
title={t('documentEditor.advanced', { defaultValue: 'Opciones fiscales' })}
|
|
691
|
-
summary={advancedSummary || t('documentEditor.advanced_defaults', { defaultValue: 'Valores sugeridos' })}
|
|
692
|
-
open={advancedOpen}
|
|
693
|
-
onOpenChange={setAdvancedOpen}
|
|
694
|
-
>
|
|
695
|
-
{groups.advanced.length > 0 && <FieldGrid>{groups.advanced.map((f) => fieldCell(f))}</FieldGrid>}
|
|
696
|
-
{headerContribs.map(({ id, component: C }) => (
|
|
697
|
-
<C key={id} {...contribProps} />
|
|
647
|
+
className={`rounded px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
|
648
|
+
s.key === sourceKey ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground hover:bg-accent'
|
|
649
|
+
}`}
|
|
650
|
+
>
|
|
651
|
+
{tl(s.label)}
|
|
652
|
+
</button>
|
|
698
653
|
))}
|
|
699
|
-
</
|
|
654
|
+
</div>
|
|
700
655
|
)}
|
|
656
|
+
{activeSource && (
|
|
657
|
+
<SourcePicker
|
|
658
|
+
key={activeSource.key}
|
|
659
|
+
source={activeSource}
|
|
660
|
+
value={sourceId}
|
|
661
|
+
header={header}
|
|
662
|
+
label={tl(activeSource.label)}
|
|
663
|
+
onPick={(id) => {
|
|
664
|
+
setSourceKey(activeSource.key)
|
|
665
|
+
setSourceId(id)
|
|
666
|
+
if (id) void loadFromSource(activeSource, id)
|
|
667
|
+
}}
|
|
668
|
+
/>
|
|
669
|
+
)}
|
|
670
|
+
</div>
|
|
671
|
+
</EditorSection>
|
|
672
|
+
)
|
|
701
673
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
674
|
+
const totalsPanel = <TotalsPanel rows={totalsRows} />
|
|
675
|
+
|
|
676
|
+
const linesSection = lineCfg && (
|
|
677
|
+
<EditorSection
|
|
678
|
+
slot={isAllocation ? 'editor-allocation' : 'editor-lines'}
|
|
679
|
+
title={linesTitle}
|
|
680
|
+
hint={
|
|
681
|
+
isAllocation && openBalance > 0 && amountKey ? (
|
|
682
|
+
<button type="button" className="underline-offset-2 hover:underline" onClick={() => updateField(amountKey, openBalance)}>
|
|
683
|
+
{t('documentEditor.pay_all', { defaultValue: 'Cobrar todo ({{amount}})', amount: fmt.money(openBalance) })}
|
|
684
|
+
</button>
|
|
685
|
+
) : undefined
|
|
686
|
+
}
|
|
687
|
+
>
|
|
688
|
+
{isAllocation ? (
|
|
689
|
+
allocPartyId ? (
|
|
690
|
+
<PaymentAllocator
|
|
691
|
+
documents={openDocs}
|
|
692
|
+
result={allocation}
|
|
693
|
+
value={alloc}
|
|
694
|
+
onChange={setAlloc}
|
|
695
|
+
currency={currency}
|
|
696
|
+
loading={docsLoading}
|
|
697
|
+
today={today}
|
|
698
|
+
/>
|
|
699
|
+
) : (
|
|
700
|
+
<p className="text-sm text-muted-foreground">
|
|
701
|
+
{t('documentEditor.pick_party_first', { defaultValue: 'Elige el cliente para ver sus documentos con saldo.' })}
|
|
702
|
+
</p>
|
|
703
|
+
)
|
|
704
|
+
) : (
|
|
705
|
+
<DocumentLinesGrid
|
|
706
|
+
value={lines}
|
|
707
|
+
onChange={(next) => {
|
|
708
|
+
setLines(next)
|
|
709
|
+
setServerIssues([])
|
|
710
|
+
}}
|
|
711
|
+
columns={(lineCfg.columns as LineItemsColumn[] | undefined) ?? ['discount', 'tax']}
|
|
712
|
+
priceSource={lineCfg.price_source ?? (kind === 'purchase' ? 'cost' : 'sale')}
|
|
713
|
+
discountMode={lineCfg.discount_mode}
|
|
714
|
+
mode={linesFromSourceDoc ? 'from_source' : 'free'}
|
|
715
|
+
policy={kind === 'credit' ? { allowZeroQuantity: linesFromSourceDoc } : undefined}
|
|
716
|
+
search={search}
|
|
717
|
+
currency={currency}
|
|
718
|
+
/>
|
|
719
|
+
)}
|
|
720
|
+
{!fullscreen && totalsPanel}
|
|
721
|
+
</EditorSection>
|
|
722
|
+
)
|
|
707
723
|
|
|
708
|
-
|
|
724
|
+
const advanced = (groups.advanced.length > 0 || headerContribs.length > 0) && (
|
|
725
|
+
<CollapsibleSection
|
|
726
|
+
slot="advanced-options"
|
|
727
|
+
title={t('documentEditor.advanced', { defaultValue: 'Opciones fiscales' })}
|
|
728
|
+
summary={advancedSummary || t('documentEditor.advanced_defaults', { defaultValue: 'Valores sugeridos' })}
|
|
729
|
+
open={advancedOpen}
|
|
730
|
+
onOpenChange={setAdvancedOpen}
|
|
731
|
+
>
|
|
732
|
+
{groups.advanced.length > 0 && <FieldGrid>{groups.advanced.map((f) => fieldCell(f))}</FieldGrid>}
|
|
733
|
+
{headerContribs.map(({ id, component: C }) => (
|
|
734
|
+
<C key={id} {...contribProps} />
|
|
735
|
+
))}
|
|
736
|
+
</CollapsibleSection>
|
|
737
|
+
)
|
|
709
738
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
)}
|
|
730
|
-
</
|
|
731
|
-
|
|
739
|
+
const contributedSections = [...bodyContribs, ...sideContribs].map(({ id, component: C }) => (
|
|
740
|
+
<section key={id} data-contribution={id}>
|
|
741
|
+
<C {...contribProps} />
|
|
742
|
+
</section>
|
|
743
|
+
))
|
|
744
|
+
|
|
745
|
+
const notes = groups.notes.length > 0 && <FieldGrid>{groups.notes.map((f) => fieldCell(f, true))}</FieldGrid>
|
|
746
|
+
|
|
747
|
+
const previewSection = previewEnabled ? (
|
|
748
|
+
<CollapsibleSection slot="preview" title={tl(type.preview!.label ?? 'Vista previa')} open={previewOpen} onOpenChange={togglePreview}>
|
|
749
|
+
{busy === 'preview' ? (
|
|
750
|
+
<p className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
751
|
+
<Loader2 className="size-4 animate-spin" aria-hidden />
|
|
752
|
+
{t('documentEditor.preview_loading', { defaultValue: 'Generando vista previa…' })}
|
|
753
|
+
</p>
|
|
754
|
+
) : (
|
|
755
|
+
<>
|
|
756
|
+
<PreviewPanel preview={preview} />
|
|
757
|
+
<Button type="button" variant="ghost" size="sm" onClick={() => void runPreview()} disabled={busy !== null}>
|
|
758
|
+
{t('documentEditor.preview_refresh', { defaultValue: 'Actualizar vista previa' })}
|
|
759
|
+
</Button>
|
|
760
|
+
</>
|
|
761
|
+
)}
|
|
762
|
+
</CollapsibleSection>
|
|
763
|
+
) : localPreview ? (
|
|
764
|
+
<CollapsibleSection slot="preview" title={t('documentEditor.preview', { defaultValue: 'Vista previa' })} open={previewOpen} onOpenChange={setPreviewOpen}>
|
|
765
|
+
<DraftPreview
|
|
766
|
+
title={tl(type.label)}
|
|
767
|
+
party={party ? String(party.name ?? party.legal_name ?? '') : undefined}
|
|
768
|
+
header={[...groups.essential, ...groups.advanced]
|
|
769
|
+
.filter(visible)
|
|
770
|
+
.map((f) => {
|
|
771
|
+
const v = header[f.key]
|
|
772
|
+
const opt = f.options?.find((o) => String(o.value) === String(v))
|
|
773
|
+
return { label: tl(f.label), value: v == null || v === '' ? '' : opt ? friendlyOptionLabel(tl(String(opt.label ?? v))) : String(v) }
|
|
774
|
+
})
|
|
775
|
+
.filter((r) => r.value)}
|
|
776
|
+
lines={lines}
|
|
777
|
+
totals={totalsRows}
|
|
778
|
+
fmt={fmt}
|
|
779
|
+
/>
|
|
780
|
+
</CollapsibleSection>
|
|
781
|
+
) : null
|
|
732
782
|
|
|
733
|
-
|
|
734
|
-
|
|
783
|
+
const checklist = <ValidationChecklist issues={shownIssues} title={t('documentEditor.review', { defaultValue: 'Revisa antes de guardar' })} />
|
|
784
|
+
|
|
785
|
+
return (
|
|
786
|
+
<div className="flex min-h-0 flex-1 flex-col" data-slot="document-editor" data-kind={kind} data-layout={fullscreen ? 'fullscreen' : 'dialog'}>
|
|
787
|
+
{fullscreen ? (
|
|
788
|
+
<div className="-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-2 lg:grid lg:grid-cols-[minmax(0,1fr)_340px] lg:gap-8">
|
|
789
|
+
<div className="min-w-0 space-y-8" data-slot="editor-main">
|
|
790
|
+
<FormErrorBanner message={formError} />
|
|
791
|
+
{essentials}
|
|
792
|
+
{loadFrom}
|
|
793
|
+
{linesSection}
|
|
794
|
+
{advanced}
|
|
795
|
+
{contributedSections}
|
|
796
|
+
{notes}
|
|
797
|
+
</div>
|
|
798
|
+
<aside className="mt-8 space-y-6 lg:sticky lg:top-0 lg:mt-0 lg:self-start" data-slot="editor-aside">
|
|
799
|
+
<div className="rounded-lg border p-4">{totalsPanel}</div>
|
|
800
|
+
{checklist}
|
|
801
|
+
{previewSection}
|
|
802
|
+
</aside>
|
|
803
|
+
</div>
|
|
804
|
+
) : (
|
|
805
|
+
<div className="-mx-1 min-h-0 flex-1 space-y-8 overflow-y-auto px-1 py-2">
|
|
806
|
+
<FormErrorBanner message={formError} />
|
|
807
|
+
{essentials}
|
|
808
|
+
{loadFrom}
|
|
809
|
+
{linesSection}
|
|
810
|
+
{advanced}
|
|
811
|
+
{contributedSections}
|
|
812
|
+
{notes}
|
|
813
|
+
{previewSection}
|
|
814
|
+
{checklist}
|
|
815
|
+
</div>
|
|
816
|
+
)}
|
|
735
817
|
|
|
736
818
|
<footer className="flex shrink-0 flex-wrap items-center justify-end gap-2 border-t pt-4" data-slot="editor-actions">
|
|
737
819
|
{footerContribs.map(({ id, component: C }) => (
|
|
@@ -45,7 +45,7 @@ import { emitRecordMutation } from './record-mutation-events'
|
|
|
45
45
|
import { isLineItemsField, resolveWidget, scopeValueFromFilterToken } from './dynamic-form-schema'
|
|
46
46
|
import { createCatalogProductSearch, withDefaultTaxRate } from './business/catalog-product-search'
|
|
47
47
|
import { DocumentEditor } from './business/document-editor'
|
|
48
|
-
import { editorLinesConfig, isEditorLayout } from './business/document-editor-model'
|
|
48
|
+
import { editorLinesConfig, isEditorLayout, isFullscreenEditor } from './business/document-editor-model'
|
|
49
49
|
import type {
|
|
50
50
|
ActionFieldDef,
|
|
51
51
|
DocumentFormType,
|
|
@@ -336,14 +336,22 @@ export function DocumentFormDialog({
|
|
|
336
336
|
if (open && type && delegatedModel) return null
|
|
337
337
|
|
|
338
338
|
// layout "editor": una sola pantalla por secciones (DocumentEditor). Sin él
|
|
339
|
-
// sigue el wizard de siempre (retrocompatible).
|
|
339
|
+
// sigue el wizard de siempre (retrocompatible). Un documento con renglones
|
|
340
|
+
// (factura, cotización, pedido, OC) ocupa toda la pantalla; NC y cobro
|
|
341
|
+
// siguen en el diálogo grande.
|
|
340
342
|
if (open && type && step !== 'type' && (isEditorLayout(type) || !!initialSource)) {
|
|
343
|
+
const fullscreen = isFullscreenEditor(type, forms)
|
|
341
344
|
return (
|
|
342
345
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
343
346
|
<DialogContent
|
|
344
|
-
className=
|
|
345
|
-
style={
|
|
347
|
+
className={fullscreen ? 'flex flex-col overflow-hidden sm:max-w-none' : 'flex max-h-[92dvh] flex-col overflow-hidden'}
|
|
348
|
+
style={
|
|
349
|
+
fullscreen
|
|
350
|
+
? { width: 'calc(100vw - 2rem)', maxWidth: '1440px', height: 'calc(100dvh - 2rem)', maxHeight: 'calc(100dvh - 2rem)' }
|
|
351
|
+
: { maxHeight: '92dvh', maxWidth: '960px', width: '95vw' }
|
|
352
|
+
}
|
|
346
353
|
data-slot="document-editor-dialog"
|
|
354
|
+
data-fullscreen={fullscreen ? 'true' : undefined}
|
|
347
355
|
>
|
|
348
356
|
<DialogHeader className="shrink-0">
|
|
349
357
|
<DialogTitle>{tl(type.label)}</DialogTitle>
|
|
@@ -359,6 +367,7 @@ export function DocumentFormDialog({
|
|
|
359
367
|
initialSource={initialSource}
|
|
360
368
|
searchProducts={lineSearch}
|
|
361
369
|
currency={currency}
|
|
370
|
+
fullscreen={fullscreen}
|
|
362
371
|
onCancel={() => (canGoBackToTypes ? setStep('type') : onOpenChange(false))}
|
|
363
372
|
onSaved={(rec) => {
|
|
364
373
|
onSaved?.(rec)
|
package/src/index.ts
CHANGED
|
@@ -201,8 +201,10 @@ export {
|
|
|
201
201
|
} from './list-primary-action'
|
|
202
202
|
export {
|
|
203
203
|
emitRecordMutation,
|
|
204
|
+
emitRecordMutationSettled,
|
|
204
205
|
subscribeRecordMutations,
|
|
205
206
|
RECORD_MUTATION_EVENT,
|
|
207
|
+
RECORD_MUTATION_SETTLE_DELAYS,
|
|
206
208
|
type RecordMutationKind,
|
|
207
209
|
type RecordMutationDetail,
|
|
208
210
|
} from './record-mutation-events'
|
|
@@ -31,6 +31,29 @@ export function emitRecordMutation(model: string | undefined | null, kind: Recor
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Delays (ms) at which a mutation is announced again. The backend's event
|
|
36
|
+
* subscribers write AFTER the action answered (stamping → the invoice's state,
|
|
37
|
+
* a payment → its auto-stamped REP), so a list or tab that reloaded at once kept
|
|
38
|
+
* showing the previous state until a manual reload (retest Pitsline r5).
|
|
39
|
+
*/
|
|
40
|
+
export const RECORD_MUTATION_SETTLE_DELAYS: readonly number[] = [1500, 5000]
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Announce a mutation now and again after `delays`, so what the server derives
|
|
44
|
+
* asynchronously from it reaches mounted lists too. Returns a cancel function.
|
|
45
|
+
*/
|
|
46
|
+
export function emitRecordMutationSettled(
|
|
47
|
+
model: string | undefined | null,
|
|
48
|
+
kind: RecordMutationKind,
|
|
49
|
+
delays: readonly number[] = RECORD_MUTATION_SETTLE_DELAYS,
|
|
50
|
+
): () => void {
|
|
51
|
+
emitRecordMutation(model, kind)
|
|
52
|
+
if (!model || typeof window === 'undefined') return () => {}
|
|
53
|
+
const timers = delays.map((ms) => window.setTimeout(() => emitRecordMutation(model, kind), ms))
|
|
54
|
+
return () => timers.forEach((t) => window.clearTimeout(t))
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
/**
|
|
35
58
|
* Listen for mutations of `model` (case-insensitive). Returns an unsubscribe.
|
|
36
59
|
*/
|