@fayz-ai/plugin-forms 0.10.0 → 0.10.1

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.
@@ -0,0 +1,3 @@
1
+ import type { AnalyticsCardDef } from '@fayz-ai/core';
2
+ export declare function buildFormsAnalyticsCards(): AnalyticsCardDef[];
3
+ //# sourceMappingURL=cards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cards.d.ts","sourceRoot":"","sources":["../../src/analytics/cards.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AA2BrD,wBAAgB,wBAAwB,IAAI,gBAAgB,EAAE,CA2E7D"}
package/dist/config.d.ts CHANGED
@@ -9,6 +9,12 @@ export interface CustomFormsPluginOptions {
9
9
  labels?: Partial<CustomFormsLabels>;
10
10
  /** App-owned registries shown inside Forms & Documents settings. */
11
11
  settingsRegistries?: PluginRegistryDef[];
12
+ /**
13
+ * Pass `false` to leave form submissions out of /analytics. Contributed by
14
+ * default — a filled form has no other place in the product where it can be
15
+ * read in bulk.
16
+ */
17
+ analyticsCards?: false;
12
18
  }
13
19
  export interface CustomFormsLabels {
14
20
  pageTitle: string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAE3D,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,uBAAuB,CAAA;IACtC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACnC,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,CAAA;CACzC;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,iBAAiB,CAAA;CAC1B;AAYD,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,iBAAiB,CAInF"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC/E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAA;AAE3D,MAAM,WAAW,wBAAwB;IACvC,KAAK,CAAC,EAAE,WAAW,CAAA;IACnB,UAAU,CAAC,EAAE,UAAU,CAAA;IACvB,YAAY,CAAC,EAAE,uBAAuB,CAAA;IACtC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,GAAG,UAAU,CAAA;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,CAAA;IACnC,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,iBAAiB,EAAE,CAAA;IACxC;;;;OAIG;IACH,cAAc,CAAC,EAAE,KAAK,CAAA;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAA;IACjB,aAAa,EAAE,MAAM,CAAA;IACrB,gBAAgB,EAAE,MAAM,CAAA;IACxB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,WAAW,EAAE,MAAM,CAAA;IACnB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,iBAAiB,CAAA;CAC1B;AAYD,wBAAgB,aAAa,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,iBAAiB,CAInF"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAUnD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAA;AA2CxD,YAAY,EAAE,wBAAwB,EAAE,CAAA;AACxC,OAAO,EAAE,4BAA4B,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAEvH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,cAAc,CAmL1F"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAUnD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,UAAU,CAAA;AA4CxD,YAAY,EAAE,wBAAwB,EAAE,CAAA;AACxC,OAAO,EAAE,4BAA4B,EAAE,KAAK,kBAAkB,EAAE,KAAK,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAEvH,wBAAgB,uBAAuB,CAAC,OAAO,CAAC,EAAE,wBAAwB,GAAG,cAAc,CAwL1F"}
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { createOrUpdateFormTool, prefillFromPerson, FormRenderer, printDocument } from './chunk-RBLXIKWB.js';
2
2
  import React2, { useState, useCallback, useRef, useMemo, useEffect } from 'react';
