@oondemand/oon-core-front 0.3.33 → 0.3.35

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.d.ts CHANGED
@@ -603,11 +603,28 @@ type OonPipelineCreateDef = {
603
603
  defaultStage?: string;
604
604
  defaultValues?: Record<string, unknown>;
605
605
  };
606
+ type OonPipelineActionGroup = "approval" | "workStatus" | "custom";
607
+ type OonPipelineActionTone = "success" | "danger" | "warning" | "neutral" | "brand";
608
+ type OonPipelineOperationalKind = "approval" | "work-status" | "business";
609
+ /**
610
+ * Ações padrão da esteira. Quando omitido, aprovação/reprovação e status de
611
+ * trabalho ficam habilitados automaticamente quando os campos necessários
612
+ * existem na metadata do model.
613
+ */
614
+ type OonPipelineDefaultActionsConfig = {
615
+ approval?: boolean;
616
+ workStatus?: boolean;
617
+ workStatusField?: string;
618
+ excludedStages?: string[];
619
+ };
620
+ type OonPipelineDefaultActionsDef = false | OonPipelineDefaultActionsConfig;
606
621
  /**
607
622
  * Ações operacionais exibidas no cabeçalho do ticket.
608
623
  *
609
624
  * - setField/transition atualizam um campo do próprio ticket;
610
- * - formAction carrega dados opcionais e envia um formulário a um endpoint.
625
+ * - formAction carrega dados opcionais e envia um formulário a um endpoint;
626
+ * - group/tone organizam a ação visualmente sem acoplar regra de negócio à UI;
627
+ * - operationalKind cria um contrato estável para telemetria, SLA e produtividade.
611
628
  */
612
629
  type OonPipelineActionDef = Omit<OonRowActionDef, "type"> & {
613
630
  type: OonRowActionDef["type"] | "setField" | "transition" | "formAction";
@@ -618,6 +635,9 @@ type OonPipelineActionDef = Omit<OonRowActionDef, "type"> & {
618
635
  loadEndpoint?: string;
619
636
  fields?: OonFormFieldDef[];
620
637
  initialValues?: Record<string, unknown>;
638
+ group?: OonPipelineActionGroup;
639
+ tone?: OonPipelineActionTone;
640
+ operationalKind?: OonPipelineOperationalKind;
621
641
  };
622
642
  /**
623
643
  * Extensão pública da view de esteira com filtros, inclusão, lista, ações e
@@ -642,7 +662,9 @@ type OonPipelineViewDef = PipelineViewDef & {
642
662
  defaultView?: OonPipelineViewMode;
643
663
  /** Colunas e ações da visualização em lista. */
644
664
  list?: OonCollectionListConfig;
645
- /** Ações exibidas no cabeçalho do ticket aberto. */
665
+ /** Ações padrão do Core; use false para desabilitá-las nesta esteira. */
666
+ defaultActions?: OonPipelineDefaultActionsDef;
667
+ /** Ações adicionais ou overrides exibidos no cabeçalho do ticket aberto. */
646
668
  ticketActions?: OonPipelineActionDef[];
647
669
  };
648
670
 
@@ -712,6 +734,8 @@ interface CentralUiManifest {
712
734
  viewModes?: OonPipelineViewMode[];
713
735
  defaultView?: OonPipelineViewMode;
714
736
  list?: OonCollectionListConfig;
737
+ /** Habilitadas por padrão; use false para remover aprovação e status de trabalho. */
738
+ defaultActions?: OonPipelineDefaultActionsDef;
715
739
  ticketActions?: OonPipelineActionDef[];
716
740
  }>;
