@asteby/metacore-runtime-react 49.3.1 → 49.5.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.
Files changed (55) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/api-context.d.ts +9 -1
  3. package/dist/api-context.d.ts.map +1 -1
  4. package/dist/api-context.js +6 -4
  5. package/dist/business/document-editor-panels.d.ts +2 -1
  6. package/dist/business/document-editor-panels.d.ts.map +1 -1
  7. package/dist/business/document-editor-panels.js +6 -6
  8. package/dist/business/document-editor.d.ts.map +1 -1
  9. package/dist/business/document-editor.js +23 -9
  10. package/dist/business/index.d.ts +1 -0
  11. package/dist/business/index.d.ts.map +1 -1
  12. package/dist/business/index.js +1 -0
  13. package/dist/business/line-items-editor.d.ts +6 -1
  14. package/dist/business/line-items-editor.d.ts.map +1 -1
  15. package/dist/business/line-items-editor.js +109 -83
  16. package/dist/business/line-product-cell.d.ts +32 -0
  17. package/dist/business/line-product-cell.d.ts.map +1 -0
  18. package/dist/business/line-product-cell.js +100 -0
  19. package/dist/dynamic-select-field.d.ts +2 -1
  20. package/dist/dynamic-select-field.d.ts.map +1 -1
  21. package/dist/dynamic-select-field.js +49 -19
  22. package/dist/dynamic-table.d.ts +31 -1
  23. package/dist/dynamic-table.d.ts.map +1 -1
  24. package/dist/dynamic-table.js +37 -4
  25. package/dist/entity-select.d.ts.map +1 -1
  26. package/dist/entity-select.js +13 -11
  27. package/dist/field-grid.d.ts +7 -1
  28. package/dist/field-grid.d.ts.map +1 -1
  29. package/dist/field-grid.js +4 -2
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -0
  33. package/dist/query-batch.d.ts.map +1 -1
  34. package/dist/query-batch.js +17 -1
  35. package/dist/record-picker-actions.d.ts +27 -0
  36. package/dist/record-picker-actions.d.ts.map +1 -0
  37. package/dist/record-picker-actions.js +32 -0
  38. package/package.json +1 -1
  39. package/src/__tests__/api-batch-opt-out.test.tsx +83 -0
  40. package/src/__tests__/document-editor-inline-selects.test.tsx +202 -0
  41. package/src/__tests__/dynamic-table-bulk-actions.test.tsx +140 -0
  42. package/src/__tests__/dynamic-table-load-error.test.tsx +97 -0
  43. package/src/api-context.tsx +19 -4
  44. package/src/business/document-editor-panels.tsx +13 -10
  45. package/src/business/document-editor.tsx +72 -20
  46. package/src/business/index.ts +1 -0
  47. package/src/business/line-items-editor.tsx +229 -174
  48. package/src/business/line-product-cell.tsx +219 -0
  49. package/src/dynamic-select-field.tsx +51 -22
  50. package/src/dynamic-table.tsx +91 -4
  51. package/src/entity-select.tsx +17 -39
  52. package/src/field-grid.tsx +15 -1
  53. package/src/index.ts +2 -0
  54. package/src/query-batch.ts +18 -1
  55. package/src/record-picker-actions.tsx +81 -0
@@ -20,7 +20,7 @@
20
20
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
21
21
  import { useTranslation } from 'react-i18next'
22
22
  import { Button } from '@asteby/metacore-ui/primitives'
23
- import { Loader2 } from 'lucide-react'
23
+ import { Loader2, ReceiptText } from 'lucide-react'
24
24
  import { toast } from 'sonner'
25
25
  import { useApi } from '../api-context'
26
26
  import { useOrgTaxRate, useTimeZone } from '../org-runtime-context'