3
- import { registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, getActiveTenantId, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
4
- import { PluginSettingsPanel, registerTimelineSource, useLimitGuard, invalidateLimit, askAI, PermissionGate, QuickActionsButton } from '@fayz-ai/admin';
3
+ import { tl, registerTranslations, createSafeDataProvider, getSupabaseClientOptional, useTranslation, getActiveTenantId, emitDomainEvent, useDataVersion } from '@fayz-ai/core';
4
+ import { PluginSettingsPanel, registerTimelineSource, useLimitGuard, invalidateLimit, askAI, PermissionGate, PageActions } from '@fayz-ai/admin';
5
5
  import { create } from 'zustand';
6
- import { FileText, Image, Paperclip, Pencil, Archive, Plus, Search, ArrowLeft, Printer, ChevronDown, Upload, Eye } from 'lucide-react';
7
- import { Badge, Button, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel } from '@fayz-ai/ui';
6
+ import { FileText, Image, Paperclip, Plus, WandSparkles, Pencil, Archive, Search, ArrowLeft, Printer, ChevronDown, Upload, Eye } from 'lucide-react';
7
+ import { Badge, RowActions, Button, PageHeaderActions, Card, DataTable, useSaveBar, Dropdown, DropdownTrigger, DropdownContent, DropdownSeparator, DropdownLabel } from '@fayz-ai/ui';
8
8
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
9
9
 
10
10
  // src/data/tables.ts
@@ -881,32 +881,40 @@ function TemplateListView({ store, config, onEdit, onNew }) {
881
881
  const loading = store((s) => s.templatesLoading);
882
882
  const usage = store((s) => s.templateUsage);
883
883
  const orgId = getFormsTenantId();
884
- const createActions = useMemo(() => [
885
- {
886
- id: "with-ai",
887
- label: t("assistant.askAI"),
888
- description: t("customForms.newWithAIHint"),
889
- icon: "WandSparkles",
890
- highlight: true,
891
- action: () => askAI({
892
- hint: t("customForms.askPlaceholder"),
893
- intro: {
894
- role: "setup",
895
- persona: t("customForms.askPersona"),
896
- pitch: t("customForms.askPitch"),
897
- description: t("customForms.askDescription"),
898
- suggestions: [0, 1, 2].map((i) => ({ label: t(`customForms.askSuggestion.${i}`) }))
899
- }
900
- })
901
- },
902
- {
903
- id: "by-hand",
904
- label: t("customForms.newByHand"),
905
- description: t("customForms.newByHandHint"),
906
- icon: "SlidersHorizontal",
907
- action: onNew
908
- }
909
- ], [t, onNew]);
884
+ const createActions = useMemo(() => {
885
+ const permission = { feature: "custom_forms", action: "create" };
886
+ return [
887
+ {
888
+ id: "new",
889
+ label: t("customForms.newTemplate"),
890
+ description: t("customForms.newByHandHint"),
891
+ icon: Plus,
892
+ intent: "primary",
893
+ creates: true,
894
+ onSelect: onNew,
895
+ permission
896
+ },
897
+ {
898
+ id: "ai",
899
+ label: t("assistant.askAI"),
900
+ description: t("customForms.newWithAIHint"),
901
+ icon: WandSparkles,
902
+ intent: "secondary",
903
+ creates: true,
904
+ onSelect: () => askAI({
905
+ hint: t("customForms.askPlaceholder"),
906
+ intro: {
907
+ role: "setup",
908
+ persona: t("customForms.askPersona"),
909
+ pitch: t("customForms.askPitch"),
910
+ description: t("customForms.askDescription"),
911
+ suggestions: [0, 1, 2].map((i) => ({ label: t(`customForms.askSuggestion.${i}`) }))
912
+ }
913
+ }),
914
+ permission
915
+ }
916
+ ];
917
+ }, [t, onNew]);
910
918
  const version = useDataVersion(`${FORM_TEMPLATE_EVENT}*`);
911
919
  useEffect(() => {
912
920
  if (!orgId) return;
@@ -969,32 +977,31 @@ function TemplateListView({ store, config, onEdit, onNew }) {
969
977
  {
970
978
  id: "actions",
971
979
  header: "",
972
- cell: ({ row }) => /* @__PURE__ */ jsxs("div", { className: "flex justify-end gap-1", children: [
973
- /* @__PURE__ */ jsx(
974
- "button",
975
- {
976
- onClick: (e) => {
977
- e.stopPropagation();
978
- onEdit(row.original);
979
- },
980
- className: "rounded-md p-1.5 hover:bg-muted",
981
- title: t("common.edit"),
982
- children: /* @__PURE__ */ jsx(Pencil, { className: "h-3.5 w-3.5 text-muted-foreground" })
983
- }
984
- ),
985
- /* @__PURE__ */ jsx(
986
- "button",
987
- {
988
- onClick: (e) => {
989
- e.stopPropagation();
990
- void handleArchive(row.original);
980
+ // RowActions is the one renderer for what can be done to a single row
981
+ // it owns the sizing, the accessible name and the rule that keeps a
982
+ // destructive action off the row itself.
983
+ cell: ({ row }) => /* @__PURE__ */ jsx(
984
+ RowActions,
985
+ {
986
+ actions: [
987
+ {
988
+ id: "edit",
989
+ label: t("common.edit"),
990
+ icon: Pencil,
991
+ onSelect: () => onEdit(row.original)
991
992
  },
992
- className: "rounded-md p-1.5 hover:bg-muted",
993
- title: t("customForms.archive"),
994
- children: /* @__PURE__ */ jsx(Archive, { className: "h-3.5 w-3.5 text-muted-foreground" })
995
- }
996
- )
997
- ] })
993
+ {
994
+ id: "archive",
995
+ label: t("customForms.archive"),
996
+ icon: Archive,
997
+ intent: "danger",
998
+ onSelect: () => {
999
+ void handleArchive(row.original);
1000
+ }
1001
+ }
1002
+ ]
1003
+ }
1004
+ )
998
1005
  }
999
1006
  ], [t, usage, onEdit, handleArchive]);
1000
1007
  const emptyState = /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center py-6 text-center", children: [
@@ -1007,14 +1014,7 @@ function TemplateListView({ store, config, onEdit, onNew }) {
1007
1014
  ] }) })