717
741
  documents?: Array<{
@@ -817,10 +841,11 @@ interface CorePipelineProps {
817
841
  viewModes?: OonPipelineViewMode[];
818
842
  defaultView?: OonPipelineViewMode;
819
843
  list?: OonCollectionListConfig;
844
+ defaultActions?: OonPipelineDefaultActionsDef;
820
845
  ticketActions?: OonPipelineActionDef[];
821
846
  }
822
847
  /** Esteira com quadro/lista, filtros, inclusão e ações operacionais no ticket. */
823
- declare function CorePipeline({ model, label, endpoint, stageField, stages, titleField, form, relations, ticketModal, filters, create, viewModes, defaultView, list, ticketActions, }: CorePipelineProps): react.JSX.Element;
848
+ declare function CorePipeline({ model, label, endpoint, stageField, stages, titleField, form, relations, ticketModal, filters, create, viewModes, defaultView, list, defaultActions, ticketActions, }: CorePipelineProps): react.JSX.Element;
824
849
 
825
850
  interface CoreIntegrationProps {
826
851
  label?: string;
@@ -1003,4 +1028,4 @@ declare function useCoreMetadata(): _tanstack_react_query.UseQueryResult<NoInfer
1003
1028
  /** Descrição de uma model específica — base do modo "dynamic". */
1004
1029
  declare function useModelSchema(name: string): _tanstack_react_query.UseQueryResult<NoInfer<ModelSummary>, Error>;
1005
1030
 
1006
- export { type CentralFormFieldDef, type CentralUiManifest, type CollectionMode, type CollectionViewDef, CoreActions, CoreAssistant, CoreCards, CoreCollection, CoreCurrency, CoreDashboard, CoreDataGrid, CoreDocument, CoreEmptyState, CoreField, CoreForm, CoreIntegration, CoreLoadingState, type CoreMetadata, CorePage, CorePageHeader, CorePipeline, CoreRelationField, CoreToolbar, type DashboardViewDef, type DashboardWidgetDef, type DocumentViewDef, type FieldKind, type FieldMeta, type ListParams, type ManifestRuntime, type ModelSummary, type OonActionDef, type OonApiConfig, type OonAppConfig, type OonAppKind, type OonAuthConfig, type OonAuthMode, type OonBlockDef, type OonColumnDef, type OonComponentRegistry, type OonCoreFrontConfig, type OonError, type OonFormFieldDef, type OonLayoutConfig, type OonMenuItem, type OonModule, type OonNavigationConfig, type OonNavigationMode, type OonPageDef, type OonPipelineTicketModalConfig, type OonPipelineViewDef, type OonRoute, type OonSchemaVersion, type OonSecurityConfig, type OonUiManifest, type OonUser, type OonViewDefinition, type PaginatedResult, type Pagination, PermissionGate, type PipelineViewDef, defineCollectionView, defineDashboard, defineDocumentView, defineOonModule, definePipelineView, manifestToConfig, oonCoreFront, start, startFromManifest, useCoreMetadata, useModelSchema, useOonApi, useOonAuth, useOonResource };
1031
+ export { type CentralFormFieldDef, type CentralUiManifest, type CollectionMode, type CollectionViewDef, CoreActions, CoreAssistant, CoreCards, CoreCollection, CoreCurrency, CoreDashboard, CoreDataGrid, CoreDocument, CoreEmptyState, CoreField, CoreForm, CoreIntegration, CoreLoadingState, type CoreMetadata, CorePage, CorePageHeader, CorePipeline, CoreRelationField, CoreToolbar, type DashboardViewDef, type DashboardWidgetDef, type DocumentViewDef, type FieldKind, type FieldMeta, type ListParams, type ManifestRuntime, type ModelSummary, type OonActionDef, type OonApiConfig, type OonAppConfig, type OonAppKind, type OonAuthConfig, type OonAuthMode, type OonBlockDef, type OonColumnDef, type OonComponentRegistry, type OonCoreFrontConfig, type OonError, type OonFormFieldDef, type OonLayoutConfig, type OonMenuItem, type OonModule, type OonNavigationConfig, type OonNavigationMode, type OonPageDef, type OonPipelineActionDef, type OonPipelineActionGroup, type OonPipelineActionTone, type OonPipelineDefaultActionsConfig, type OonPipelineDefaultActionsDef, type OonPipelineTicketModalConfig, type OonPipelineViewDef, type OonRoute, type OonSchemaVersion, type OonSecurityConfig, type OonUiManifest, type OonUser, type OonViewDefinition, type PaginatedResult, type Pagination, PermissionGate, type PipelineViewDef, defineCollectionView, defineDashboard, defineDocumentView, defineOonModule, definePipelineView, manifestToConfig, oonCoreFront, start, startFromManifest, useCoreMetadata, useModelSchema, useOonApi, useOonAuth, useOonResource };
package/dist/index.js CHANGED
@@ -88,11 +88,11 @@ function createRestClient(deps) {
88
88
  instance.interceptors.response.use(
89
89
  (res) => res,
90
90
  (error) => {
91
- const normalized = normalizeError(error);
91
+ const normalized2 = normalizeError(error);
92
92
  const requestPath = String(error.config?.url || "").split("?")[0].replace(/\/$/, "");
93
93
  const isLogin = requestPath.endsWith("/auth/autenticar");
94
- if (normalized.status === 401 && !isLogin) onUnauthorized?.();
95
- return Promise.reject(normalized);
94
+ if (normalized2.status === 401 && !isLogin) onUnauthorized?.();
95
+ return Promise.reject(normalized2);
96
96
  }
97
97
  );
98
98
  return instance;
@@ -778,8 +778,8 @@ function PlusIcon() {
778
778
  function renderCell(value, kind) {
779
779
  const formatted = formatCell(value, kind);
780
780
  if (kind === "enum") {
781
- const normalized = String(value ?? "").toLowerCase();
782
- const palette = /erro|cancel|inativo|recus|critica/.test(normalized) ? "red" : /pendente|aguardando|rascunho/.test(normalized) ? "orange" : /processando|conferencia|integrando|validando/.test(normalized) ? "blue" : /ativo|sucesso|conclu|aplicada|disponivel|aprov/.test(normalized) ? "green" : "gray";
781
+ const normalized2 = String(value ?? "").toLowerCase();
782
+ const palette = /erro|cancel|inativo|recus|critica/.test(normalized2) ? "red" : /pendente|aguardando|rascunho/.test(normalized2) ? "orange" : /processando|conferencia|integrando|validando/.test(normalized2) ? "blue" : /ativo|sucesso|conclu|aplicada|disponivel|aprov/.test(normalized2) ? "green" : "gray";
783
783
  return /* @__PURE__ */ jsx7(Badge, { colorPalette: palette, variant: "subtle", borderRadius: "full", px: "7px", py: "3px", fontSize: "9px", fontWeight: "600", textTransform: "capitalize", children: formatted });
784
784
  }
785
785
  if (kind === "boolean") {
@@ -1424,9 +1424,9 @@ function parseCurrencyInitial(value) {
1424
1424
  return Number.isFinite(parsed) ? String(parsed) : "";
1425
1425
  }
1426
1426
  function formatCurrency(value) {
1427
- const normalized = parseCurrencyInitial(value);
1428
- if (!normalized) return "";
1429
- const numeric = Number(normalized);
1427
+ const normalized2 = parseCurrencyInitial(value);
1428
+ if (!normalized2) return "";
1429
+ const numeric = Number(normalized2);
1430
1430
  if (!Number.isFinite(numeric)) return "";
1431
1431
  return new Intl.NumberFormat("pt-BR", {
1432
1432
  style: "currency",
@@ -1833,7 +1833,7 @@ function DynamicForm({ title, eyebrow = "Cadastro", fields, initialValues, submi
1833
1833
 
1834
1834
  // src/components/internal/CoreDetailModal.tsx
1835
1835
  import { useEffect as useEffect6, useMemo as useMemo7, useState as useState9 } from "react";
1836
- import { Box as Box8, Button as Button8, Flex as Flex7, Heading as Heading3, Text as Text10 } from "@chakra-ui/react";
1836
+ import { Box as Box8, Button as Button8, Flex as Flex7, Heading as Heading3, IconButton, Text as Text10 } from "@chakra-ui/react";
1837
1837
  import { useQuery as useQuery6, useQueryClient as useQueryClient2 } from "@tanstack/react-query";
1838
1838
 
1839
1839
  // src/components/internal/CoreTabbedDetail.tsx
@@ -1980,6 +1980,13 @@ function interpolateEndpoint(endpoint, row, parent) {
1980
1980
 
1981
1981
  // src/components/internal/CoreRowAction.tsx
1982
1982
  import { jsx as jsx12, jsxs as jsxs8 } from "react/jsx-runtime";
1983
+ var ACTION_TONES = {
1984
+ success: { bg: "#DCFCE7", color: "#166534", border: "#BBF7D0", hover: "#D1FAE5" },
1985
+ danger: { bg: "#FEE2E2", color: "#991B1B", border: "#FECACA", hover: "#FDE2E2" },
1986
+ warning: { bg: "#FEF3C7", color: "#92400E", border: "#FDE68A", hover: "#FDEDB3" },
1987
+ neutral: { bg: "#F1F5F9", color: "#334155", border: "#E2E8F0", hover: "#E8EEF5" },
1988
+ brand: { bg: "#E0F2FE", color: "#075985", border: "#BAE6FD", hover: "#D5EEFC" }
1989
+ };
1983
1990
  function messageOf2(error) {
1984
1991
  return error instanceof Error ? error.message : typeof error === "string" ? error : "N\xE3o foi poss\xEDvel executar a a\xE7\xE3o.";
1985
1992
  }
@@ -1990,6 +1997,25 @@ function unwrap2(value) {
1990
1997
  if (data && typeof data === "object") return data;
1991
1998
  return record;
1992
1999
  }
2000
+ function emitOperationalEvent(action, row, changes) {
2001
+ if (typeof window === "undefined" || typeof window.dispatchEvent !== "function") return;
2002
+ if (!action.operationalKind) return;
2003
+ const field = action.field;
2004
+ window.dispatchEvent(new CustomEvent("oon:pipeline-action", {
2005
+ detail: {
2006
+ actionId: action.id,
2007
+ label: action.label,
2008
+ group: action.group,
2009
+ operationalKind: action.operationalKind,
2010
+ recordId: row._id ?? row.id,
2011
+ field,
2012
+ previousValue: field ? row[field] : void 0,
2013
+ nextValue: field ? changes?.[field] ?? action.value : void 0,
2014
+ changes,
2015
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
2016
+ }
2017
+ }));
2018
+ }
1993
2019
  function CoreRowAction({ action, row, parent, onOpenDetail, onRefresh, onUpdate }) {
1994
2020
  const { http } = useOonApi();
1995
2021
  const [loading, setLoading] = useState6(false);
@@ -1997,6 +2023,9 @@ function CoreRowAction({ action, row, parent, onOpenDetail, onRefresh, onUpdate
1997
2023
  const [formOpen, setFormOpen] = useState6(false);
1998
2024
  const [formInitialValues, setFormInitialValues] = useState6({});
1999
2025
  const pipelineAction = action;
2026
+ const tone = pipelineAction.tone ? ACTION_TONES[pipelineAction.tone] : void 0;
2027
+ const isWorkStatus = pipelineAction.group === "workStatus";
2028
+ const isSelected = isWorkStatus && !!pipelineAction.field && row[pipelineAction.field] === pipelineAction.value;
2000
2029
  if (evaluateCondition(action.hiddenWhen, row)) return null;
2001
2030
  const disabled = evaluateCondition(action.disabledWhen, row) || loading;
2002
2031
  const confirmAction = () => action.confirm ? window.confirm(action.confirm.description ?? action.confirm.title ?? `Executar ${action.label}?`) : true;
@@ -2030,7 +2059,9 @@ function CoreRowAction({ action, row, parent, onOpenDetail, onRefresh, onUpdate
2030
2059
  if (!pipelineAction.field || !onUpdate || !confirmAction()) return;
2031
2060
  setLoading(true);
2032
2061
  try {
2033
- await onUpdate({ [pipelineAction.field]: pipelineAction.value });
2062
+ const changes = { [pipelineAction.field]: pipelineAction.value };
2063
+ await onUpdate(changes);
2064
+ emitOperationalEvent(pipelineAction, row, changes);
2034
2065
  onRefresh?.(pipelineAction.refresh);
2035
2066
  } catch (err) {
2036
2067
  setError(err);
@@ -2044,6 +2075,7 @@ function CoreRowAction({ action, row, parent, onOpenDetail, onRefresh, onUpdate
2044
2075
  try {
2045
2076
  const url = interpolateEndpoint(action.endpoint, row, parent);
2046
2077
  await http.request({ url, method: action.method ?? "POST" });
2078
+ emitOperationalEvent(pipelineAction, row);
2047
2079
  onRefresh?.(action.refresh);
2048
2080
  } catch (err) {
2049
2081
  setError(err);
@@ -2051,8 +2083,35 @@ function CoreRowAction({ action, row, parent, onOpenDetail, onRefresh, onUpdate
2051
2083
  setLoading(false);
2052
2084
  }
2053
2085
  };
2054
- return /* @__PURE__ */ jsxs8("span", { children: [
2055
- /* @__PURE__ */ jsx12(Button5, { type: "button", size: "xs", minH: "28px", px: "9px", borderRadius: "6px", fontSize: "10px", loading, disabled, onClick: run, children: action.label }),
2086
+ return /* @__PURE__ */ jsxs8("span", { style: { display: "inline-flex", flexDirection: "column" }, children: [
2087
+ /* @__PURE__ */ jsxs8(
2088
+ Button5,
2089
+ {
2090
+ type: "button",
2091
+ size: "xs",
2092
+ minH: isWorkStatus ? "32px" : "28px",
2093
+ px: isWorkStatus ? "12px" : "9px",
2094
+ borderRadius: isWorkStatus ? "full" : "6px",
2095
+ fontSize: "10px",
2096
+ fontWeight: "700",
2097
+ variant: tone ? "outline" : void 0,
2098
+ bg: isWorkStatus && !isSelected ? "white" : tone?.bg,
2099
+ color: tone?.color,
2100
+ borderColor: isSelected ? tone?.color : tone?.border,
2101
+ borderWidth: isSelected ? "2px" : "1px",
2102
+ boxShadow: isSelected ? `0 0 0 3px ${tone?.border ?? "#E2E8F0"}` : tone ? "none" : void 0,
2103
+ "aria-pressed": isWorkStatus ? isSelected : void 0,
2104
+ loading,
2105
+ disabled,
2106
+ onClick: run,
2107
+ _hover: tone ? { bg: tone.hover, borderColor: tone.border } : void 0,
2108
+ _disabled: tone ? { opacity: isSelected ? 1 : 0.55, cursor: "not-allowed" } : void 0,
2109
+ children: [
2110
+ isWorkStatus ? /* @__PURE__ */ jsx12("span", { "aria-hidden": "true", style: { width: 8, height: 8, display: "inline-block", marginRight: 7, borderRadius: "999px", background: tone?.color ?? "#64748B", boxShadow: isSelected ? "0 0 0 2px rgba(255,255,255,.9)" : "none" } }) : null,
2111
+ action.label
2112
+ ]
2113
+ }
2114
+ ),
2056
2115
  error ? /* @__PURE__ */ jsx12(Text7, { mt: "3px", color: "#B42318", fontSize: "9px", children: messageOf2(error) }) : null,
2057
2116
  formOpen && pipelineAction.type === "formAction" ? /* @__PURE__ */ jsx12(
2058
2117
  DynamicForm,
@@ -2070,6 +2129,7 @@ function CoreRowAction({ action, row, parent, onOpenDetail, onRefresh, onUpdate
2070
2129
  try {
2071
2130
  const url = interpolateEndpoint(pipelineAction.endpoint, row, parent);
2072
2131
  await http.request({ url, method: pipelineAction.method ?? "POST", data: values });
2132
+ emitOperationalEvent(pipelineAction, row, values);
2073
2133
  setFormOpen(false);
2074
2134
  onRefresh?.(pipelineAction.refresh);
2075
2135
  } catch (err) {
@@ -2488,7 +2548,9 @@ function InlineParentForm({
2488
2548
  groups,
2489
2549
  parent,
2490
2550
  resource,
2491
- onSaved
2551
+ onSaved,
2552
+ formId,
2553
+ externalSubmit = false
2492
2554
  }) {
2493
2555
  const fields = rawFields;
2494
2556
  const [values, setValues] = useState8({});
@@ -2552,6 +2614,7 @@ function InlineParentForm({
2552
2614
  return /* @__PURE__ */ jsx14(
2553
2615
  "form",
2554
2616
  {
2617
+ id: formId,
2555
2618
  noValidate: true,
2556
2619
  onSubmit: async (event) => {
2557
2620
  event.preventDefault();
@@ -2580,7 +2643,7 @@ function InlineParentForm({
2580
2643
  children: /* @__PURE__ */ jsxs10(Stack3, { gap: "16px", children: [
2581
2644
  error ? /* @__PURE__ */ jsx14(Box7, { role: "alert", p: "10px 12px", color: "#B42318", bg: "#FEF3F2", borderWidth: "1px", borderColor: "#FECDCA", borderRadius: "7px", fontSize: "11px", children: messageOf3(error) }) : null,
2582
2645
  renderFields(),
2583
- /* @__PURE__ */ jsx14(Flex6, { justify: "flex-end", children: /* @__PURE__ */ jsx14(Button7, { type: "submit", colorPalette: "brand", loading: saving, children: "Salvar dados principais" }) })
2646
+ !externalSubmit ? /* @__PURE__ */ jsx14(Flex6, { justify: "flex-end", children: /* @__PURE__ */ jsx14(Button7, { type: "submit", colorPalette: "brand", loading: saving, children: "Salvar dados principais" }) }) : null
2584
2647
  ] })
2585
2648
  }
2586
2649
  );
@@ -2595,7 +2658,9 @@ function CoreTabbedDetail({
2595
2658
  relations,
2596
2659
  refreshTokens,
2597
2660
  onRefresh,
2598
- onParentSaved
2661
+ onParentSaved,
2662
+ formId,
2663
+ externalSubmit
2599
2664
  }) {
2600
2665
  const active = tabs.find((tab) => tab.id === activeTab) ?? tabs[0];
2601
2666
  return /* @__PURE__ */ jsxs10(Stack3, { gap: 4, children: [
@@ -2621,7 +2686,9 @@ function CoreTabbedDetail({
2621
2686
  groups: active.groups,
2622
2687
  parent: parent ?? {},
2623
2688
  resource,
2624
- onSaved: onParentSaved
2689
+ onSaved: onParentSaved,
2690
+ formId,
2691
+ externalSubmit
2625
2692
  }
2626
2693
  ) : (active.type === "relatedGrid" || active.type === "readonlyGrid") && parent ? /* @__PURE__ */ jsx14(
2627
2694
  CoreRelatedGrid,
@@ -2650,7 +2717,8 @@ function CoreDetailModal({
2650
2717
  initialRecord,
2651
2718
  initialTab,
2652
2719
  relations,
2653
- actions,
2720
+ topActions,
2721
+ bottomActions,
2654
2722
  onClose,
2655
2723
  onSaved
2656
2724
  }) {
@@ -2695,18 +2763,38 @@ function CoreDetailModal({
2695
2763
  };
2696
2764
  const width = config.size === "full" ? "98vw" : config.size === "md" ? "760px" : config.size === "lg" ? "980px" : "1180px";
2697
2765
  const displayTitle = parent?.[config.titleField ?? ""] ? String(parent[config.titleField ?? ""]) : title;
2698
- return /* @__PURE__ */ jsx15(Flex7, { position: "fixed", inset: 0, zIndex: 1e3, align: "center", justify: "center", p: 4, bg: "rgba(15,23,42,.48)", children: /* @__PURE__ */ jsxs11(Box8, { role: "dialog", "aria-modal": "true", w: `min(${width}, 98vw)`, maxH: "94vh", overflow: "hidden", bg: "white", borderRadius: "14px", boxShadow: "0 24px 70px rgba(15,23,42,.24)", children: [
2699
- /* @__PURE__ */ jsxs11(Flex7, { minH: "72px", px: "20px", align: "center", justify: "space-between", gap: 4, borderBottomWidth: "1px", borderColor: "#E5E9ED", children: [
2700
- /* @__PURE__ */ jsxs11(Box8, { children: [
2766
+ const activeIsForm = (config.tabs.find((tab) => tab.id === activeTab) ?? config.tabs[0])?.type === "form";
2767
+ const formId = `oon-detail-form-${model.replace(/[^a-zA-Z0-9_-]/g, "-")}`;
2768
+ return /* @__PURE__ */ jsx15(Flex7, { position: "fixed", inset: 0, zIndex: 1e3, align: "center", justify: "center", p: 4, bg: "rgba(15,23,42,.48)", children: /* @__PURE__ */ jsxs11(Box8, { role: "dialog", "aria-modal": "true", display: "flex", flexDirection: "column", w: `min(${width}, 98vw)`, h: "94vh", maxH: "94vh", overflow: "hidden", bg: "white", borderRadius: "14px", boxShadow: "0 24px 70px rgba(15,23,42,.24)", children: [
2769
+ /* @__PURE__ */ jsxs11(Flex7, { minH: "72px", px: "20px", py: "10px", align: "center", justify: "space-between", gap: 4, borderBottomWidth: "1px", borderColor: "#E5E9ED", children: [
2770
+ /* @__PURE__ */ jsxs11(Box8, { minW: "160px", children: [
2701
2771
  /* @__PURE__ */ jsx15(Text10, { color: "#0474AF", fontSize: "10px", fontWeight: "700", textTransform: "uppercase", children: "Detalhe" }),
2702
2772
  /* @__PURE__ */ jsx15(Heading3, { size: "md", children: recordId ? displayTitle : `Novo ${title}` })
2703
2773
  ] }),
2704
- /* @__PURE__ */ jsxs11(Flex7, { align: "center", justify: "flex-end", gap: "6px", wrap: "wrap", children: [
2705
- recordId ? actions : null,
2706
- /* @__PURE__ */ jsx15(Button8, { onClick: onClose, children: "Fechar" })
2774
+ /* @__PURE__ */ jsxs11(Flex7, { flex: "1", align: "center", justify: "flex-end", gap: "8px", wrap: "wrap", children: [
2775
+ recordId ? topActions : null,
2776
+ /* @__PURE__ */ jsx15(
2777
+ IconButton,
2778
+ {
2779
+ "aria-label": "Fechar",
2780
+ title: "Fechar",
2781
+ size: "sm",
2782
+ minW: "32px",
2783
+ h: "32px",
2784
+ borderRadius: "full",
2785
+ variant: "ghost",
2786
+ color: "#475569",
2787
+ fontSize: "21px",
2788
+ fontWeight: "400",
2789
+ lineHeight: "1",
2790
+ onClick: onClose,
2791
+ _hover: { bg: "#F1F5F9", color: "#0F172A" },
2792
+ children: "\xD7"
2793
+ }
2794
+ )
2707
2795
  ] })
2708
2796
  ] }),
2709
- /* @__PURE__ */ jsx15(Box8, { maxH: "calc(94vh - 73px)", p: "20px", overflowY: "auto", children: schemaQuery.isLoading || parentQuery.isLoading ? /* @__PURE__ */ jsx15(Text10, { children: "Carregando..." }) : /* @__PURE__ */ jsx15(
2797
+ /* @__PURE__ */ jsx15(Box8, { flex: "1", minH: 0, p: "20px", overflowY: "auto", children: schemaQuery.isLoading || parentQuery.isLoading ? /* @__PURE__ */ jsx15(Text10, { children: "Carregando..." }) : /* @__PURE__ */ jsx15(
2710
2798
  CoreTabbedDetail,
2711
2799
  {
2712
2800
  tabs: config.tabs,
@@ -2718,13 +2806,35 @@ function CoreDetailModal({
2718
2806
  relations,
2719
2807
  refreshTokens,
2720
2808
  onRefresh: refresh,
2809
+ formId,
2810
+ externalSubmit: true,
2721
2811
  onParentSaved: (saved) => {
2722
2812
  setParent(saved);
2723
2813
  onSaved?.(saved);
2724
2814
  refresh(["parent"]);
2725
2815
  }
2726
2816
  }
2727
- ) })
2817
+ ) }),
2818
+ /* @__PURE__ */ jsxs11(
2819
+ Flex7,
2820
+ {
2821
+ flex: "0 0 auto",
2822
+ minH: "64px",
2823
+ px: "20px",
2824
+ py: "10px",
2825
+ align: "center",
2826
+ justify: "space-between",
2827
+ gap: "12px",
2828
+ bg: "white",
2829
+ borderTopWidth: "1px",
2830
+ borderColor: "#E5E9ED",
2831
+ boxShadow: "0 -8px 24px rgba(15,23,42,.06)",
2832
+ children: [
2833
+ /* @__PURE__ */ jsx15(Box8, { children: recordId ? bottomActions : null }),
2834
+ /* @__PURE__ */ jsx15(Button8, { type: "submit", form: formId, colorPalette: "brand", disabled: !activeIsForm || schemaQuery.isLoading || parentQuery.isLoading, children: "Salvar" })
2835
+ ]
2836
+ }
2837
+ )
2728
2838
  ] }) });
2729
2839
  }
2730
2840
 
@@ -2872,6 +2982,133 @@ function CoreDocument({ model, label, endpoint, columns, form }) {
2872
2982
  import { useState as useState11 } from "react";
2873
2983
  import { Badge as Badge4, Box as Box10, Button as Button9, Flex as Flex8, Heading as Heading5, Input as Input6, NativeSelect as NativeSelect5, Stack as Stack5, Text as Text12 } from "@chakra-ui/react";
2874
2984
  import { useMutation as useMutation3, useQuery as useQuery7, useQueryClient as useQueryClient4 } from "@tanstack/react-query";
2985
+
2986
+ // src/pipelineTicket.ts
2987
+ var DEFAULT_EXCLUDED_STAGES = [
2988
+ "cancelado",
2989
+ "cancelada",
2990
+ "cancelled",
2991
+ "rejeitado",
2992
+ "rejeitada",
2993
+ "rejected"
2994
+ ];
2995
+ function normalized(value) {
2996
+ return String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").trim().toLowerCase();
2997
+ }
2998
+ function slug(value) {
2999
+ return normalized(value).replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
3000
+ }
3001
+ function approvalTone(action) {
3002
+ const key = normalized(`${action.id ?? ""} ${action.label}`);
3003
+ if (/recusar|reprovar|reject/.test(key)) return "danger";
3004
+ if (/aprovar|approve/.test(key)) return "success";
3005
+ return void 0;
3006
+ }
3007
+ function workStatusPresentation(value) {
3008
+ const key = normalized(value);
3009
+ if (/aguardando.*inicio|nao.*iniciado|waiting/.test(key)) {
3010
+ return { order: 0, label: "Aguardando in\xEDcio", tone: "warning" };
3011
+ }
3012
+ if (/trabalhando|em.*andamento|working/.test(key)) {
3013
+ return { order: 1, label: "Trabalhando", tone: "success" };
3014
+ }
3015
+ if (/revisar|revisao|review/.test(key)) {
3016
+ return { order: 2, label: "Revis\xE3o", tone: "danger" };
3017
+ }
3018
+ return void 0;
3019
+ }
3020
+ function decoratePipelineAction(action, stageField, workStatusField) {
3021
+ const approval = action.type === "transition" && action.field === stageField && approvalTone(action);
3022
+ const workStatus = action.type === "setField" && action.field === workStatusField;
3023
+ const group = action.group ?? (approval ? "approval" : workStatus ? "workStatus" : "custom");
3024
+ const tone = action.tone ?? (approval || (workStatus ? workStatusPresentation(action.value)?.tone : void 0)) ?? (group === "custom" ? "neutral" : void 0);
3025
+ const operationalKind = action.operationalKind ?? (group === "approval" ? "approval" : group === "workStatus" ? "work-status" : "business");
3026
+ return { ...action, group, tone, operationalKind };
3027
+ }
3028
+ function resolvePipelineActions({
3029
+ stageField,
3030
+ stages,
3031
+ workStatusValues = [],
3032
+ ticketActions = [],
3033
+ defaultActions
3034
+ }) {
3035
+ const config = defaultActions === false ? void 0 : defaultActions ?? {};
3036
+ const workStatusField = config?.workStatusField ?? "statusTrabalho";
3037
+ const explicit = ticketActions.map((action) => decoratePipelineAction(action, stageField, workStatusField));
3038
+ if (defaultActions === false) return explicit;
3039
+ const resolved = [];
3040
+ const hasExplicitApproval = explicit.some((action) => action.group === "approval");
3041
+ const hasExplicitWorkStatus = explicit.some((action) => action.group === "workStatus");
3042
+ if (config?.approval !== false && !hasExplicitApproval) {
3043
+ const excluded = /* @__PURE__ */ new Set([
3044
+ ...DEFAULT_EXCLUDED_STAGES,
3045
+ ...(config?.excludedStages ?? []).map(normalized)
3046
+ ]);
3047
+ const approvalStages = stages.filter((stage) => !excluded.has(normalized(stage.id)));
3048
+ approvalStages.forEach((stage, index) => {
3049
+ const next = approvalStages[index + 1];
3050
+ if (next) {
3051
+ resolved.push({
3052
+ id: `core-aprovar-${slug(stage.id)}`,
3053
+ label: "Aprovar",
3054
+ type: "transition",
3055
+ field: stageField,
3056
+ value: next.id,
3057
+ hiddenWhen: { field: stageField, notEquals: stage.id },
3058
+ confirm: { description: `Aprovar este ticket e alterar a etapa para ${next.label}?` },
3059
+ refresh: ["all"],
3060
+ group: "approval",
3061
+ tone: "success",
3062
+ operationalKind: "approval"
3063
+ });
3064
+ }
3065
+ });
3066
+ approvalStages.forEach((stage, index) => {
3067
+ const previous = approvalStages[index - 1];
3068
+ if (previous) {
3069
+ resolved.push({
3070
+ id: `core-recusar-${slug(stage.id)}`,
3071
+ label: "Recusar",
3072
+ type: "transition",
3073
+ field: stageField,
3074
+ value: previous.id,
3075
+ hiddenWhen: { field: stageField, notEquals: stage.id },
3076
+ confirm: { description: `Recusar este ticket e retornar a etapa para ${previous.label}?` },
3077
+ refresh: ["all"],
3078
+ group: "approval",
3079
+ tone: "danger",
3080
+ operationalKind: "approval"
3081
+ });
3082
+ }
3083
+ });
3084
+ }
3085
+ if (config?.workStatus !== false && !hasExplicitWorkStatus && workStatusValues.length) {
3086
+ workStatusValues.map((value) => ({ value, presentation: workStatusPresentation(value) })).filter((item) => Boolean(item.presentation)).sort((a, b) => a.presentation.order - b.presentation.order).forEach(({ value, presentation }) => {
3087
+ resolved.push({
3088
+ id: `core-status-trabalho-${slug(value)}`,
3089
+ label: presentation.label,
3090
+ type: "setField",
3091
+ field: workStatusField,
3092
+ value,
3093
+ disabledWhen: { field: workStatusField, equals: value },
3094
+ refresh: ["all"],
3095
+ group: "workStatus",
3096
+ tone: presentation.tone,
3097
+ operationalKind: "work-status"
3098
+ });
3099
+ });
3100
+ }
3101
+ return [...resolved, ...explicit];
3102
+ }
3103
+ function groupPipelineActions(actions) {
3104
+ return {
3105
+ approval: actions.filter((action) => action.group === "approval"),
3106
+ workStatus: actions.filter((action) => action.group === "workStatus"),
3107
+ custom: actions.filter((action) => action.group === "custom")
3108
+ };
3109
+ }
3110
+
3111
+ // src/components/CorePipeline.tsx
2875
3112
  import { jsx as jsx18, jsxs as jsxs13 } from "react/jsx-runtime";
2876
3113
  function ticketId(row) {
2877
3114
  const id = row._id ?? row.id;
@@ -2887,6 +3124,13 @@ function mergeFormFields2(metadataFields, overrides) {
2887
3124
  });
2888
3125
  return merged;
2889
3126
  }
3127
+ function workStatusColor(value) {
3128
+ const normalized2 = String(value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
3129
+ if (/aguardando.*inicio|nao.*iniciado|waiting/.test(normalized2)) return "#D97706";
3130
+ if (/trabalhando|em.*andamento|working/.test(normalized2)) return "#16A34A";
3131
+ if (/revisar|revisao|review/.test(normalized2)) return "#DC2626";
3132
+ return "#94A3B8";
3133
+ }
2890
3134
  function PipelineFilterControl({
2891
3135
  filter,
2892
3136
  value,
@@ -2945,6 +3189,7 @@ function CorePipeline({
2945
3189
  viewModes = ["board"],
2946
3190
  defaultView,
2947
3191
  list,
3192
+ defaultActions,
2948
3193
  ticketActions = []
2949
3194
  }) {
2950
3195
  const schemaQuery = useModelSchema(model);
@@ -2993,6 +3238,17 @@ function CorePipeline({
2993
3238
  if (viewMode === "board" && listQuery.isError) return /* @__PURE__ */ jsx18(ErrorState, { error: listQuery.error });
2994
3239
  const stageFieldMeta = schema?.fields.find((field) => field.name === stageField);
2995
3240
  const resolvedStages = stages ?? (stageFieldMeta?.options ?? []).map((option) => ({ id: option, label: option }));
3241
+ const defaultActionsConfig = defaultActions === false ? void 0 : defaultActions;
3242
+ const workStatusField = defaultActionsConfig?.workStatusField ?? "statusTrabalho";
3243
+ const workStatusMeta = schema?.fields.find((field) => field.name === workStatusField);
3244
+ const resolvedTicketActions = resolvePipelineActions({
3245
+ stageField,
3246
+ stages: resolvedStages,
3247
+ workStatusValues: workStatusMeta?.options ?? [],
3248
+ ticketActions,
3249
+ defaultActions
3250
+ });
3251
+ const groupedTicketActions = groupPipelineActions(resolvedTicketActions);
2996
3252
  const resolvedTitleField = titleField ?? ticketModal?.titleField ?? schema?.fields.find((field) => field.kind === "string")?.name ?? schema?.searchable[0] ?? "_id";
2997
3253
  const detailFields = (schema?.fields ?? []).filter((field) => field.name !== stageField && field.name !== resolvedTitleField && field.kind !== "raw").slice(0, 3);
2998
3254
  const formFields = mergeFormFields2(formFieldsFromMeta(schema?.fields ?? []), form);
@@ -3014,7 +3270,7 @@ function CorePipeline({
3014
3270
  ...defaultStage ? { [stageField]: defaultStage } : {}
3015
3271
  });
3016
3272
  };
3017
- const modalActions = selectedTicket && !creating ? /* @__PURE__ */ jsx18(Flex8, { gap: "5px", align: "center", wrap: "wrap", children: ticketActions.map((action, index) => /* @__PURE__ */ jsx18(
3273
+ const renderModalAction = (action, index) => selectedTicket ? /* @__PURE__ */ jsx18(
3018
3274
  CoreRowAction,
3019
3275
  {
3020
3276
  action,
@@ -3032,7 +3288,12 @@ function CorePipeline({
3032
3288
  }
3033
3289
  },
3034
3290
  action.id ?? `${action.label}-${index}`
3035
- )) }) : null;
3291
+ ) : null;
3292
+ const modalTopActions = selectedTicket && !creating && groupedTicketActions.workStatus.length ? /* @__PURE__ */ jsx18(Flex8, { role: "group", "aria-label": "Status da atividade", gap: "6px", align: "center", wrap: "wrap", children: groupedTicketActions.workStatus.map(renderModalAction) }) : null;
3293
+ const modalBottomActions = selectedTicket && !creating ? /* @__PURE__ */ jsxs13(Flex8, { gap: "8px", align: "center", wrap: "wrap", children: [
3294
+ groupedTicketActions.approval.length ? /* @__PURE__ */ jsx18(Flex8, { role: "group", "aria-label": "Aprova\xE7\xE3o do ticket", gap: "6px", align: "center", wrap: "wrap", children: groupedTicketActions.approval.map(renderModalAction) }) : null,
3295
+ groupedTicketActions.custom.length ? /* @__PURE__ */ jsx18(Flex8, { role: "group", "aria-label": "A\xE7\xF5es do neg\xF3cio", gap: "6px", align: "center", wrap: "wrap", children: groupedTicketActions.custom.map(renderModalAction) }) : null
3296
+ ] }) : null;
3036
3297
  return /* @__PURE__ */ jsxs13(Stack5, { gap: 5, children: [
3037
3298
  /* @__PURE__ */ jsxs13(Flex8, { align: { base: "stretch", md: "flex-end" }, justify: "space-between", gap: 4, direction: { base: "column", md: "row" }, children: [
3038
3299
  /* @__PURE__ */ jsxs13(Box10, { children: [
@@ -3118,7 +3379,24 @@ function CorePipeline({
3118
3379
  },
3119
3380
  _hover: { borderColor: "brand.300", transform: "translateY(-1px)", boxShadow: "0 7px 18px rgba(7, 38, 46, 0.09)" },
3120
3381
  children: [
3121
- /* @__PURE__ */ jsx18(Text12, { fontSize: "13px", fontWeight: "700", color: "#24323A", mb: 2, lineClamp: 2, children: ticketTitle }),
3382
+ /* @__PURE__ */ jsxs13(Flex8, { align: "flex-start", justify: "space-between", gap: "8px", mb: 2, children: [
3383
+ /* @__PURE__ */ jsx18(Text12, { fontSize: "13px", fontWeight: "700", color: "#24323A", lineClamp: 2, children: ticketTitle }),
3384
+ /* @__PURE__ */ jsx18(
3385
+ Box10,
3386
+ {
3387
+ as: "span",
3388
+ title: `Status da atividade: ${String(row[workStatusField] ?? "N\xE3o informado")}`,
3389
+ "aria-label": `Status da atividade: ${String(row[workStatusField] ?? "N\xE3o informado")}`,
3390
+ flex: "0 0 auto",
3391
+ w: "10px",
3392
+ h: "10px",
3393
+ mt: "4px",
3394
+ borderRadius: "full",
3395
+ bg: workStatusColor(row[workStatusField]),
3396
+ boxShadow: "0 0 0 3px white, 0 0 0 4px #E2E8F0"
3397
+ }
3398
+ )
3399
+ ] }),
3122
3400
  /* @__PURE__ */ jsx18(Stack5, { gap: 1, children: detailFields.map((field) => /* @__PURE__ */ jsxs13(Flex8, { gap: 2, align: "baseline", justify: "space-between", children: [
3123
3401
  /* @__PURE__ */ jsx18(Text12, { fontSize: "10px", color: "gray.500", textTransform: "uppercase", letterSpacing: "0.03em", children: fieldLabel(field) }),
3124
3402
  /* @__PURE__ */ jsx18(Text12, { fontSize: "11px", color: "#46545C", fontWeight: "500", textAlign: "right", lineClamp: 1, children: formatCell(row[field.name], field.kind) })
@@ -3141,7 +3419,8 @@ function CorePipeline({
3141
3419
  initialRecord: selectedTicket,
3142
3420
  initialTab: creating ? create?.initialTab : void 0,
3143
3421
  relations,
3144
- actions: modalActions,
3422
+ topActions: modalTopActions,
3423
+ bottomActions: modalBottomActions,
3145
3424
  onClose: () => {
3146
3425
  setSelectedTicket(null);
3147
3426
  setCreating(false);
@@ -3254,10 +3533,10 @@ function registryKey(key) {
3254
3533
  }
3255
3534
  function resolveComponent(registry, key, bucket) {
3256
3535
  if (!key) return null;
3257
- const normalized = registryKey(key);
3536
+ const normalized2 = registryKey(key);
3258
3537
  const inferred = key.includes(".") ? PREFIX_BUCKETS[key.split(".")[0]] : void 0;
3259
3538
  const targetBucket = bucket ?? inferred ?? "pageComponents";
3260
- return registry?.[targetBucket]?.[normalized] ?? registry?.[targetBucket]?.[key] ?? null;
3539
+ return registry?.[targetBucket]?.[normalized2] ?? registry?.[targetBucket]?.[key] ?? null;
3261
3540
  }
3262
3541
  function MissingRenderer({ renderer, children }) {
3263
3542
  if (import.meta.env?.MODE === "production") return /* @__PURE__ */ jsx21(Fragment4, { children: children ?? null });
@@ -3371,7 +3650,7 @@ function CorePage2({ page, registry }) {
3371
3650
 
3372
3651
  // src/app/moduleRegistry.tsx
3373
3652
  import { jsx as jsx24 } from "react/jsx-runtime";
3374
- function slug(value) {
3653
+ function slug2(value) {
3375
3654
  return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
3376
3655
  }
3377
3656
  function requiredAccess2(source) {
@@ -3381,7 +3660,7 @@ function requiredAccess2(source) {
3381
3660
  function viewId(view) {
3382
3661
  if (view.id) return view.id;
3383
3662
  if (view.type === "dashboard") return "dashboard";
3384
- return slug(view.model);
3663
+ return slug2(view.model);
3385
3664
  }
3386
3665
  function viewPath(view) {
3387
3666
  if (view.path) return view.path;
@@ -3436,6 +3715,7 @@ function renderView(view, registry) {
3436
3715
  viewModes: v.viewModes,
3437
3716
  defaultView: v.defaultView,
3438
3717
  list: v.list,
3718
+ defaultActions: v.defaultActions,
3439
3719
  ticketActions: v.ticketActions
3440
3720
  }
3441
3721
  );
@@ -4574,6 +4854,7 @@ function manifestToConfig(manifest, runtime) {
4574
4854
  viewModes: p.viewModes,
4575
4855
  defaultView: p.defaultView,
4576
4856
  list: p.list,
4857
+ defaultActions: p.defaultActions,
4577
4858
  ticketActions: p.ticketActions
4578
4859
  };
4579
4860
  views.push(view);