@@ -584,10 +584,10 @@ export function DocumentEditor({
584
584
  }
585
585
 
586
586
  const fieldRecord = useMemo(() => seedRecord(seeds, record), [seeds, record])
587
- const fieldCell = (field: ActionFieldDef, fullWidth = false) =>
587
+ const fieldCell = (field: ActionFieldDef, fullWidth = false, className?: string) =>
588
588
  visible(field) ? (
589
- <FieldCell key={field.key} fullWidth={fullWidth}>
590
- <FieldLabel htmlFor={field.key} required={field.required}>
589
+ <FieldCell key={field.key} fullWidth={fullWidth} className={className}>
590
+ <FieldLabel htmlFor={field.key} required={field.required} tone="sentence">
591
591
  {tl(field.label)}
592
592
  </FieldLabel>
593
593
  {renderField(withFriendlyOptions(gatedOptions(field, header)), header[field.key], (v: any) => updateField(field.key, v), header, fieldRecord, fieldErrors)}
@@ -641,10 +641,14 @@ export function DocumentEditor({
641
641
 
642
642
  const essentials = (
643
643
  <EditorSection slot="editor-essentials">
644
- <FieldGrid>
645
- {groups.party && fieldCell(groups.party)}
646
- {groups.essential.map((f) => fieldCell(f))}
647
- </FieldGrid>
644
+ {/* Contraparte + fechas + condiciones en un bloque: tres columnas cuando
645
+ el contenedor lo permite (la contraparte ocupa dos), una en móvil. */}
646
+ <div className="@container rounded-xl border bg-card/40 p-4">
647
+ <FieldGrid className="gap-x-4 @2xl:grid-cols-3">
648
+ {groups.party && fieldCell(groups.party, false, '@2xl:col-span-2')}
649
+ {groups.essential.map((f) => fieldCell(f))}
650
+ </FieldGrid>
651
+ </div>
648
652
  {party && (
649
653
  <PartyCard name={party.name ?? party.legal_name} rows={summaryRows} credit={credit} fmt={fmt}>
650
654
  {partyContribs.map(({ id, component: C }) => (
@@ -659,14 +663,18 @@ export function DocumentEditor({
659
663
  <EditorSection
660
664
  slot="load-from-source"
661
665
  title={t('documentEditor.load_from', { defaultValue: 'Cargar desde…' })}
662
- hint={busy === 'source' ? t('documentEditor.loading_source', { defaultValue: 'Cargando…' }) : sourceNote}
666
+ hint={
667
+ busy === 'source'
668
+ ? t('documentEditor.loading_source', { defaultValue: 'Cargando…' })
669
+ : sourceNote ?? (sourceId ? undefined : t('documentEditor.load_from_hint', { defaultValue: 'Opcional · precarga contraparte y renglones' }))
670
+ }
663
671
  >
664
672
  <div className="flex flex-wrap items-center gap-2">
665
673
  {sources.length > 1 && (
666
674
  <div
667
675
  role="radiogroup"
668
676
  aria-label={t('documentEditor.source_kind', { defaultValue: 'Documento de origen' })}
669
- className="inline-flex flex-wrap gap-1 rounded-md border p-1"
677
+ className="inline-flex h-9 shrink-0 items-center gap-0.5 rounded-md border bg-muted/40 p-0.5"
670
678
  >
671
679
  {sources.map((s) => (
672
680
  <button
@@ -679,8 +687,10 @@ export function DocumentEditor({
679
687
  setSourceKey(s.key)
680
688
  setSourceId('')
681
689
  }}
682
- className={`rounded px-3 py-1.5 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
683
- s.key === sourceKey ? 'bg-primary/10 font-medium text-primary' : 'text-muted-foreground hover:bg-accent'
690
+ className={`h-full rounded-sm px-3 text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/50 ${
691
+ s.key === sourceKey
692
+ ? 'bg-background font-medium text-foreground shadow-xs dark:bg-input/60'
693
+ : 'text-muted-foreground hover:text-foreground'
684
694
  }`}
685
695
  >
686
696
  {tl(s.label)}
@@ -688,6 +698,14 @@ export function DocumentEditor({
688
698
  ))}
689
699
  </div>
690
700
  )}
701
+ {!activeSource && (
702
+ <div
703
+ className="flex h-9 min-w-60 flex-1 items-center rounded-md border border-dashed px-3 text-sm text-muted-foreground"
704
+ data-slot="source-picker-empty"
705
+ >
706
+ {t('documentEditor.pick_source_kind', { defaultValue: 'Elige el tipo de documento para buscarlo' })}
707
+ </div>
708
+ )}
691
709
  {activeSource && (
692
710
  <SourcePicker
693
711
  key={activeSource.key}
@@ -707,7 +725,19 @@ export function DocumentEditor({
707
725
  </EditorSection>
708
726
  )
709
727
 
710
- const totalsPanel = <TotalsPanel rows={totalsRows} />
728
+ // Sin renglones todavía: un estado vacío amable en vez de $0.00 en cada fila.
729
+ const noLines = !isAllocation && !!lineCfg && lines.length === 0
730
+ const totalsPanel = noLines ? (
731
+ <TotalsPanel rows={totalsRows.filter((r) => r.emphasis)} muted />
732
+ ) : (
733
+ <TotalsPanel rows={totalsRows} />
734
+ )
735
+ const totalsEmpty = noLines && (
736
+ <div className="flex items-start gap-3 text-sm text-muted-foreground" data-slot="totals-empty">
737
+ <ReceiptText className="mt-0.5 size-4 shrink-0" aria-hidden />
738
+ <p>{t('documentEditor.totals_empty', { defaultValue: 'Aún no hay renglones. Busca un producto en la tabla para ver el desglose.' })}</p>
739
+ </div>
740
+ )
711
741
 
712
742
  const linesSection = lineCfg && (
713
743
  <EditorSection
@@ -751,6 +781,7 @@ export function DocumentEditor({
751
781
  policy={kind === 'credit' ? { allowZeroQuantity: linesFromSourceDoc } : undefined}
752
782
  search={search}
753
783
  currency={currency}
784
+ showTotals={false}
754
785
  />
755
786
  )}
756
787
  {!fullscreen && totalsPanel}
@@ -817,13 +848,26 @@ export function DocumentEditor({
817
848
  </CollapsibleSection>
818
849
  ) : null
819
850
 
851
+ const isMac = typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || '')
820
852
  const checklist = <ValidationChecklist issues={shownIssues} title={t('documentEditor.review', { defaultValue: 'Revisa antes de guardar' })} />
821
853
 
822
854
  return (
823
- <div className="flex min-h-0 flex-1 flex-col" data-slot="document-editor" data-kind={kind} data-layout={fullscreen ? 'fullscreen' : 'dialog'}>
855
+ <div
856
+ className="flex min-h-0 flex-1 flex-col"
857
+ data-slot="document-editor"
858
+ data-kind={kind}
859
+ data-layout={fullscreen ? 'fullscreen' : 'dialog'}
860
+ onKeyDown={(e) => {
861
+ // ⌘/Ctrl+Enter guarda desde cualquier campo del editor.
862
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey) && busy === null) {
863
+ e.preventDefault()
864
+ void submit()
865
+ }
866
+ }}
867
+ >
824
868
  {fullscreen ? (
825
- <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">
826
- <div className="min-w-0 space-y-8" data-slot="editor-main">
869
+ <div className="-mx-1 min-h-0 flex-1 overflow-y-auto px-1 py-2 lg:grid lg:grid-cols-[minmax(0,1fr)_320px] lg:gap-6">
870
+ <div className="min-w-0 space-y-6" data-slot="editor-main">
827
871
  <FormErrorBanner message={formError} />
828
872
  {essentials}
829
873
  {loadFrom}
@@ -832,14 +876,17 @@ export function DocumentEditor({
832
876
  {contributedSections}
833
877
  {notes}
834
878
  </div>
835
- <aside className="mt-8 space-y-6 lg:sticky lg:top-0 lg:mt-0 lg:self-start" data-slot="editor-aside">
836
- <div className="rounded-lg border p-4">{totalsPanel}</div>
879
+ <aside className="mt-6 space-y-4 lg:sticky lg:top-0 lg:mt-0 lg:self-start" data-slot="editor-aside">
880
+ <div className="space-y-3 rounded-xl border bg-card/40 p-4">
881
+ {totalsEmpty}
882
+ {totalsPanel}
883
+ </div>
837
884
  {checklist}
838
885
  {previewSection}
839
886
  </aside>
840
887
  </div>
841
888
  ) : (
842
- <div className="-mx-1 min-h-0 flex-1 space-y-8 overflow-y-auto px-1 py-2">
889
+ <div className="-mx-1 min-h-0 flex-1 space-y-6 overflow-y-auto px-1 py-2">
843
890
  <FormErrorBanner message={formError} />
844
891
  {essentials}
845
892
  {loadFrom}
@@ -853,6 +900,11 @@ export function DocumentEditor({
853
900
  )}
854
901
 
855
902
  <footer className="flex shrink-0 flex-wrap items-center justify-end gap-2 border-t pt-4" data-slot="editor-actions">
903
+ <span className="mr-auto hidden items-center gap-1 text-xs text-muted-foreground sm:inline-flex" data-slot="editor-shortcut">
904
+ <kbd className="rounded border bg-muted px-1.5 py-0.5 font-mono text-[0.7rem]">{isMac ? '⌘' : 'Ctrl'}</kbd>
905
+ <kbd className="rounded border bg-muted px-1.5 py-0.5 font-mono text-[0.7rem]">Enter</kbd>
906
+ <span>{t('documentEditor.shortcut_save', { defaultValue: 'para guardar' })}</span>
907
+ </span>
856
908
  {footerContribs.map(({ id, component: C }) => (
857
909
  <C key={id} {...contribProps} />
858
910
  ))}
@@ -897,7 +949,7 @@ function SourcePicker({
897
949
  [source, label],
898
950
  )
899
951
  return (
900
- <div className="min-w-[240px] flex-1" data-source={source.key}>
952
+ <div className="min-w-60 flex-1" data-source={source.key}>
901
953
  {renderField(field, value, (v: any) => onPick(v == null || v === '' ? '' : String(v)), header, seed ? { id: seed.value, label: seed.label } : undefined)}
902
954
  </div>
903
955
  )
@@ -4,6 +4,7 @@ export * from './feedback'
4
4
  export * from './line-items'
5
5
  export * from './document-lines'
6
6
  export * from './line-items-editor'
7
+ export * from './line-product-cell'
7
8
  export * from './payment'
8
9
  export * from './payment-capture'
9
10
  export * from './refund'