@fayz-ai/admin 0.36.0 → 0.37.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/dist/index.js CHANGED
@@ -10,8 +10,8 @@ export { openEntity, resolveEntityAnchor, resolveEntityHref, resolveEntityRoute
10
10
  import { useAdminPath, navigateTo, normalizeAdminPath, routeScore, matchRoute } from './chunk-VX3WJLKK.js';
11
11
  export { navigateTo as adminNavigateTo, useAdminPath } from './chunk-VX3WJLKK.js';
12
12
  export { ImportWizard } from './chunk-46UABOOC.js';
13
- import { createArchetypeLookup, getPersonLookup } from './chunk-HZGF5N3A.js';
14
- export { ScheduleDateExceptions, ScheduleEditor, ScheduleGridPanel, createArchetypeLookup, deleteSchedule, getPersonBookable, getPersonLookup, getScheduleBlockConfig, getSchedules, mergeBlockSettings, parseBlockSettings, registerPersonLookup, replaceWeeklySchedules, saveSchedule, setPersonBookable, setScheduleBlockConfig, subscribeScheduleBlockConfig } from './chunk-HZGF5N3A.js';
13
+ import { createArchetypeLookup, getPersonLookup } from './chunk-TRDVM4ZO.js';
14
+ export { ScheduleDateExceptions, ScheduleEditor, ScheduleGridPanel, createArchetypeLookup, deleteSchedule, getPersonBookable, getPersonLookup, getScheduleBlockConfig, getSchedules, mergeBlockSettings, parseBlockSettings, registerPersonLookup, replaceWeeklySchedules, saveSchedule, setPersonBookable, setScheduleBlockConfig, subscribeScheduleBlockConfig } from './chunk-TRDVM4ZO.js';
15
15
  import { getFayzSupabaseClientOptional, getSupabaseClientOptional as getSupabaseClientOptional$1, useAppConfigStore, activePluginIds, hasPlugin, useWorkspaceSwitchingStore, searchWorkspaces, OrgMark, WorkspaceChooserGate, WorkspaceSwitchingHost, createFayzSupabaseClient, resolveAppId, createSupabaseOrgAdapter, createMockOrgAdapter, getFayzSupabaseClient } from './chunk-4YF6R7T3.js';
16
16
  export { OrgMark, activePluginIds, createFayzSupabaseClient, createMockOrgAdapter, createSupabaseOrgAdapter, getFayzSupabaseClient, getFayzSupabaseClientOptional, hasPlugin, orgInitials, useAppConfigStore } from './chunk-4YF6R7T3.js';
17
17
  import { readOrgSnapshot, useTenantOptional, useOrgAdapter, useTenant, OrgProvider, useOrgAdapterOptional } from './chunk-IDOTNT43.js';
@@ -32,7 +32,7 @@ import { useAuthStore, useAuth, buildAppHandoffUrl } from '@fayz-ai/auth';
32
32
  export { useAuth } from '@fayz-ai/auth';
33
33
  import { create, useStore as useStore$1 } from 'zustand';
34
34
  import * as LucideIcons from 'lucide-react';
35
- import { Pencil, Trash2, Upload, Search, ArrowDown, ArrowUp, ArrowUpDown, Check, Download, ChevronLeft, ChevronRight, AlertTriangle, Building2, Inbox, Package, Sparkles, Plug, DollarSign, Clock, X, WandSparkles, Loader2, ArrowDownToLine, ArrowUpFromLine, CheckCircle2, Crown, ArrowRight, History, SquarePen, Settings2, Mic, User, Plus, Minus, ListChecks, ChevronDown, ArrowLeft, Calendar, ClipboardList, CheckSquare, BookOpen, Lock, PinOff, Pin, Eye, Users, StickyNote, CheckSquare2, Square, RotateCcw, Copy, Globe, LayoutTemplate, Store, ChevronsUpDown, ExternalLink, PenSquare, RefreshCw, XCircle, Camera, Edit, Star, Link2Off, Layers, ShoppingBag, MoreHorizontal, ChevronUp, List, LayoutGrid, CircleDollarSign, Receipt, Settings, Mail, MessageCircle, Cake, HelpCircle, Wrench, ShieldCheck, MessageSquare, Paperclip, Infinity, CheckCheck, PartyPopper, Compass, Play, MapPin, Landmark, LocateFixed, ArrowUpCircle, ArrowUpRight, Pause, CalendarDays, CircleCheckBig, UserX, Ban, CircleAlert, CircleEllipsis, FileText, CircleDashed, Power, ArrowRightLeft, File as File$1, FileImage, FileSpreadsheet, AlertCircle, CreditCard, SlidersHorizontal, Puzzle, Shield, Palette, Blocks, ShieldAlert, UserCog, LogOut, Bell, Database, Radio, Zap, Save, ImagePlus, UserPlus, Contact, UserMinus, Send, Sun, Moon, Monitor, Languages, Info, Link2, KeyRound, Bug, Home, Heart, Briefcase, BarChart3, Tag, Truck, Globe2 } from 'lucide-react';
35
+ import { Pencil, Copy, Trash2, Upload, Search, ArrowDown, ArrowUp, ArrowUpDown, Check, Download, ChevronLeft, ChevronRight, AlertTriangle, Building2, Inbox, Package, Sparkles, Plug, DollarSign, Clock, X, WandSparkles, Loader2, ArrowDownToLine, ArrowUpFromLine, CheckCircle2, Crown, ArrowRight, History, SquarePen, Settings2, Mic, User, Plus, Minus, ListChecks, ChevronDown, ArrowLeft, Calendar, ClipboardList, CheckSquare, BookOpen, Lock, PinOff, Pin, Eye, Users, StickyNote, CheckSquare2, Square, RotateCcw, Globe, LayoutTemplate, Store, ChevronsUpDown, ExternalLink, PenSquare, RefreshCw, XCircle, Camera, Edit, Star, Link2Off, Layers, ShoppingBag, MoreHorizontal, ChevronUp, List, LayoutGrid, CircleDollarSign, Receipt, Settings, Mail, MessageCircle, Cake, HelpCircle, Wrench, ShieldCheck, MessageSquare, Paperclip, Infinity, CheckCheck, PartyPopper, Compass, Play, MapPin, Landmark, LocateFixed, ArrowUpCircle, ArrowUpRight, Pause, CalendarDays, CircleCheckBig, UserX, Ban, CircleAlert, CircleEllipsis, FileText, CircleDashed, Power, ArrowRightLeft, File as File$1, FileImage, FileSpreadsheet, AlertCircle, CreditCard, SlidersHorizontal, Puzzle, Shield, Palette, Blocks, ShieldAlert, UserCog, LogOut, Bell, Database, Radio, Zap, Save, ImagePlus, UserPlus, Contact, UserMinus, Send, Sun, Moon, Monitor, Languages, Info, Link2, KeyRound, Bug, Home, Heart, Briefcase, BarChart3, Tag, Truck, Globe2 } from 'lucide-react';
36
36
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
37
37
  import { createPortal } from 'react-dom';
38
38
  import { profileHasSystemPermission, mergeLimitDeclarations } from '@fayz-ai/core/access';
@@ -4078,7 +4078,9 @@ async function fetchUnitBootstrap(tenantId2, deviceId) {
4078
4078
  { unitColumn: v?.unit_column ?? "unit_id", shareable: v?.shareable !== false }
4079
4079
  ])