1008
1015
  ] });
1009
1016
  return /* @__PURE__ */ jsxs("div", { className: "space-y-4", children: [
1010
- /* @__PURE__ */ jsx(PermissionGate, { feature: "custom_forms", action: "create", children: /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(
1011
- QuickActionsButton,
1012
- {
1013
- label: t("customForms.newTemplate"),
1014
- variant: "default",
1015
- actions: createActions
1016
- }
1017
- ) }) }),
1017
+ /* @__PURE__ */ jsx(PageHeaderActions, { children: /* @__PURE__ */ jsx(PageActions, { actions: createActions }) }),
1018
1018
  /* @__PURE__ */ jsxs(Card, { className: "overflow-hidden", children: [
1019
1019
  /* @__PURE__ */ jsx("div", { className: "flex flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: /* @__PURE__ */ jsxs("div", { className: "relative min-w-[10rem] flex-1", children: [
1020
1020
  /* @__PURE__ */ jsx(Search, { className: "pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" }),
@@ -1841,6 +1841,89 @@ function registerFormsTimelineSource() {
1841
1841
  }
1842
1842
  });
1843
1843
  }
1844
+ var SUBMISSIONS = "plg_forms_rep_submissions";
1845
+ var CAT = tl("Forms", "Formul\xE1rios");
1846
+ var STATUS_LABELS = {
1847
+ draft: tl("Draft", "Rascunho"),
1848
+ completed: tl("Completed", "Conclu\xEDdo"),
1849
+ signed: tl("Signed", "Assinado"),
1850
+ archived: tl("Archived", "Arquivado")
1851
+ };
1852
+ function buildFormsAnalyticsCards() {
1853
+ const base = { domain: "custom_forms", compare: true, canonicalOwner: "public.documents" };
1854
+ return [
1855
+ {
1856
+ ...base,
1857
+ id: "forms.submissions",
1858
+ name: tl("Form submissions", "Formul\xE1rios preenchidos"),
1859
+ description: tl(
1860
+ "Forms filled in during the period, whoever filled them.",
1861
+ "Formul\xE1rios preenchidos no per\xEDodo, por quem quer que seja."
1862
+ ),
1863
+ icon: "FileText",
1864
+ category: CAT,
1865
+ grain: "submission",
1866
+ query: {
1867
+ source: { kind: "view", name: SUBMISSIONS },
1868
+ dimensions: [{ key: "created_at", label: tl("Date", "Data"), grain: "auto" }],
1869
+ measures: [{ key: "submissions", label: tl("Submissions", "Preenchimentos"), agg: "count", format: "number" }]
1870
+ },
1871
+ viz: { type: "kpi", spark: true },
1872
+ dashboard: { span: 1, order: 30 }
1873
+ },
1874
+ {
1875
+ ...base,
1876
+ id: "forms.by-template",
1877
+ name: tl("Submissions by form", "Preenchimentos por formul\xE1rio"),
1878
+ description: tl(
1879
+ "Which templates are actually being used \u2014 and which one nobody answers.",
1880
+ "Quais modelos est\xE3o realmente em uso \u2014 e qual ningu\xE9m responde."
1881
+ ),
1882
+ icon: "ClipboardList",
1883
+ category: CAT,
1884
+ grain: "submission",
1885
+ query: {
1886
+ source: { kind: "view", name: SUBMISSIONS },
1887
+ dimensions: [{ key: "template_name", label: tl("Form", "Formul\xE1rio") }],
1888
+ measures: [{ key: "submissions", label: tl("Submissions", "Preenchimentos"), agg: "count", format: "number" }],
1889
+ defaultSort: "submissions",
1890
+ defaultSortDir: "desc"
1891
+ },
1892
+ viz: { type: "breakdown", limit: 8 }
1893
+ },
1894
+ {
1895
+ // The list the product never had: one row per filled form, with the
1896
+ // person behind it. No measures, so it is a plain select — the rows ARE
1897
+ // the report, and the viewer's export button turns them into a file.
1898
+ ...base,
1899
+ id: "forms.submissions-list",
1900
+ name: tl("Submission list", "Lista de preenchimentos"),
1901
+ description: tl(
1902
+ "Every form filled in the period, with who filled it \u2014 ready to export.",
1903
+ "Todo formul\xE1rio preenchido no per\xEDodo, com quem preencheu \u2014 pronto para exportar."
1904
+ ),
1905
+ icon: "Table",
1906
+ category: CAT,
1907
+ grain: "submission",
1908
+ query: {
1909
+ source: { kind: "view", name: SUBMISSIONS },
1910
+ defaultSort: "created_at",
1911
+ defaultSortDir: "desc"
1912
+ },
1913
+ columns: [
1914
+ { key: "created_at", label: tl("Filled", "Preenchido em"), type: "date", sortable: true },
1915
+ { key: "template_name", label: tl("Form", "Formul\xE1rio"), type: "text" },
1916
+ { key: "person_name", label: tl("Person", "Pessoa"), type: "person", idKey: "person_id" },
1917
+ { key: "person_email", label: tl("E-mail", "E-mail"), type: "email" },
1918
+ { key: "person_phone", label: tl("Phone", "Telefone"), type: "text" },
1919
+ { key: "status", label: tl("Status", "Situa\xE7\xE3o"), type: "text", valueMap: STATUS_LABELS },
1920
+ { key: "template_category", label: tl("Category", "Categoria"), type: "text", visible: false },
1921
+ { key: "signed_at", label: tl("Signed at", "Assinado em"), type: "date", visible: false }
1922
+ ],
1923
+ viz: { type: "table" }
1924
+ }
1925
+ ];
1926
+ }
1844
1927
  function registerBuiltInProviders(formsProvider) {
1845
1928
  registerDocumentTypeProvider({
1846
1929
  id: "custom_forms:templates",
@@ -1949,6 +2032,10 @@ function createCustomFormsPlugin(options) {
1949
2032
  { key: "form_templates", label: "Form templates", table: "plg_forms_templates" },
1950
2033
  { key: "documents_month", label: "Documents / month", table: "documents", period: "month" }
1951
2034
  ],
2035
+ // O formulário preenchido vira número e vira LISTA em /analytics — até aqui
2036
+ // ele só existia dentro da ficha de uma pessoa, um de cada vez. Read model
2037
+ // na migration 006.
2038
+ analyticsCards: options?.analyticsCards === false ? [] : buildFormsAnalyticsCards(),
1952
2039
  navigation: [],
1953
2040
  routes: [],
1954
2041
  settings: [