4080
4080
  ),
4081
- reachable: Array.isArray(raw.reachable) ? raw.reachable : void 0,
4081
+ // user_unit_ids_in walks the tree with UNION over (id, depth): a branch
4082
+ // reached directly AND through its HQ comes back twice.
4083
+ reachable: Array.isArray(raw.reachable) ? [...new Set(raw.reachable)] : void 0,
4082
4084
  devicePin: { set: Boolean(raw.device_pin_set), unitId: raw.device_pin ?? null },
4083
4085
  accountPin: { set: Boolean(raw.account_pin_set), unitId: raw.account_pin ?? null }
4084
4086
  };
@@ -22799,6 +22801,10 @@ function PayeeAccountsSection({
22799
22801
  const [form, setForm] = useState(EMPTY_FORM2);
22800
22802
  const [saving, setSaving] = useState(false);
22801
22803
  const [deleting, setDeleting] = useState(null);
22804
+ const can = usePermissionOptional();
22805
+ const canRead = can("financial", "read");
22806
+ const canEdit = can("financial", "edit");
22807
+ const canDelete = can("financial", "delete");
22802
22808
  const db2 = () => getSupabaseClientOptional();
22803
22809
  const load = useCallback(async () => {
22804
22810
  const client3 = db2();
@@ -22872,6 +22878,7 @@ function PayeeAccountsSection({
22872
22878
  await load();
22873
22879
  toast.success(t("crud.archetype.payeeAccounts.deleted"));
22874
22880
  };
22881
+ if (!canRead) return null;
22875
22882
  if (loading) {
22876
22883
  return /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsxs(CardContent, { className: "space-y-2 py-6", children: [
22877
22884
  /* @__PURE__ */ jsx("div", { className: "h-4 w-56 animate-pulse rounded bg-muted/40" }),
@@ -22887,7 +22894,7 @@ function PayeeAccountsSection({
22887
22894
  return /* @__PURE__ */ jsxs("div", { className: "space-y-3", children: [
22888
22895
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
22889
22896
  /* @__PURE__ */ jsx("h3", { className: "text-sm font-semibold", children: t("crud.archetype.payeeAccounts.title") }),
22890
- /* @__PURE__ */ jsxs(
22897
+ canEdit && /* @__PURE__ */ jsxs(
22891
22898
  Button,
22892
22899
  {
22893
22900
  type: "button",
@@ -22928,7 +22935,7 @@ function PayeeAccountsSection({
22928
22935
  (a.holder_name || a.holder_document) && /* @__PURE__ */ jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: [a.holder_name, a.holder_document].filter(Boolean).join(" \xB7 ") })
22929
22936
  ] }),
22930
22937
  /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 gap-1", children: [
22931
- /* @__PURE__ */ jsx(
22938
+ canEdit && /* @__PURE__ */ jsx(
22932
22939
  Button,
22933
22940
  {
22934
22941
  type: "button",
@@ -22943,7 +22950,7 @@ function PayeeAccountsSection({
22943
22950
  children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5" })
22944
22951
  }
22945
22952
  ),
22946
- /* @__PURE__ */ jsx(
22953
+ canDelete && /* @__PURE__ */ jsx(
22947
22954
  Button,
22948
22955
  {
22949
22956
  type: "button",
@@ -23472,21 +23479,77 @@ function RelatedSection({ parentId, related, compact }) {
23472
23479
  }
23473
23480
  }
23474
23481
  const shown = child.fields.filter((f) => f.showInTable !== false);
23482
+ const groupKey = related.duplicable ? child.fields.find((f) => f.type === "relation")?.key : void 0;
23483
+ const [lastEdit, setLastEdit] = React127.useState(null);
23484
+ function siblingsToSync(row, key) {
23485
+ if (!groupKey || key === groupKey || row[groupKey] == null) return [];
23486
+ return rows.filter((r) => r.id !== row.id && r[groupKey] === row[groupKey] && r[key] !== row[key]);
23487
+ }
23488
+ async function applyToSiblings(row, key) {
23489
+ const alvos = siblingsToSync(row, key);
23490
+ if (!alvos.length) return;
23491
+ const value = row[key];
23492
+ const ids = new Set(alvos.map((r) => r.id));
23493
+ const antes = rows;
23494
+ setRows((prev) => prev.map((r) => ids.has(r.id) ? { ...r, [key]: value } : r));
23495
+ setLastEdit(null);
23496
+ if (orfa) return;
23497
+ try {
23498
+ await Promise.all(alvos.map((r) => provider2.update(r.id, { [key]: value })));
23499
+ } catch (e) {
23500
+ setRows(antes);
23501
+ toast.error(errorMessage(e));
23502
+ }
23503
+ }
23475
23504
  async function patch(id, key, value, price) {
23476
23505
  const antes = rows.find((r) => r.id === id);
23477
23506
  if (!antes) return;
23507
+ if (key !== groupKey) setLastEdit({ id, key });
23478
23508
  const mudanca = { [key]: value, ...heranca(key, price, { ...antes, [key]: value }) };
23479
23509
  if (Object.entries(mudanca).every(([k, v]) => antes[k] === v)) return;
23480
23510
  setRows((prev) => prev.map((r) => r.id === id ? { ...r, ...mudanca } : r));
23481
23511
  if (orfa) return;
23482
- try {
23483
- await provider2.update(id, mudanca);
23484
- } catch (e) {
23485
- setRows((prev) => prev.map((r) => r.id === id ? antes : r));
23486
- toast.error(errorMessage(e));
23487
- }
23512
+ queueWrite(id, mudanca, antes);
23513
+ }
23514
+ const escritas = React127.useRef(/* @__PURE__ */ new Map());
23515
+ function queueWrite(id, mudanca, antes) {
23516
+ const atual = escritas.current.get(id);
23517
+ const entry = atual ?? { mudanca: {}, antes, fila: Promise.resolve() };
23518
+ entry.mudanca = { ...entry.mudanca, ...mudanca };
23519
+ if (entry.timer) clearTimeout(entry.timer);
23520
+ entry.timer = setTimeout(() => flushWrite(id), 400);
23521
+ escritas.current.set(id, entry);
23522
+ }
23523
+ function flushWrite(id) {
23524
+ const entry = escritas.current.get(id);
23525
+ if (!entry || !Object.keys(entry.mudanca).length) return;
23526
+ const { mudanca, antes } = entry;
23527
+ entry.mudanca = {};
23528
+ entry.timer = void 0;
23529
+ entry.fila = entry.fila.then(async () => {
23530
+ try {
23531
+ await provider2.update(id, mudanca);
23532
+ } catch (e) {
23533
+ setRows((prev) => prev.map((r) => r.id === id ? antes : r));
23534
+ toast.error(errorMessage(e));
23535
+ }
23536
+ });
23537
+ const fila = entry.fila;
23538
+ void fila.then(() => {
23539
+ const e = escritas.current.get(id);
23540
+ if (e && e.fila === fila && !e.timer) escritas.current.delete(id);
23541
+ });
23488
23542
  }
23543
+ React127.useEffect(() => () => {
23544
+ for (const [id, e] of escritas.current) {
23545
+ if (e.timer) {
23546
+ clearTimeout(e.timer);
23547
+ flushWrite(id);
23548
+ }
23549
+ }
23550
+ }, []);
23489
23551
  const required = child.fields.filter((f) => f.required);
23552
+ const criando = React127.useRef(false);
23490
23553
  async function commitDraft(key, next, price) {
23491
23554
  const completo = { ...next, ...heranca(key, price, next) };
23492
23555
  setDraft(completo);
@@ -23495,17 +23558,36 @@ function RelatedSection({ parentId, related, compact }) {
23495
23558
  );
23496
23559
  const pronto = { ...defaults, ...completo };
23497
23560
  if (required.some((f) => pronto[f.key] == null || pronto[f.key] === "")) return;
23561
+ if (criando.current) return;
23498
23562
  if (orfa) {
23499
23563
  setRows((prev) => [...prev, { id: `draft-${prev.length}-${completo[key]}`, ...pronto }]);
23500
23564
  setDraft(null);
23501
23565
  return;
23502
23566
  }
23567
+ criando.current = true;
23503
23568
  try {
23504
23569
  await provider2.create(pronto);
23505
23570
  setDraft(null);
23506
23571
  await load();
23507
23572
  } catch (e) {
23508
23573
  toast.error(errorMessage(e));
23574
+ } finally {
23575
+ criando.current = false;
23576
+ }
23577
+ }
23578
+ async function duplicate(row) {
23579
+ const copia = Object.fromEntries(
23580
+ child.fields.filter((f) => row[f.key] !== void 0).map((f) => [f.key, row[f.key]])
23581
+ );
23582
+ if (orfa) {
23583
+ setRows((prev) => [...prev, { ...copia, id: `draft-${prev.length}-copy` }]);
23584
+ return;
23585
+ }
23586
+ try {
23587
+ await provider2.create(copia);
23588
+ await load();
23589
+ } catch (e) {
23590
+ toast.error(errorMessage(e));
23509
23591
  }
23510
23592
  }
23511
23593
  function heranca(fieldKey, price, linha) {
@@ -23608,25 +23690,57 @@ function RelatedSection({ parentId, related, compact }) {
23608
23690
  error ? /* @__PURE__ */ jsx("p", { className: "rounded-lg border border-destructive/30 bg-destructive-soft px-3 py-2 text-xs text-destructive-soft-foreground", children: error }) : loading ? /* @__PURE__ */ jsx("div", { className: "h-8 w-40 animate-pulse rounded bg-muted/40" }) : rows.length === 0 && draft === null ? /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: compact ? "py-3" : "py-4", children: /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: related.emptyHint ?? "\u2014" }) }) }) : /* @__PURE__ */ jsx(Card, { children: /* @__PURE__ */ jsx(CardContent, { className: "overflow-x-auto p-0", children: /* @__PURE__ */ jsxs("table", { className: "w-full min-w-[26rem] text-xs", children: [
23609
23691
  /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", { className: "border-b text-left text-[10px] font-medium uppercase tracking-wide text-muted-foreground", children: [
23610
23692
  shown.map((f) => /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-medium", children: f.label }, f.key)),
23611
- /* @__PURE__ */ jsx("th", { className: "w-10 px-3 py-2" })
23693
+ /* @__PURE__ */ jsx("th", { className: related.duplicable ? "w-16 px-3 py-2" : "w-10 px-3 py-2" })
23612
23694
  ] }) }),
23613
23695
  /* @__PURE__ */ jsxs("tbody", { children: [
23614
23696
  rows.map((row) => /* @__PURE__ */ jsxs("tr", { className: "border-b last:border-0", children: [
23615
- shown.map((f) => /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 align-middle", children: cell(f, row[f.key], (next, price) => {
23616
- void patch(row.id, f.key, next, price);
23617
- }, row) }, f.key)),
23618
- /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 text-right", children: /* @__PURE__ */ jsx(
23619
- "button",
23620
- {
23621
- type: "button",
23622
- "aria-label": `${label3}: remover`,
23623
- onClick: () => {
23624
- void remove(row.id);
23625
- },
23626
- className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive",
23627
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
23628
- }
23629
- ) })
23697
+ shown.map((f) => /* @__PURE__ */ jsxs("td", { className: "px-3 py-1.5 align-middle", children: [
23698
+ cell(f, row[f.key], (next, price) => {
23699
+ void patch(row.id, f.key, next, price);
23700
+ }, row),
23701
+ lastEdit?.id === row.id && lastEdit?.key === f.key && siblingsToSync(row, f.key).length > 0 && /* @__PURE__ */ jsxs(
23702
+ "button",
23703
+ {
23704
+ type: "button",
23705
+ onClick: () => {
23706
+ void applyToSiblings(row, f.key);
23707
+ },
23708
+ className: "mt-1 text-[11px] font-medium text-primary hover:underline",
23709
+ children: [
23710
+ "Aplicar para todos (",
23711
+ siblingsToSync(row, f.key).length,
23712
+ ")"
23713
+ ]
23714
+ }
23715
+ )
23716
+ ] }, f.key)),
23717
+ /* @__PURE__ */ jsxs("td", { className: "whitespace-nowrap px-3 py-1.5 text-right", children: [
23718
+ related.duplicable && /* @__PURE__ */ jsx(
23719
+ "button",
23720
+ {
23721
+ type: "button",
23722
+ "aria-label": `${label3}: duplicar`,
23723
+ title: "Duplicar",
23724
+ onClick: () => {
23725
+ void duplicate(row);
23726
+ },
23727
+ className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
23728
+ children: /* @__PURE__ */ jsx(Copy, { className: "h-3.5 w-3.5" })
23729
+ }
23730
+ ),
23731
+ /* @__PURE__ */ jsx(
23732
+ "button",
23733
+ {
23734
+ type: "button",
23735
+ "aria-label": `${label3}: remover`,
23736
+ onClick: () => {
23737
+ void remove(row.id);
23738
+ },
23739
+ className: "rounded-md p-1 text-muted-foreground transition-colors hover:bg-muted hover:text-destructive",
23740
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
23741
+ }
23742
+ )
23743
+ ] })
23630
23744
  ] }, row.id)),
23631
23745
  draft !== null && /* @__PURE__ */ jsxs("tr", { className: "border-b last:border-0 bg-muted/20", children: [
23632
23746
  shown.map((f) => /* @__PURE__ */ jsx("td", { className: "px-3 py-1.5 align-middle", children: cell(
@@ -23698,6 +23812,27 @@ function RelatedSection({ parentId, related, compact }) {
23698
23812
  )
23699
23813
  ] });
23700
23814
  }
23815
+ async function copyRelatedRows(related, fromParentId, toParentId) {
23816
+ const data = related.entity.data;
23817
+ if (!data) return;
23818
+ const fk = columnOf(related.entity, related.foreignKey);
23819
+ const reader = resolveDataProvider(
23820
+ { ...related.entity, data: { ...data, filters: { ...data.filters ?? {}, [fk]: fromParentId } } },
23821
+ [],
23822
+ { cache: false }
23823
+ );
23824
+ const writer = resolveDataProvider(
23825
+ { ...related.entity, data: { ...data, defaults: { ...data.defaults ?? {}, [fk]: toParentId } } },
23826
+ [],
23827
+ { cache: false }
23828
+ );
23829
+ const result = await reader.list({ page: 1, pageSize: 500 });
23830
+ const keys = related.entity.fields.map((f) => f.key).filter((k) => k !== related.foreignKey);
23831
+ for (const row of result?.data ?? []) {
23832
+ const copia = Object.fromEntries(keys.filter((k) => row[k] !== void 0).map((k) => [k, row[k]]));
23833
+ await writer.create({ ...copia, [related.foreignKey]: toParentId });
23834
+ }
23835
+ }
23701
23836
  var PRICING_FIELDS = /* @__PURE__ */ new Set(["price", "cost", "currency", "unit"]);
23702
23837
  var STOCK_FIELDS = /* @__PURE__ */ new Set(["stock", "minStock"]);
23703
23838
  var SKIP_FIELDS2 = /* @__PURE__ */ new Set(["status", "isActive", "tags", "imageUrl", "name", "description", "sku", "category"]);
@@ -24391,6 +24526,8 @@ function BankField({
24391
24526
  const t = useTranslation();
24392
24527
  const v = value ?? {};
24393
24528
  const withMethod = { ...v, method: v.method ?? payeeMethodOf(v) };
24529
+ const canEdit = usePermissionOptional()("financial", "edit");
24530
+ if (!canEdit) return null;
24394
24531
  return /* @__PURE__ */ jsxs(
24395
24532
  AttachedFieldSection,
24396
24533
  {
@@ -25757,6 +25894,8 @@ function useUnitFilter(options = {}) {
25757
25894
  const myUnitIds = useUnitStore((s) => s.myUnitIds);
25758
25895
  const activeUnitId = useUnitStore((s) => s.activeUnitId);
25759
25896
  const beginSwitch = useUnitSwitchingStore((s) => s.begin);
25897
+ const can = usePermissionOptional();
25898
+ const canManageUnits = can("units", "manage");
25760
25899
  const reachable = React127.useMemo(() => unitFilterOptions(units, myUnitIds), [units, myUnitIds]);
25761
25900
  const [local2, setLocal] = React127.useState(activeUnitId);
25762
25901
  React127.useEffect(() => {
@@ -25783,8 +25922,13 @@ function useUnitFilter(options = {}) {
25783
25922
  options: reachable.map((u) => ({ value: u.id, label: u.name })),
25784
25923
  value,
25785
25924
  onChange: setValue,
25786
- searchable: reachable.length > 8
25787
- } : null, [visible, filterLabel, allLabel, reachable, value, setValue]);
25925
+ // Unit names are typed, not scrolled for — search even for a short list.
25926
+ searchable: true,
25927
+ searchPlaceholder: label(t, "units.search", "Buscar unidade"),
25928
+ manage: canManageUnits ? { label: label(t, "units.manage", "Gerenciar unidades"), onSelect: () => {
25929
+ window.location.hash = "#/settings/locations";
25930
+ } } : void 0
25931
+ } : null, [visible, filterLabel, allLabel, reachable, value, setValue, canManageUnits, t]);
25788
25932
  return { visible, value: visible ? value : unitMode ? activeUnitId : void 0, setValue, options: reachable, label: filterLabel, allLabel, def };
25789
25933
  }
25790
25934
  var ALL_TAB = "__all__";
@@ -26467,6 +26611,13 @@ function useCrudColumns(entityDef, options) {
26467
26611
  onSelect: () => setCrudHash(options.basePath, id, "edit"),
26468
26612
  hidden: options.feature ? !can(options.feature, "edit") : false
26469
26613
  },
26614
+ ...options.onDuplicate ? [{
26615
+ id: "duplicate",
26616
+ label: t("crud.list.duplicate"),
26617
+ icon: Copy,
26618
+ onSelect: () => options.onDuplicate(row.original),
26619
+ hidden: options.feature ? !can(options.feature, "create") : false
26620
+ }] : [],
26470
26621
  ...options.onDelete ? [{
26471
26622
  id: "delete",
26472
26623
  label: t("common.delete"),
@@ -26481,7 +26632,7 @@ function useCrudColumns(entityDef, options) {
26481
26632
  });
26482
26633
  }
26483
26634
  return tanCols;
26484
- }, [cols, displayField, options?.basePath, options?.onDelete, options?.feature, options?.readOnly, t, can]);
26635
+ }, [cols, displayField, options?.basePath, options?.onDelete, options?.onDuplicate, options?.feature, options?.readOnly, t, can]);
26485
26636
  }
26486
26637
  function CrudPage({ entityDef: rawEntityDef, useStore: useStore2, basePath, display, feature, readOnly, navTabs, activeNavTab, stats, embedded, emptyHint, aiCreate }) {
26487
26638
  const store = useStore2();
@@ -26572,7 +26723,33 @@ function CrudPage({ entityDef: rawEntityDef, useStore: useStore2, basePath, disp
26572
26723
  const handleInlineUpdate = useCallback(async (id, field, value) => {
26573
26724
  await store.update(id, { [field]: value });
26574
26725
  }, [store]);
26575
- const tanColumns = useCrudColumns(entityDef, { basePath: normalizedBasePath, onDelete: readOnly ? void 0 : (item) => setDeleteItem(item), onInlineUpdate: handleInlineUpdate, feature, readOnly });
26726
+ const handleDuplicate = useCallback(async (row) => {
26727
+ try {
26728
+ const display2 = entityDef.displayField ?? entityDef.fields[0]?.key;
26729
+ const copia = Object.fromEntries(
26730
+ entityDef.fields.filter((f) => f.showInForm !== false && row[f.key] !== void 0).map((f) => [f.key, row[f.key]])
26731
+ );
26732
+ if (display2 && typeof copia[display2] === "string") copia[display2] = `${copia[display2]} ${t("crud.list.copySuffix")}`;
26733
+ const created = await store.create(copia);
26734
+ for (const related of entityDef.relatedSections ?? []) {
26735
+ await copyRelatedRows(related, row.id, created.id);
26736
+ }
26737
+ toast.success(t("crud.list.duplicated", { entity: entityDef.name }));
26738
+ setCrudHash(normalizedBasePath, created.id, "edit");
26739
+ } catch (err) {
26740
+ toast.error(err instanceof Error ? err.message : String(err));
26741
+ }
26742
+ }, [entityDef, store, t, normalizedBasePath]);
26743
+ const tanColumns = useCrudColumns(entityDef, {
26744
+ basePath: normalizedBasePath,
26745
+ onDelete: readOnly ? void 0 : (item) => setDeleteItem(item),
26746
+ onDuplicate: readOnly || !entityDef.duplicable ? void 0 : (item) => {
26747
+ void handleDuplicate(item);
26748
+ },
26749
+ onInlineUpdate: handleInlineUpdate,
26750
+ feature,
26751
+ readOnly
26752
+ });
26576
26753
  const currentOrgId = useOrganizationStore((s) => s.currentOrg?.id);
26577
26754
  const guardImport = useLimitGuard(entityDef.limitKey ?? "");
26578
26755
  useEffect(() => {
@@ -27850,7 +28027,7 @@ function PersonAvailabilityTab({ item }) {
27850
28027
  ] }) });
27851
28028
  }
27852
28029
  var ScheduleEditor2 = React127__default.lazy(
27853
- () => import('./ScheduleEditor-VJYYNGSV.js').then((m) => ({ default: m.ScheduleEditor }))
28030
+ () => import('./ScheduleEditor-VGDE57L2.js').then((m) => ({ default: m.ScheduleEditor }))
27854
28031
  );
27855
28032
  var PersonTimeline2 = React127__default.lazy(
27856
28033
  () => import('./timeline-LECYQ4IA.js').then((m) => ({ default: m.PersonTimeline }))
@@ -35854,7 +36031,7 @@ function OrderDetailView(props) {
35854
36031
  /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: tile.label }),
35855
36032
  tile.icon && /* @__PURE__ */ jsx(tile.icon, { className: "h-4 w-4 shrink-0 text-muted-foreground/70" })
35856
36033
  ] }),
35857
- /* @__PURE__ */ jsx("p", { className: "mt-1 truncate text-lg font-semibold", children: tile.value })
36034
+ /* @__PURE__ */ jsx("div", { className: "mt-1 truncate text-lg font-semibold", children: tile.value })
35858
36035
  ] }, tile.id ?? i)) }),
35859
36036
  banner && /* @__PURE__ */ jsxs("div", { className: banner.tone === "warning" ? "flex items-center gap-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900" : "flex items-center gap-3 rounded-lg border border-border bg-muted/40 px-4 py-3 text-sm", children: [
35860
36037
  /* @__PURE__ */ jsx(AlertTriangle, { className: "h-4 w-4 shrink-0" }),
@@ -36130,7 +36307,8 @@ var ITEM_LABELS = {
36130
36307
  done: "Conclu\xEDdo",
36131
36308
  create: "Cadastrar",
36132
36309
  clear: "Limpar",
36133
- edit: "Editar"
36310
+ edit: "Editar",
36311
+ manage: "Gerenciar cadastro"
36134
36312
  };
36135
36313
  function OrderFormItemRow({
36136
36314
  index,
@@ -36167,6 +36345,11 @@ function OrderFormItemRow({
36167
36345
  () => (extraFieldGroups ?? []).filter((g) => g.filled).map((g) => g.id)
36168
36346
  );
36169
36347
  const lookup = entityLookups[itemKind];
36348
+ const awaitingCode = !!lookup?.byCode && !referenceId;
36349
+ const pickOnlyRow = pickOnly || awaitingCode;
36350
+ const can = usePermissionOptional();
36351
+ const manage = lookup?.manage;
36352
+ const canManage = !!manage && (!manage.permission || can(manage.permission.feature, manage.permission.action ?? "edit"));
36170
36353
  const itemCreate = useLookupCreate(lookup, (created) => {
36171
36354
  onChange({
36172
36355
  referenceId: created.id,
@@ -36205,12 +36388,12 @@ function OrderFormItemRow({
36205
36388
  },
36206
36389
  g.id
36207
36390
  )),
36208
- !pickOnly && /* @__PURE__ */ jsxs("span", { className: "fz-tight-hide shrink-0 text-xs text-muted-foreground tabular-nums", children: [
36391
+ !pickOnlyRow && /* @__PURE__ */ jsxs("span", { className: "fz-tight-hide shrink-0 text-xs text-muted-foreground tabular-nums", children: [
36209
36392
  quantity,
36210
36393
  " \xD7 ",
36211
36394
  formatMoney(unitPrice)
36212
36395
  ] }),
36213
- !pickOnly && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-right text-sm font-semibold tabular-nums", children: formatMoney(total) }),
36396
+ !pickOnlyRow && /* @__PURE__ */ jsx("span", { className: "shrink-0 text-right text-sm font-semibold tabular-nums", children: formatMoney(total) }),
36214
36397
  !readOnly && /* @__PURE__ */ jsx(
36215
36398
  "button",
36216
36399
  {
@@ -36239,7 +36422,20 @@ function OrderFormItemRow({
36239
36422
  tp.value
36240
36423
  )) }),
36241
36424
  /* @__PURE__ */ jsxs("div", { className: "fz-fields fz-fields-2", children: [
36242
- /* @__PURE__ */ jsx("div", { className: "min-w-0", children: lookup ? /* @__PURE__ */ jsx(
36425
+ /* @__PURE__ */ jsx("div", { className: "min-w-0", children: lookup?.byCode ? /* @__PURE__ */ jsx(
36426
+ CodeLookupField,
36427
+ {
36428
+ byCode: lookup.byCode,
36429
+ resolvedLabel: referenceId ? lookup.byCode.display?.({ description }) ?? description : null,
36430
+ clearLabel: l.clear,
36431
+ onResolved: (r) => onChange({
36432
+ referenceId: r.id,
36433
+ description: r.label,
36434
+ ...r.price != null ? { unitPrice: r.price } : {}
36435
+ }),
36436
+ onClear: () => onChange({ referenceId: void 0, description: "", unitPrice: 0 })
36437
+ }
36438
+ ) : lookup ? /* @__PURE__ */ jsx(
36243
36439
  SearchSelect,
36244
36440
  {
36245
36441
  label: l.item,
@@ -36263,6 +36459,8 @@ function OrderFormItemRow({
36263
36459
  onCreate: itemCreate.onCreate,
36264
36460
  onEdit: itemCreate.onEdit,
36265
36461
  editLabel: l.edit,
36462
+ onManage: canManage ? () => navigateTo(manage.route) : void 0,
36463
+ manageLabel: manage?.label ?? l.manage,
36266
36464
  placeholder: `${l.item}\u2026`,
36267
36465
  renderOption: (opt) => /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1", children: [
36268
36466
  /* @__PURE__ */ jsx("p", { className: "truncate text-sm", children: opt.label }),
@@ -36284,7 +36482,7 @@ function OrderFormItemRow({
36284
36482
  }
36285
36483
  )
36286
36484
  ] }) }),
36287
- !pickOnly && /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 gap-3", children: [
36485
+ !pickOnlyRow && /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 gap-3", children: [
36288
36486
  /* @__PURE__ */ jsxs("div", { className: "w-16 shrink-0", children: [
36289
36487
  /* @__PURE__ */ jsx("label", { className: "text-[10px] font-medium text-muted-foreground", children: l.qty }),
36290
36488
  /* @__PURE__ */ jsx(
@@ -36312,7 +36510,7 @@ function OrderFormItemRow({
36312
36510
  ) })
36313
36511
  ] })
36314
36512
  ] }),
36315
- showAdjustments && !pickOnly && /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-3", children: [
36513
+ showAdjustments && !pickOnlyRow && /* @__PURE__ */ jsxs("div", { className: "flex items-end gap-3", children: [
36316
36514
  /* @__PURE__ */ jsx("div", { className: "flex-1", children: /* @__PURE__ */ jsx(
36317
36515
  CurrencyInput,
36318
36516
  {
@@ -36350,7 +36548,7 @@ function OrderFormItemRow({
36350
36548
  )
36351
36549
  ] }),
36352
36550
  extraFields,
36353
- !pickOnly && (!showAdjustments || (extraFieldGroups ?? []).some((g) => !openExtras.includes(g.id))) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1.5", children: [
36551
+ !pickOnlyRow && (!showAdjustments || (extraFieldGroups ?? []).some((g) => !openExtras.includes(g.id))) && /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center gap-x-4 gap-y-1.5", children: [
36354
36552
  !showAdjustments && /* @__PURE__ */ jsxs(
36355
36553
  "button",
36356
36554
  {
@@ -36379,7 +36577,7 @@ function OrderFormItemRow({
36379
36577
  g.id
36380
36578
  ))
36381
36579
  ] }),
36382
- !pickOnly && (extraFieldGroups ?? []).filter((g) => openExtras.includes(g.id)).map((g) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
36580
+ !pickOnlyRow && (extraFieldGroups ?? []).filter((g) => openExtras.includes(g.id)).map((g) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
36383
36581
  /* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: g.field }),
36384
36582
  g.onClear && /* @__PURE__ */ jsx(
36385
36583
  "button",
@@ -36395,7 +36593,7 @@ function OrderFormItemRow({
36395
36593
  }
36396
36594
  )
36397
36595
  ] }, g.id)),
36398
- !pickOnly && /* @__PURE__ */ jsx("div", { className: "flex justify-end pt-1", children: /* @__PURE__ */ jsxs(
36596
+ !pickOnlyRow && /* @__PURE__ */ jsx("div", { className: "flex justify-end pt-1", children: /* @__PURE__ */ jsxs(
36399
36597
  "button",
36400
36598
  {
36401
36599
  type: "button",
@@ -36412,6 +36610,73 @@ function OrderFormItemRow({
36412
36610
  ] });
36413
36611
  }
36414
36612
  OrderFormItemRow.displayName = "OrderFormItemRow";
36613
+ function CodeLookupField({ byCode, resolvedLabel, clearLabel, onResolved, onClear }) {
36614
+ const [code, setCode] = React127.useState("");
36615
+ const [busy, setBusy] = React127.useState(false);
36616
+ const [error, setError] = React127.useState(null);
36617
+ async function submit() {
36618
+ if (!code.trim() || busy) return;
36619
+ setBusy(true);
36620
+ setError(null);
36621
+ try {
36622
+ onResolved(await byCode.resolve(code));
36623
+ setCode("");
36624
+ } catch (e) {
36625
+ setError(e instanceof Error ? e.message : String(e));
36626
+ } finally {
36627
+ setBusy(false);
36628
+ }
36629
+ }
36630
+ if (resolvedLabel) {
36631
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
36632
+ /* @__PURE__ */ jsx("label", { className: "text-[10px] font-medium text-muted-foreground", children: byCode.label }),
36633
+ /* @__PURE__ */ jsxs("div", { className: "mt-0.5 flex items-center gap-2 rounded-input border border-input bg-muted/40 px-2.5 py-1.5", children: [
36634
+ /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate font-mono text-sm", children: resolvedLabel }),
36635
+ /* @__PURE__ */ jsx("button", { type: "button", onClick: onClear, "aria-label": clearLabel, className: "p-0.5 text-muted-foreground transition-colors hover:text-destructive", children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" }) })
36636
+ ] })
36637
+ ] });
36638
+ }
36639
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
36640
+ /* @__PURE__ */ jsx("label", { className: "text-[10px] font-medium text-muted-foreground", children: byCode.label }),
36641
+ /* @__PURE__ */ jsxs("div", { className: "mt-0.5 flex gap-2", children: [
36642
+ /* @__PURE__ */ jsx(
36643
+ "input",
36644
+ {
36645
+ type: "text",
36646
+ value: code,
36647
+ autoFocus: true,
36648
+ autoComplete: "off",
36649
+ inputMode: "numeric",
36650
+ onChange: (e) => {
36651
+ setCode(e.target.value);
36652
+ setError(null);
36653
+ },
36654
+ onKeyDown: (e) => {
36655
+ if (e.key === "Enter") {
36656
+ e.preventDefault();
36657
+ void submit();
36658
+ }
36659
+ },
36660
+ placeholder: byCode.placeholder,
36661
+ "aria-invalid": !!error,
36662
+ className: "min-w-0 flex-1 rounded-input border border-input bg-card px-2.5 py-1.5 font-mono text-sm shadow-[inset_0_1px_0_rgb(0_0_0_/0.06)] focus:outline-none focus:ring-2 focus:ring-ring aria-[invalid=true]:border-destructive"
36663
+ }
36664
+ ),
36665
+ /* @__PURE__ */ jsx(
36666
+ "button",
36667
+ {
36668
+ type: "button",
36669
+ onClick: () => void submit(),
36670
+ disabled: busy || !code.trim(),
36671
+ "aria-label": byCode.label,
36672
+ className: "fz-tap inline-flex shrink-0 items-center rounded-button border border-input bg-card px-2.5 text-muted-foreground transition-colors hover:text-foreground disabled:opacity-50",
36673
+ children: busy ? /* @__PURE__ */ jsx(Loader2, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ jsx(Search, { className: "h-4 w-4" })
36674
+ }
36675
+ )
36676
+ ] }),
36677
+ error ? /* @__PURE__ */ jsx("p", { role: "alert", className: "mt-1 text-[11px] text-destructive", children: error }) : byCode.hint && /* @__PURE__ */ jsx("p", { className: "mt-1 text-[11px] text-muted-foreground", children: byCode.hint })
36678
+ ] });
36679
+ }
36415
36680
  function OrderFormAddItemStep({ itemTypes, onSelect, onCancel, label: label3 = "Que tipo de item?", cancelLabel = "Cancelar" }) {
36416
36681
  return /* @__PURE__ */ jsxs("div", { className: "animate-in fade-in-0 slide-in-from-top-1 border-b bg-primary/5 px-4 py-3", children: [
36417
36682
  /* @__PURE__ */ jsx("p", { className: "mb-2 text-xs font-medium text-muted-foreground", children: label3 }),