@fayz-ai/plugin-shop 0.9.0 → 0.9.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.
Files changed (56) hide show
  1. package/README.md +4 -4
  2. package/dist/{ShopPage-JP27MZS2.js → ShopPage-PVHFKDF3.js} +7 -7
  3. package/dist/ShopPage-PVHFKDF3.js.map +1 -0
  4. package/dist/data/categoryDataProvider.d.ts +5 -0
  5. package/dist/data/categoryDataProvider.d.ts.map +1 -0
  6. package/dist/data/collectionDataProvider.d.ts +5 -0
  7. package/dist/data/collectionDataProvider.d.ts.map +1 -0
  8. package/dist/data/customerGroupDataProvider.d.ts +5 -0
  9. package/dist/data/customerGroupDataProvider.d.ts.map +1 -0
  10. package/dist/data/customerSegmentDataProvider.d.ts +16 -0
  11. package/dist/data/customerSegmentDataProvider.d.ts.map +1 -0
  12. package/dist/data/giftCardDataProvider.d.ts +5 -0
  13. package/dist/data/giftCardDataProvider.d.ts.map +1 -0
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1088 -86
  17. package/dist/index.js.map +1 -1
  18. package/dist/views/ChannelsPage.d.ts +1 -1
  19. package/dist/views/ChannelsPage.d.ts.map +1 -1
  20. package/dist/views/ContentPage.d.ts +7 -0
  21. package/dist/views/ContentPage.d.ts.map +1 -0
  22. package/dist/views/EmailTemplatePage.d.ts +9 -0
  23. package/dist/views/EmailTemplatePage.d.ts.map +1 -0
  24. package/dist/views/EmailsPage.d.ts +7 -0
  25. package/dist/views/EmailsPage.d.ts.map +1 -0
  26. package/dist/views/ShopPage.d.ts.map +1 -0
  27. package/dist/views/categoryEntity.d.ts +4 -0
  28. package/dist/views/categoryEntity.d.ts.map +1 -0
  29. package/dist/views/collectionEntity.d.ts +4 -0
  30. package/dist/views/collectionEntity.d.ts.map +1 -0
  31. package/dist/views/customerGroupEntity.d.ts +4 -0
  32. package/dist/views/customerGroupEntity.d.ts.map +1 -0
  33. package/dist/views/customerSegmentEntity.d.ts +13 -0
  34. package/dist/views/customerSegmentEntity.d.ts.map +1 -0
  35. package/dist/views/giftCardEntity.d.ts +4 -0
  36. package/dist/views/giftCardEntity.d.ts.map +1 -0
  37. package/package.json +6 -8
  38. package/dist/ShopPage-JP27MZS2.js.map +0 -1
  39. package/dist/ShopPage.d.ts.map +0 -1
  40. package/src/ShopPage.tsx +0 -425
  41. package/src/components/ShopSettings.tsx +0 -108
  42. package/src/data/customerDataProvider.ts +0 -31
  43. package/src/data/discountDataProvider.ts +0 -42
  44. package/src/data/productDataProvider.ts +0 -54
  45. package/src/data/shared.ts +0 -42
  46. package/src/data/shippingZoneDataProvider.ts +0 -49
  47. package/src/index.ts +0 -270
  48. package/src/views/ChannelsPage.tsx +0 -274
  49. package/src/views/OrderDetailView.tsx +0 -319
  50. package/src/views/ProductImagesTab.tsx +0 -183
  51. package/src/views/customerEntity.tsx +0 -110
  52. package/src/views/customerLookup.ts +0 -31
  53. package/src/views/discountEntity.tsx +0 -144
  54. package/src/views/productEntity.tsx +0 -229
  55. package/src/views/shippingZoneEntity.tsx +0 -123
  56. /package/dist/{ShopPage.d.ts → views/ShopPage.d.ts} +0 -0
package/dist/index.js CHANGED
@@ -1,9 +1,10 @@
1
1
  import React3, { useState, useRef, useEffect } from 'react';
2
- import { setShopTenantResolver, getShopProvider } from '@fayz-ai/shop';
3
- import { useOrganizationStore, registerEntityPath, createCrudPage, ClientOrdersTab, adminNavigateTo, createClientOrdersProvider, SettingsGroup, SelectRow, ToggleRow } from '@fayz-ai/saas';
2
+ import { registerMessageDefaults } from '@fayz-ai/core';
3
+ import { setShopTenantResolver, buildShopMessageDefaults, buildShopDomainEvents, getShopProvider } from '@fayz-ai/shop';
4
+ import { useOrganizationStore, registerEntityPath, createCrudPage, adminNavigateTo, ClientOrdersTab, createClientOrdersProvider, SettingsGroup, SelectRow, ToggleRow } from '@fayz-ai/admin';
5
+ import { Package, Eye, FileEdit, PackageX, UsersRound, Users, UserX, Filter, ShoppingBag, Repeat, CircleDollarSign, Tag, TicketCheck, Layers, CreditCard, Upload, ImageOff, Film, FileText, Trash2, Link2, MapPin, Music, Camera, MessageCircle, Globe, ExternalLink, Check, X, Plus } from 'lucide-react';
4
6
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
5
- import { Link2, MapPin, ShoppingBag, Music, Camera, MessageCircle, Globe, ExternalLink, Check, X, Trash2, Plus } from 'lucide-react';
6
- import { Button, Input } from '@fayz-ai/ui';
7
+ import { HeaderTabs, Button, Card, Hint, toast, Input } from '@fayz-ai/ui';
7
8
 
8
9
  // src/index.ts
9
10
  function ProductImagesTab({ item, getProvider }) {
@@ -786,6 +787,491 @@ function buildShopShippingZoneEntity(currency = "BRL") {
786
787
  facets: [{ field: "active", allLabel: "Todas" }]
787
788
  };
788
789
  }
790
+ function buildShopCategoryEntity() {
791
+ const fields = [
792
+ {
793
+ key: "name",
794
+ label: "Nome",
795
+ type: "text",
796
+ required: true,
797
+ group: "basic",
798
+ placeholder: "Camisetas",
799
+ searchable: true,
800
+ showInTable: true,
801
+ sortable: true
802
+ },
803
+ {
804
+ key: "slug",
805
+ label: "Identificador",
806
+ type: "text",
807
+ group: "basic",
808
+ placeholder: "camisetas",
809
+ hint: "Segmento de URL na vitrine. Deixe vazio para gerar a partir do nome.",
810
+ searchable: true,
811
+ showInTable: true,
812
+ sortable: true,
813
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-muted-foreground", children: String(v ?? "") })
814
+ },
815
+ {
816
+ key: "parentId",
817
+ label: "Categoria pai",
818
+ type: "text",
819
+ group: "basic",
820
+ placeholder: "sem pai \u2014 n\xEDvel principal",
821
+ hint: "Deixe vazio para uma categoria de primeiro n\xEDvel.",
822
+ showInTable: false
823
+ },
824
+ {
825
+ key: "description",
826
+ label: "Descri\xE7\xE3o",
827
+ type: "textarea",
828
+ group: "basic",
829
+ span: 2,
830
+ placeholder: "Descri\xE7\xE3o opcional",
831
+ showInTable: false
832
+ },
833
+ {
834
+ key: "sortOrder",
835
+ label: "Ordem",
836
+ type: "number",
837
+ group: "basic",
838
+ min: 0,
839
+ hint: "Menor aparece primeiro na vitrine.",
840
+ showInTable: true,
841
+ sortable: true,
842
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right text-muted-foreground", children: Number(v ?? 0) })
843
+ }
844
+ ];
845
+ return {
846
+ name: "Categoria",
847
+ namePlural: "Categorias",
848
+ icon: "FolderTree",
849
+ displayField: "name",
850
+ defaultSort: "sortOrder",
851
+ fields,
852
+ fieldGroups: [
853
+ { id: "basic", label: "Categoria", columns: 2 }
854
+ ]
855
+ };
856
+ }
857
+ function buildShopCollectionEntity() {
858
+ const fields = [
859
+ {
860
+ key: "title",
861
+ label: "T\xEDtulo",
862
+ type: "text",
863
+ required: true,
864
+ group: "basic",
865
+ placeholder: "Ver\xE3o 2026",
866
+ searchable: true,
867
+ showInTable: true,
868
+ sortable: true
869
+ },
870
+ {
871
+ key: "handle",
872
+ label: "Identificador",
873
+ type: "text",
874
+ group: "basic",
875
+ placeholder: "verao-2026",
876
+ hint: "Segmento de URL na vitrine. Deixe vazio para gerar a partir do t\xEDtulo.",
877
+ searchable: true,
878
+ showInTable: true,
879
+ sortable: true,
880
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "font-mono text-xs text-muted-foreground", children: String(v ?? "") })
881
+ },
882
+ {
883
+ key: "description",
884
+ label: "Descri\xE7\xE3o",
885
+ type: "textarea",
886
+ group: "basic",
887
+ span: 2,
888
+ placeholder: "Como esta cole\xE7\xE3o \xE9 apresentada na loja",
889
+ showInTable: false
890
+ },
891
+ {
892
+ key: "productIds",
893
+ label: "Produtos",
894
+ type: "text",
895
+ showInForm: false,
896
+ showInTable: true,
897
+ sortable: false,
898
+ renderCell: (v) => {
899
+ const count = Array.isArray(v) ? v.length : 0;
900
+ return /* @__PURE__ */ jsx("span", { className: "block text-right text-muted-foreground", children: count === 1 ? "1 produto" : `${count} produtos` });
901
+ }
902
+ },
903
+ {
904
+ key: "published",
905
+ label: "Publicada",
906
+ type: "boolean",
907
+ group: "basic",
908
+ hint: "Cole\xE7\xE3o despublicada some da vitrine, mas continua aqui.",
909
+ // Rotula os dois estados para as abas do cabeçalho: "true"/"false" cru
910
+ // seria o que o lojista leria no topo da própria lista.
911
+ options: [
912
+ { value: "true", label: "Publicadas" },
913
+ { value: "false", label: "Rascunhos" }
914
+ ],
915
+ showInTable: true,
916
+ sortable: true,
917
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${v ? "bg-emerald-100 text-emerald-800" : "bg-gray-100 text-gray-700"}`, children: v ? "Publicada" : "Rascunho" })
918
+ },
919
+ {
920
+ key: "sortOrder",
921
+ label: "Ordem",
922
+ type: "number",
923
+ group: "basic",
924
+ min: 0,
925
+ hint: "Menor aparece primeiro.",
926
+ showInTable: false
927
+ },
928
+ // — SEO —
929
+ {
930
+ key: "seoTitle",
931
+ label: "T\xEDtulo da p\xE1gina",
932
+ type: "text",
933
+ group: "seo",
934
+ span: 2,
935
+ placeholder: "T\xEDtulo exibido nos resultados de busca",
936
+ hint: "Vazio usa o t\xEDtulo da cole\xE7\xE3o.",
937
+ showInTable: false
938
+ },
939
+ {
940
+ key: "seoDescription",
941
+ label: "Meta descri\xE7\xE3o",
942
+ type: "textarea",
943
+ group: "seo",
944
+ span: 2,
945
+ placeholder: "Descreva esta cole\xE7\xE3o para os buscadores",
946
+ showInTable: false
947
+ }
948
+ ];
949
+ return {
950
+ name: "Cole\xE7\xE3o",
951
+ namePlural: "Cole\xE7\xF5es",
952
+ icon: "Layers",
953
+ displayField: "title",
954
+ defaultSort: "sortOrder",
955
+ fields,
956
+ fieldGroups: [
957
+ { id: "basic", label: "Cole\xE7\xE3o", columns: 2 },
958
+ { id: "seo", label: "Busca (SEO)", description: "Como esta cole\xE7\xE3o aparece nos resultados de busca.", columns: 2 }
959
+ ]
960
+ // Sem facet de "publicada": como booleano ele vira uma tira de abas
961
+ // "Todas · Sim · Não", que não diz de QUE é o sim — e uma loja com dez
962
+ // coleções não precisa filtrar por isso; a coluna já mostra o estado.
963
+ };
964
+ }
965
+ var STATUS_TONE3 = {
966
+ active: "bg-emerald-100 text-emerald-800",
967
+ redeemed: "bg-gray-100 text-gray-700",
968
+ expired: "bg-orange-100 text-orange-700",
969
+ disabled: "bg-red-100 text-red-700"
970
+ };
971
+ var STATUS_LABEL2 = {
972
+ active: "Ativo",
973
+ redeemed: "Resgatado",
974
+ expired: "Expirado",
975
+ disabled: "Desativado"
976
+ };
977
+ function buildShopGiftCardEntity(currencyCode = "BRL") {
978
+ const money3 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currencyCode }).format(Number(v) || 0);
979
+ const fields = [
980
+ {
981
+ key: "code",
982
+ label: "C\xF3digo",
983
+ type: "text",
984
+ group: "card",
985
+ span: 2,
986
+ placeholder: "gerado automaticamente",
987
+ hint: "O que o cliente digita no checkout. Deixe vazio para gerar um c\xF3digo.",
988
+ searchable: true,
989
+ showInTable: true,
990
+ sortable: true,
991
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "font-mono text-xs font-medium", children: String(v ?? "") })
992
+ },
993
+ {
994
+ key: "initialBalance",
995
+ label: "Valor",
996
+ type: "currency",
997
+ required: true,
998
+ group: "card",
999
+ min: 0,
1000
+ currency: currencyCode,
1001
+ currencySymbol: "R$",
1002
+ currencyLocale: "pt-BR",
1003
+ hint: "Valor de face do cart\xE3o. N\xE3o muda depois de emitido.",
1004
+ showInTable: true,
1005
+ sortable: true,
1006
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right text-muted-foreground", children: money3(v) })
1007
+ },
1008
+ {
1009
+ key: "balance",
1010
+ label: "Saldo",
1011
+ type: "currency",
1012
+ group: "card",
1013
+ min: 0,
1014
+ currency: currencyCode,
1015
+ currencySymbol: "R$",
1016
+ currencyLocale: "pt-BR",
1017
+ hint: "O que ainda pode ser gasto. Cai a cada resgate.",
1018
+ showInTable: true,
1019
+ sortable: true,
1020
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right font-medium", children: money3(v) })
1021
+ },
1022
+ {
1023
+ key: "status",
1024
+ label: "Status",
1025
+ type: "segmented",
1026
+ group: "card",
1027
+ span: 2,
1028
+ options: [
1029
+ { label: "Ativo", value: "active", description: "Aceito no checkout." },
1030
+ { label: "Desativado", value: "disabled", description: "Suspenso sem apagar o saldo." }
1031
+ ],
1032
+ showInTable: true,
1033
+ sortable: false,
1034
+ renderCell: (v) => {
1035
+ const s = String(v ?? "active");
1036
+ return /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-medium ${STATUS_TONE3[s] ?? STATUS_TONE3.active}`, children: STATUS_LABEL2[s] ?? s });
1037
+ }
1038
+ },
1039
+ // — Destinatário —
1040
+ {
1041
+ key: "customerEmail",
1042
+ label: "E-mail do destinat\xE1rio",
1043
+ type: "email",
1044
+ group: "recipient",
1045
+ placeholder: "opcional",
1046
+ searchable: true,
1047
+ showInTable: true,
1048
+ sortable: false,
1049
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: String(v ?? "\u2014") })
1050
+ },
1051
+ {
1052
+ key: "expiresAt",
1053
+ label: "Validade",
1054
+ type: "date",
1055
+ group: "recipient",
1056
+ hint: "Vazio = n\xE3o expira.",
1057
+ showInTable: false
1058
+ },
1059
+ {
1060
+ key: "note",
1061
+ label: "Observa\xE7\xE3o",
1062
+ type: "textarea",
1063
+ group: "recipient",
1064
+ span: 2,
1065
+ placeholder: "S\xF3 o admin v\xEA.",
1066
+ showInTable: false
1067
+ }
1068
+ ];
1069
+ return {
1070
+ name: "Vale-presente",
1071
+ namePlural: "Gift Cards",
1072
+ icon: "CreditCard",
1073
+ displayField: "code",
1074
+ defaultSort: "createdAt",
1075
+ defaultSortDir: "desc",
1076
+ fields,
1077
+ fieldGroups: [
1078
+ { id: "card", label: "Cart\xE3o", columns: 2 },
1079
+ { id: "recipient", label: "Destinat\xE1rio", columns: 2 }
1080
+ ],
1081
+ facets: [{ field: "status", allLabel: "Todos", display: "tabs" }]
1082
+ };
1083
+ }
1084
+ var SWATCH = {
1085
+ slate: "bg-slate-200 text-slate-800",
1086
+ emerald: "bg-emerald-100 text-emerald-800",
1087
+ amber: "bg-amber-100 text-amber-800",
1088
+ rose: "bg-rose-100 text-rose-800",
1089
+ violet: "bg-violet-100 text-violet-800"
1090
+ };
1091
+ function buildShopCustomerGroupEntity() {
1092
+ const fields = [
1093
+ {
1094
+ key: "name",
1095
+ label: "Nome",
1096
+ type: "text",
1097
+ required: true,
1098
+ group: "basic",
1099
+ placeholder: "Atacado",
1100
+ searchable: true,
1101
+ showInTable: true,
1102
+ sortable: true,
1103
+ renderCell: (_v, row) => /* @__PURE__ */ jsx("span", { className: `inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${SWATCH[row.color] ?? SWATCH.slate}`, children: row.name })
1104
+ },
1105
+ {
1106
+ key: "color",
1107
+ label: "Cor",
1108
+ type: "select",
1109
+ group: "basic",
1110
+ options: [
1111
+ { value: "slate", label: "Neutro" },
1112
+ { value: "emerald", label: "Verde" },
1113
+ { value: "amber", label: "\xC2mbar" },
1114
+ { value: "rose", label: "Rosa" },
1115
+ { value: "violet", label: "Violeta" }
1116
+ ],
1117
+ hint: "S\xF3 para reconhecer o grupo de relance na lista.",
1118
+ showInTable: false
1119
+ },
1120
+ {
1121
+ key: "description",
1122
+ label: "Descri\xE7\xE3o",
1123
+ type: "textarea",
1124
+ group: "basic",
1125
+ span: 2,
1126
+ placeholder: "Para que serve este grupo",
1127
+ searchable: true,
1128
+ showInTable: true,
1129
+ sortable: false,
1130
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: String(v ?? "\u2014") })
1131
+ },
1132
+ {
1133
+ key: "customerIds",
1134
+ label: "Clientes",
1135
+ type: "text",
1136
+ showInForm: false,
1137
+ showInTable: true,
1138
+ sortable: false,
1139
+ renderCell: (v) => {
1140
+ const count = Array.isArray(v) ? v.length : 0;
1141
+ return /* @__PURE__ */ jsx("span", { className: "block text-right text-muted-foreground", children: count === 1 ? "1 cliente" : `${count} clientes` });
1142
+ }
1143
+ }
1144
+ ];
1145
+ return {
1146
+ name: "Grupo",
1147
+ namePlural: "Grupos",
1148
+ icon: "UsersRound",
1149
+ displayField: "name",
1150
+ defaultSort: "name",
1151
+ fields,
1152
+ fieldGroups: [{ id: "basic", label: "Grupo", columns: 2 }]
1153
+ };
1154
+ }
1155
+ function describeSegment(s, money3) {
1156
+ const parts = [];
1157
+ if (s.minOrders != null && s.maxOrders != null) parts.push(`${s.minOrders}\u2013${s.maxOrders} pedidos`);
1158
+ else if (s.minOrders != null) parts.push(`${s.minOrders}+ pedidos`);
1159
+ else if (s.maxOrders != null) parts.push(`at\xE9 ${s.maxOrders} pedidos`);
1160
+ if (s.minSpent != null && s.maxSpent != null) parts.push(`gastou ${money3(s.minSpent)}\u2013${money3(s.maxSpent)}`);
1161
+ else if (s.minSpent != null) parts.push(`gastou ${money3(s.minSpent)}+`);
1162
+ else if (s.maxSpent != null) parts.push(`gastou at\xE9 ${money3(s.maxSpent)}`);
1163
+ if (s.joinedAfter) parts.push(`entrou ap\xF3s ${new Date(s.joinedAfter).toLocaleDateString("pt-BR")}`);
1164
+ if (s.joinedBefore) parts.push(`entrou antes de ${new Date(s.joinedBefore).toLocaleDateString("pt-BR")}`);
1165
+ return parts.length ? parts.join(" \xB7 ") : "Todos os clientes";
1166
+ }
1167
+ function buildShopCustomerSegmentEntity(currencyCode = "BRL") {
1168
+ const money3 = (v) => new Intl.NumberFormat("pt-BR", { style: "currency", currency: currencyCode }).format(v);
1169
+ const fields = [
1170
+ {
1171
+ key: "name",
1172
+ label: "Nome",
1173
+ type: "text",
1174
+ required: true,
1175
+ group: "basic",
1176
+ span: 2,
1177
+ placeholder: "Clientes fi\xE9is",
1178
+ searchable: true,
1179
+ showInTable: true,
1180
+ sortable: true
1181
+ },
1182
+ {
1183
+ key: "description",
1184
+ label: "Descri\xE7\xE3o",
1185
+ type: "textarea",
1186
+ group: "basic",
1187
+ span: 2,
1188
+ placeholder: "Para que serve este segmento",
1189
+ searchable: true,
1190
+ showInTable: false
1191
+ },
1192
+ // — Regra —
1193
+ {
1194
+ key: "minOrders",
1195
+ label: "M\xEDnimo de pedidos",
1196
+ type: "number",
1197
+ group: "rule",
1198
+ min: 0,
1199
+ placeholder: "sem m\xEDnimo",
1200
+ showInTable: false
1201
+ },
1202
+ {
1203
+ key: "maxOrders",
1204
+ label: "M\xE1ximo de pedidos",
1205
+ type: "number",
1206
+ group: "rule",
1207
+ min: 0,
1208
+ placeholder: "sem m\xE1ximo",
1209
+ showInTable: false
1210
+ },
1211
+ {
1212
+ key: "minSpent",
1213
+ label: "Gasto m\xEDnimo",
1214
+ type: "currency",
1215
+ group: "rule",
1216
+ min: 0,
1217
+ currency: currencyCode,
1218
+ currencySymbol: "R$",
1219
+ currencyLocale: "pt-BR",
1220
+ placeholder: "sem m\xEDnimo",
1221
+ showInTable: false
1222
+ },
1223
+ {
1224
+ key: "maxSpent",
1225
+ label: "Gasto m\xE1ximo",
1226
+ type: "currency",
1227
+ group: "rule",
1228
+ min: 0,
1229
+ currency: currencyCode,
1230
+ currencySymbol: "R$",
1231
+ currencyLocale: "pt-BR",
1232
+ placeholder: "sem m\xE1ximo",
1233
+ showInTable: false
1234
+ },
1235
+ { key: "joinedAfter", label: "Entrou depois de", type: "date", group: "rule", showInTable: false },
1236
+ { key: "joinedBefore", label: "Entrou antes de", type: "date", group: "rule", showInTable: false },
1237
+ // Coluna derivada: a regra escrita por extenso.
1238
+ {
1239
+ key: "minSpent_rule",
1240
+ label: "Regra",
1241
+ type: "text",
1242
+ showInForm: false,
1243
+ showInTable: true,
1244
+ sortable: false,
1245
+ renderCell: (_v, row) => /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: describeSegment(row, money3) })
1246
+ },
1247
+ {
1248
+ key: "matchCount",
1249
+ label: "Clientes",
1250
+ type: "number",
1251
+ showInForm: false,
1252
+ showInTable: true,
1253
+ sortable: true,
1254
+ renderCell: (v) => /* @__PURE__ */ jsx("span", { className: "block text-right font-medium", children: v == null ? "\u2014" : Number(v) })
1255
+ }
1256
+ ];
1257
+ return {
1258
+ name: "Segmento",
1259
+ namePlural: "Segmentos",
1260
+ icon: "Filter",
1261
+ displayField: "name",
1262
+ defaultSort: "name",
1263
+ fields,
1264
+ fieldGroups: [
1265
+ { id: "basic", label: "Segmento", columns: 2 },
1266
+ {
1267
+ id: "rule",
1268
+ label: "Regra",
1269
+ columns: 2,
1270
+ description: "Todos os crit\xE9rios preenchidos valem juntos. Deixe vazio o que n\xE3o quiser exigir."
1271
+ }
1272
+ ]
1273
+ };
1274
+ }
789
1275
 
790
1276
  // src/data/shared.ts
791
1277
  var resolve = (p) => typeof p === "function" ? p() : p;
@@ -923,6 +1409,211 @@ function createShopShippingZoneDataProvider(provider) {
923
1409
  }
924
1410
  };
925
1411
  }
1412
+
1413
+ // src/data/categoryDataProvider.ts
1414
+ function createShopCategoryDataProvider(provider) {
1415
+ return {
1416
+ async list(query) {
1417
+ const rows = await resolve(provider).listCategories();
1418
+ const term = query.search?.trim().toLowerCase();
1419
+ const filtered = term ? rows.filter((c) => c.name.toLowerCase().includes(term) || (c.slug ?? "").toLowerCase().includes(term)) : rows;
1420
+ return applyQuery(filtered, query);
1421
+ },
1422
+ async create(data) {
1423
+ const input = data;
1424
+ return resolve(provider).createCategory({
1425
+ name: String(input.name ?? ""),
1426
+ slug: input.slug?.trim() || void 0,
1427
+ description: input.description ?? void 0,
1428
+ // The form writes '' for "no parent"; the column is a nullable FK, and
1429
+ // '' is not a uuid — it must go as absent, not as empty text.
1430
+ parentId: input.parentId?.trim() || void 0,
1431
+ sortOrder: input.sortOrder ?? void 0
1432
+ });
1433
+ },
1434
+ async update(id, data) {
1435
+ const patch = {};
1436
+ if (data.name !== void 0) patch.name = data.name;
1437
+ if (data.slug !== void 0) patch.slug = data.slug?.trim() || void 0;
1438
+ if (data.description !== void 0) patch.description = data.description;
1439
+ if (data.parentId !== void 0) patch.parentId = data.parentId?.trim() || null;
1440
+ if (data.sortOrder !== void 0) patch.sortOrder = data.sortOrder;
1441
+ return resolve(provider).updateCategory(id, patch);
1442
+ },
1443
+ async remove(id) {
1444
+ await resolve(provider).deleteCategory(id);
1445
+ }
1446
+ };
1447
+ }
1448
+
1449
+ // src/data/collectionDataProvider.ts
1450
+ function requireCollections(p) {
1451
+ if (!p.listCollections) throw new Error("This store provider does not support collections");
1452
+ return p;
1453
+ }
1454
+ function createShopCollectionDataProvider(provider) {
1455
+ return {
1456
+ async list(query) {
1457
+ const p = resolve(provider);
1458
+ const rows = await p.listCollections?.() ?? [];
1459
+ const term = query.search?.trim().toLowerCase();
1460
+ const filtered = term ? rows.filter((c) => c.title.toLowerCase().includes(term) || (c.handle ?? "").toLowerCase().includes(term)) : rows;
1461
+ return applyQuery(filtered, query);
1462
+ },
1463
+ async create(data) {
1464
+ const p = requireCollections(resolve(provider));
1465
+ const input = data;
1466
+ return p.createCollection({
1467
+ title: String(input.title ?? ""),
1468
+ handle: input.handle?.trim() || void 0,
1469
+ description: input.description ?? null,
1470
+ seoTitle: input.seoTitle ?? null,
1471
+ seoDescription: input.seoDescription ?? null,
1472
+ published: input.published ?? true,
1473
+ sortOrder: input.sortOrder ?? void 0
1474
+ });
1475
+ },
1476
+ async update(id, data) {
1477
+ const p = requireCollections(resolve(provider));
1478
+ const { productIds: _members, ...patch } = data;
1479
+ return p.updateCollection(id, patch);
1480
+ },
1481
+ async remove(id) {
1482
+ const p = requireCollections(resolve(provider));
1483
+ await p.deleteCollection(id);
1484
+ }
1485
+ };
1486
+ }
1487
+
1488
+ // src/data/giftCardDataProvider.ts
1489
+ function requireGiftCards(p) {
1490
+ if (!p.listGiftCards) throw new Error("This store provider does not support gift cards");
1491
+ return p;
1492
+ }
1493
+ function createShopGiftCardDataProvider(provider) {
1494
+ return {
1495
+ async list(query) {
1496
+ const rows = await resolve(provider).listGiftCards?.() ?? [];
1497
+ const term = query.search?.trim().toLowerCase();
1498
+ const filtered = term ? rows.filter((g) => g.code.toLowerCase().includes(term) || (g.customerEmail ?? "").toLowerCase().includes(term)) : rows;
1499
+ return applyQuery(filtered, query, "status");
1500
+ },
1501
+ async create(data) {
1502
+ const p = requireGiftCards(resolve(provider));
1503
+ const input = data;
1504
+ return p.createGiftCard({
1505
+ code: input.code?.trim() || void 0,
1506
+ initialBalance: Number(input.initialBalance) || 0,
1507
+ status: input.status ?? "active",
1508
+ expiresAt: input.expiresAt || null,
1509
+ customerEmail: input.customerEmail || null,
1510
+ note: input.note || null
1511
+ });
1512
+ },
1513
+ async update(id, data) {
1514
+ const p = requireGiftCards(resolve(provider));
1515
+ const { initialBalance: _face, ...patch } = data;
1516
+ return p.updateGiftCard(id, patch);
1517
+ },
1518
+ async remove(id) {
1519
+ const p = requireGiftCards(resolve(provider));
1520
+ await p.deleteGiftCard(id);
1521
+ }
1522
+ };
1523
+ }
1524
+
1525
+ // src/data/customerGroupDataProvider.ts
1526
+ function requireGroups(p) {
1527
+ if (!p.listCustomerGroups) throw new Error("This store provider does not support customer groups");
1528
+ return p;
1529
+ }
1530
+ function createShopCustomerGroupDataProvider(provider) {
1531
+ return {
1532
+ async list(query) {
1533
+ const rows = await resolve(provider).listCustomerGroups?.() ?? [];
1534
+ const term = query.search?.trim().toLowerCase();
1535
+ const filtered = term ? rows.filter((g) => g.name.toLowerCase().includes(term) || (g.description ?? "").toLowerCase().includes(term)) : rows;
1536
+ return applyQuery(filtered, query);
1537
+ },
1538
+ async create(data) {
1539
+ const p = requireGroups(resolve(provider));
1540
+ const input = data;
1541
+ return p.createCustomerGroup({
1542
+ name: String(input.name ?? ""),
1543
+ description: input.description ?? null,
1544
+ color: input.color ?? null
1545
+ });
1546
+ },
1547
+ async update(id, data) {
1548
+ const p = requireGroups(resolve(provider));
1549
+ const { customerIds: _members, ...patch } = data;
1550
+ return p.updateCustomerGroup(id, patch);
1551
+ },
1552
+ async remove(id) {
1553
+ const p = requireGroups(resolve(provider));
1554
+ await p.deleteCustomerGroup(id);
1555
+ }
1556
+ };
1557
+ }
1558
+
1559
+ // src/data/customerSegmentDataProvider.ts
1560
+ function requireSegments(p) {
1561
+ if (!p.listCustomerSegments) throw new Error("This store provider does not support customer segments");
1562
+ return p;
1563
+ }
1564
+ function customerMatchesSegment(c, s) {
1565
+ const orders = c.ordersCount ?? 0;
1566
+ const spent = c.totalSpent ?? 0;
1567
+ if (s.minOrders != null && orders < s.minOrders) return false;
1568
+ if (s.maxOrders != null && orders > s.maxOrders) return false;
1569
+ if (s.minSpent != null && spent < s.minSpent) return false;
1570
+ if (s.maxSpent != null && spent > s.maxSpent) return false;
1571
+ const joined = c.createdAt?.slice(0, 10);
1572
+ if (s.joinedAfter && (!joined || joined < s.joinedAfter)) return false;
1573
+ if (s.joinedBefore && (!joined || joined > s.joinedBefore)) return false;
1574
+ return true;
1575
+ }
1576
+ function createShopCustomerSegmentDataProvider(provider) {
1577
+ return {
1578
+ async list(query) {
1579
+ const p = resolve(provider);
1580
+ const rows = await p.listCustomerSegments?.() ?? [];
1581
+ const customers = rows.length ? await p.listCustomers().catch(() => []) : [];
1582
+ const withCounts = rows.map((s) => ({
1583
+ ...s,
1584
+ matchCount: customers.filter((c) => customerMatchesSegment(c, s)).length
1585
+ }));
1586
+ const term = query.search?.trim().toLowerCase();
1587
+ const filtered = term ? withCounts.filter((s) => s.name.toLowerCase().includes(term) || (s.description ?? "").toLowerCase().includes(term)) : withCounts;
1588
+ return applyQuery(filtered, query);
1589
+ },
1590
+ async create(data) {
1591
+ const p = requireSegments(resolve(provider));
1592
+ const input = data;
1593
+ const created = await p.createCustomerSegment({
1594
+ name: String(input.name ?? ""),
1595
+ description: input.description ?? null,
1596
+ minOrders: input.minOrders ?? null,
1597
+ maxOrders: input.maxOrders ?? null,
1598
+ minSpent: input.minSpent ?? null,
1599
+ maxSpent: input.maxSpent ?? null,
1600
+ joinedAfter: input.joinedAfter || null,
1601
+ joinedBefore: input.joinedBefore || null
1602
+ });
1603
+ return { ...created, matchCount: 0 };
1604
+ },
1605
+ async update(id, data) {
1606
+ const p = requireSegments(resolve(provider));
1607
+ const { matchCount: _count, ...patch } = data;
1608
+ const updated = await p.updateCustomerSegment(id, patch);
1609
+ return { ...updated, matchCount: 0 };
1610
+ },
1611
+ async remove(id) {
1612
+ const p = requireSegments(resolve(provider));
1613
+ await p.deleteCustomerSegment(id);
1614
+ }
1615
+ };
1616
+ }
926
1617
  var CURRENCIES = [
927
1618
  { value: "BRL", label: "Real (R$)" },
928
1619
  { value: "USD", label: "US Dollar ($)" },
@@ -1217,13 +1908,133 @@ function AddChannelForm({ kind, onKind, onCancel, onAdd }) {
1217
1908
  ] })
1218
1909
  ] });
1219
1910
  }
1911
+ var TABS = [
1912
+ { value: "all", label: "Todos" },
1913
+ { value: "image", label: "Imagens" },
1914
+ { value: "video", label: "V\xEDdeos" }
1915
+ ];
1916
+ function resolve3(resolver) {
1917
+ return typeof resolver === "function" ? resolver() : resolver ?? getShopProvider();
1918
+ }
1919
+ function formatSize(bytes) {
1920
+ if (!bytes) return "";
1921
+ const units = ["B", "KB", "MB", "GB"];
1922
+ const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
1923
+ return `${(bytes / 1024 ** i).toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
1924
+ }
1925
+ function ContentPage({ resolveProvider }) {
1926
+ const provider = React3.useMemo(() => resolve3(resolveProvider), [resolveProvider]);
1927
+ const supported = typeof provider.listMediaFiles === "function";
1928
+ const [files, setFiles] = React3.useState(null);
1929
+ const [tab, setTab] = React3.useState("all");
1930
+ const [uploading, setUploading] = React3.useState(false);
1931
+ const inputRef = React3.useRef(null);
1932
+ const reload = React3.useCallback(async () => {
1933
+ if (!provider.listMediaFiles) {
1934
+ setFiles([]);
1935
+ return;
1936
+ }
1937
+ try {
1938
+ setFiles(await provider.listMediaFiles());
1939
+ } catch (err) {
1940
+ console.warn("[content] failed to list media", err);
1941
+ setFiles([]);
1942
+ }
1943
+ }, [provider]);
1944
+ React3.useEffect(() => {
1945
+ void reload();
1946
+ }, [reload]);
1947
+ const handleUpload = async (picked) => {
1948
+ if (!picked?.length || !provider.uploadMediaFile) return;
1949
+ setUploading(true);
1950
+ try {
1951
+ for (const file of Array.from(picked)) {
1952
+ await provider.uploadMediaFile(file);
1953
+ }
1954
+ toast.success(picked.length === 1 ? "Arquivo enviado" : `${picked.length} arquivos enviados`);
1955
+ await reload();
1956
+ } catch (err) {
1957
+ console.warn("[content] upload failed", err);
1958
+ toast.error("N\xE3o foi poss\xEDvel enviar o arquivo");
1959
+ } finally {
1960
+ setUploading(false);
1961
+ if (inputRef.current) inputRef.current.value = "";
1962
+ }
1963
+ };
1964
+ const handleDelete = async (file) => {
1965
+ if (!provider.deleteMediaFile) return;
1966
+ try {
1967
+ await provider.deleteMediaFile(file.id);
1968
+ setFiles((prev) => prev?.filter((f) => f.id !== file.id) ?? null);
1969
+ } catch (err) {
1970
+ console.warn("[content] delete failed", err);
1971
+ toast.error("N\xE3o foi poss\xEDvel excluir o arquivo");
1972
+ }
1973
+ };
1974
+ const visible = (files ?? []).filter((f) => tab === "all" || f.kind === tab);
1975
+ const imageCount = (files ?? []).filter((f) => f.kind === "image").length;
1976
+ const caption = files === null ? "" : `${files.length} ${files.length === 1 ? "arquivo" : "arquivos"} \xB7 ${imageCount} ${imageCount === 1 ? "imagem" : "imagens"}`;
1977
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6", children: [
1978
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-wrap items-center justify-between gap-3", children: [
1979
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
1980
+ /* @__PURE__ */ jsx(HeaderTabs, { tabs: TABS, value: tab, onChange: setTab, "aria-label": "Arquivos" }),
1981
+ caption && /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: caption })
1982
+ ] }),
1983
+ supported && provider.uploadMediaFile && /* @__PURE__ */ jsxs(Fragment, { children: [
1984
+ /* @__PURE__ */ jsx(
1985
+ "input",
1986
+ {
1987
+ ref: inputRef,
1988
+ type: "file",
1989
+ multiple: true,
1990
+ className: "hidden",
1991
+ onChange: (e) => void handleUpload(e.target.files)
1992
+ }
1993
+ ),
1994
+ /* @__PURE__ */ jsxs(Button, { onClick: () => inputRef.current?.click(), disabled: uploading, children: [
1995
+ /* @__PURE__ */ jsx(Upload, { className: "mr-2 h-4 w-4" }),
1996
+ uploading ? "Enviando\u2026" : "Enviar arquivos"
1997
+ ] })
1998
+ ] })
1999
+ ] }),
2000
+ !supported ? /* @__PURE__ */ jsxs(Card, { className: "p-12 text-center", children: [
2001
+ /* @__PURE__ */ jsx(ImageOff, { className: "mx-auto h-8 w-8 text-muted-foreground" }),
2002
+ /* @__PURE__ */ jsx("p", { className: "mt-3 font-medium", children: "Armazenamento n\xE3o configurado" }),
2003
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "A biblioteca de arquivos precisa do storage da loja, que n\xE3o est\xE1 dispon\xEDvel neste modo." })
2004
+ ] }) : files === null ? /* @__PURE__ */ jsx("div", { className: "grid gap-4 sm:grid-cols-3 lg:grid-cols-5", children: Array.from({ length: 5 }).map((_, i) => /* @__PURE__ */ jsxs(Card, { className: "p-3", children: [
2005
+ /* @__PURE__ */ jsx("div", { className: "aspect-square animate-pulse rounded-md bg-muted" }),
2006
+ /* @__PURE__ */ jsx("div", { className: "mt-2 h-3.5 w-2/3 animate-pulse rounded bg-muted" })
2007
+ ] }, i)) }) : visible.length === 0 ? /* @__PURE__ */ jsxs(Card, { className: "p-12 text-center", children: [
2008
+ /* @__PURE__ */ jsx(ImageOff, { className: "mx-auto h-8 w-8 text-muted-foreground" }),
2009
+ /* @__PURE__ */ jsx("p", { className: "mt-3 font-medium", children: "Nenhum arquivo ainda" }),
2010
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: "As fotos dos seus produtos e o que voc\xEA enviar aqui aparecem nesta biblioteca." })
2011
+ ] }) : /* @__PURE__ */ jsx("div", { className: "grid gap-4 sm:grid-cols-3 lg:grid-cols-5", children: visible.map((file) => /* @__PURE__ */ jsxs(Card, { className: "group overflow-hidden p-3", children: [
2012
+ /* @__PURE__ */ jsxs("div", { className: "relative flex aspect-square items-center justify-center overflow-hidden rounded-md bg-muted", children: [
2013
+ file.kind === "image" ? /* @__PURE__ */ jsx("img", { src: file.url, alt: file.name, loading: "lazy", className: "h-full w-full object-contain" }) : file.kind === "video" ? /* @__PURE__ */ jsx(Film, { className: "h-8 w-8 text-muted-foreground" }) : /* @__PURE__ */ jsx(FileText, { className: "h-8 w-8 text-muted-foreground" }),
2014
+ provider.deleteMediaFile && /* @__PURE__ */ jsx(Hint, { label: "Excluir", children: /* @__PURE__ */ jsx(
2015
+ "button",
2016
+ {
2017
+ type: "button",
2018
+ "aria-label": "Excluir",
2019
+ onClick: () => void handleDelete(file),
2020
+ className: "absolute right-1.5 top-1.5 rounded-md bg-card/90 p-1.5 text-muted-foreground opacity-0 shadow-button transition-opacity hover:text-destructive group-hover:opacity-100",
2021
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
2022
+ }
2023
+ ) })
2024
+ ] }),
2025
+ /* @__PURE__ */ jsx("p", { className: "mt-2 truncate text-[13px] font-medium", title: file.name, children: file.name }),
2026
+ /* @__PURE__ */ jsx("p", { className: "text-[11px] text-muted-foreground", children: [file.extension, formatSize(file.size)].filter(Boolean).join(" \xB7 ") })
2027
+ ] }, file.id)) })
2028
+ ] });
2029
+ }
1220
2030
 
1221
2031
  // src/index.ts
1222
- var ShopPage = React3.lazy(() => import('./ShopPage-JP27MZS2.js').then((m) => ({ default: m.ShopPage })));
2032
+ var ShopPage = React3.lazy(() => import('./ShopPage-PVHFKDF3.js').then((m) => ({ default: m.ShopPage })));
1223
2033
  function createShopPlugin(options) {
1224
2034
  const curr = { code: "BRL", locale: "pt-BR", symbol: "R$", ...options?.currency };
1225
2035
  setShopTenantResolver(() => useOrganizationStore.getState().currentOrg?.id);
1226
- registerEntityPath("shop_customer", "/customers");
2036
+ registerMessageDefaults(buildShopMessageDefaults());
2037
+ registerEntityPath("shop_customer", "/orders/customers");
1227
2038
  const pageFor = (section) => {
1228
2039
  const Wrapped = () => React3.createElement(
1229
2040
  React3.Suspense,
@@ -1233,43 +2044,279 @@ function createShopPlugin(options) {
1233
2044
  { provider: options?.provider, section }
1234
2045
  )
1235
2046
  );
1236
- Wrapped.displayName = section ? `ShopPage:${section}` : "ShopPageWrapper";
2047
+ Wrapped.displayName = `ShopPage:${section}` ;
1237
2048
  return Wrapped;
1238
2049
  };
1239
2050
  const resolveProvider = () => typeof options?.provider === "function" ? options.provider() : options?.provider ?? getShopProvider();
2051
+ const label = (key, fallback) => options?.sectionLabels?.[key] ?? fallback;
1240
2052
  const ProductsCrudPage = createCrudPage(buildShopProductEntity(curr.code, resolveProvider), {
1241
2053
  dataProvider: createShopProductDataProvider(resolveProvider),
1242
- feature: "shop"
2054
+ feature: "shop",
2055
+ stats: (items, total) => [
2056
+ { id: "total", label: "Produtos", value: total, icon: Package },
2057
+ { id: "active", label: "Ativos", value: items?.filter((p) => p.status === "active").length ?? 0, icon: Eye },
2058
+ { id: "draft", label: "Rascunhos", value: items?.filter((p) => p.status === "draft").length ?? 0, icon: FileEdit },
2059
+ { id: "out-of-stock", label: "Sem estoque", value: items?.filter((p) => (p.inventoryCount ?? 0) <= 0).length ?? 0, icon: PackageX }
2060
+ ]
2061
+ });
2062
+ const customerTabs = () => [
2063
+ {
2064
+ id: "customers",
2065
+ label: label("customers", "Clientes"),
2066
+ icon: Users,
2067
+ onSelect: () => adminNavigateTo("/orders/customers")
2068
+ },
2069
+ {
2070
+ id: "groups",
2071
+ label: label("customerGroups", "Grupos"),
2072
+ icon: UsersRound,
2073
+ onSelect: () => adminNavigateTo("/orders/customers/groups")
2074
+ },
2075
+ {
2076
+ id: "segments",
2077
+ label: label("customerSegments", "Segmentos"),
2078
+ icon: Filter,
2079
+ onSelect: () => adminNavigateTo("/orders/customers/segments")
2080
+ }
2081
+ ];
2082
+ const CustomerGroupsCrudPage = createCrudPage(buildShopCustomerGroupEntity(), {
2083
+ dataProvider: createShopCustomerGroupDataProvider(resolveProvider),
2084
+ feature: "shop",
2085
+ navTabs: customerTabs,
2086
+ activeNavTab: "groups",
2087
+ stats: (items, total) => [
2088
+ { id: "total", label: "Grupos", value: total, icon: UsersRound },
2089
+ {
2090
+ id: "members",
2091
+ label: "Clientes agrupados",
2092
+ icon: Users,
2093
+ // Distinct people, not the sum: someone in two groups is still one
2094
+ // customer, and a sum would report more members than the store has.
2095
+ value: new Set((items ?? []).flatMap((g) => g.customerIds ?? [])).size
2096
+ },
2097
+ {
2098
+ id: "empty",
2099
+ label: "Grupos vazios",
2100
+ icon: UserX,
2101
+ value: items?.filter((g) => (g.customerIds?.length ?? 0) === 0).length ?? 0
2102
+ },
2103
+ { id: "segments", label: "Segmentos", pending: "Ver aba", icon: Filter }
2104
+ ]
2105
+ });
2106
+ const CustomerSegmentsCrudPage = createCrudPage(buildShopCustomerSegmentEntity(curr.code), {
2107
+ dataProvider: createShopCustomerSegmentDataProvider(resolveProvider),
2108
+ feature: "shop",
2109
+ navTabs: customerTabs,
2110
+ activeNavTab: "segments",
2111
+ stats: (items, total) => [
2112
+ { id: "total", label: "Segmentos", value: total, icon: Filter },
2113
+ {
2114
+ id: "matched",
2115
+ label: "Maior segmento",
2116
+ icon: Users,
2117
+ value: items?.length ? Math.max(...items.map((s) => s.matchCount ?? 0)) : 0
2118
+ },
2119
+ {
2120
+ id: "empty",
2121
+ label: "Sem ningu\xE9m",
2122
+ icon: UserX,
2123
+ value: items?.filter((s) => (s.matchCount ?? 0) === 0).length ?? 0
2124
+ },
2125
+ { id: "groups", label: "Grupos", pending: "Ver aba", icon: UsersRound }
2126
+ ]
1243
2127
  });
1244
2128
  const CustomersCrudPage = createCrudPage(buildShopCustomerEntity(), {
1245
2129
  dataProvider: createShopCustomerDataProvider(resolveProvider),
1246
- feature: "shop"
2130
+ feature: "shop",
2131
+ navTabs: customerTabs,
2132
+ activeNavTab: "customers",
2133
+ stats: (items, total) => [
2134
+ { id: "total", label: "Total de clientes", value: total, icon: Users },
2135
+ { id: "with-orders", label: "Com pedidos", value: items?.filter((c) => (c.ordersCount ?? 0) > 0).length ?? 0, icon: ShoppingBag },
2136
+ // Não computados ainda: um zero confiante aqui seria lido como medição.
2137
+ { id: "repeat", label: "Taxa de recompra", pending: "Em breve", icon: Repeat },
2138
+ { id: "aov", label: "Ticket m\xE9dio", pending: "Em breve", icon: CircleDollarSign }
2139
+ ]
1247
2140
  });
1248
2141
  const DiscountsCrudPage = createCrudPage(buildShopDiscountEntity(), {
1249
2142
  dataProvider: createShopDiscountDataProvider(resolveProvider),
1250
- feature: "shop"
2143
+ feature: "shop",
2144
+ stats: (items, total) => [
2145
+ { id: "total", label: "Total de cupons", value: total, icon: Tag },
2146
+ { id: "active", label: "Ativos agora", value: items?.filter((d) => d.status === "active").length ?? 0, icon: Eye },
2147
+ { id: "uses", label: "Usos", value: items?.reduce((sum, d) => sum + (d.timesUsed ?? 0), 0) ?? 0, icon: TicketCheck },
2148
+ { id: "draft", label: "Rascunhos", value: items?.filter((d) => d.status === "draft").length ?? 0, icon: FileEdit }
2149
+ ]
1251
2150
  });
1252
2151
  const ShippingZonesCrudPage = createCrudPage(buildShopShippingZoneEntity(curr.code), {
1253
2152
  dataProvider: createShopShippingZoneDataProvider(resolveProvider),
1254
2153
  feature: "shop"
1255
2154
  });
1256
- const CRUD_PAGES = {
1257
- products: ProductsCrudPage,
1258
- customers: CustomersCrudPage,
1259
- discounts: DiscountsCrudPage,
1260
- shipping: ShippingZonesCrudPage
1261
- };
1262
- const SECTIONS = [
1263
- { id: "products", label: options?.sectionLabels?.products ?? "Produtos", icon: "Package" },
1264
- { id: "orders", label: options?.sectionLabels?.orders ?? "Pedidos", icon: "ShoppingCart" },
1265
- { id: "discounts", label: options?.sectionLabels?.discounts ?? "Descontos", icon: "Tag" },
1266
- { id: "shipping", label: options?.sectionLabels?.shipping ?? "Entrega", icon: "Truck" }
1267
- ];
1268
- const CustomersNavLabel = options?.sectionLabels?.customers ?? "Clientes";
2155
+ const CategoriesCrudPage = createCrudPage(buildShopCategoryEntity(), {
2156
+ dataProvider: createShopCategoryDataProvider(resolveProvider),
2157
+ feature: "shop"
2158
+ });
2159
+ const CollectionsCrudPage = createCrudPage(buildShopCollectionEntity(), {
2160
+ dataProvider: createShopCollectionDataProvider(resolveProvider),
2161
+ feature: "shop",
2162
+ stats: (items, total) => [
2163
+ { id: "total", label: "Cole\xE7\xF5es", value: total, icon: Layers },
2164
+ { id: "published", label: "Publicadas", value: items?.filter((c) => c.published).length ?? 0, icon: Eye },
2165
+ { id: "draft", label: "Rascunhos", value: items?.filter((c) => !c.published).length ?? 0, icon: FileEdit },
2166
+ {
2167
+ id: "members",
2168
+ label: "Produtos agrupados",
2169
+ icon: Package,
2170
+ value: items?.reduce((sum, c) => sum + (c.productIds?.length ?? 0), 0) ?? 0
2171
+ }
2172
+ ]
2173
+ });
2174
+ const GiftCardsCrudPage = createCrudPage(buildShopGiftCardEntity(curr.code), {
2175
+ dataProvider: createShopGiftCardDataProvider(resolveProvider),
2176
+ feature: "shop",
2177
+ stats: (items, total) => {
2178
+ const money3 = (v) => new Intl.NumberFormat(curr.locale, { style: "currency", currency: curr.code }).format(v);
2179
+ const active = items?.filter((g) => g.status === "active") ?? [];
2180
+ return [
2181
+ { id: "total", label: "Cart\xF5es emitidos", value: total, icon: CreditCard },
2182
+ { id: "active", label: "Ativos", value: active.length, icon: Eye },
2183
+ // Saldo em aberto é passivo: dinheiro já recebido que a loja ainda deve
2184
+ // em produto. É o número que a conciliação procura aqui.
2185
+ {
2186
+ id: "outstanding",
2187
+ label: "Saldo em aberto",
2188
+ icon: CircleDollarSign,
2189
+ value: money3(active.reduce((sum, g) => sum + (g.balance ?? 0), 0))
2190
+ },
2191
+ {
2192
+ id: "issued",
2193
+ label: "Valor emitido",
2194
+ icon: TicketCheck,
2195
+ value: money3(items?.reduce((sum, g) => sum + (g.initialBalance ?? 0), 0) ?? 0)
2196
+ }
2197
+ ];
2198
+ }
2199
+ });
2200
+ const ContentPageWrapped = () => React3.createElement(ContentPage, { resolveProvider: options?.provider });
2201
+ ContentPageWrapped.displayName = "ContentPage";
1269
2202
  const ChannelsPageWrapped = () => React3.createElement(ChannelsPage, { resolveProvider: options?.provider });
1270
2203
  ChannelsPageWrapped.displayName = "ChannelsPage";
1271
2204
  const navSection = options?.navSection ?? "main";
1272
2205
  const navPosition = options?.navPosition ?? 6;
2206
+ const NAV = [
2207
+ // Pedidos ▸ Pedidos · Clientes
2208
+ {
2209
+ route: "/orders",
2210
+ label: label("orders", "Pedidos"),
2211
+ icon: "ShoppingBag",
2212
+ position: navPosition,
2213
+ component: pageFor("orders")
2214
+ },
2215
+ {
2216
+ route: "/orders/customers",
2217
+ label: label("customers", "Clientes"),
2218
+ icon: "Users",
2219
+ position: navPosition + 0.1,
2220
+ component: CustomersCrudPage
2221
+ },
2222
+ // Produtos ▸ Produtos · Coleções · Categorias
2223
+ {
2224
+ route: "/products",
2225
+ label: label("products", "Produtos"),
2226
+ icon: "Package",
2227
+ position: navPosition + 1,
2228
+ component: ProductsCrudPage
2229
+ },
2230
+ {
2231
+ route: "/products/collections",
2232
+ label: label("collections", "Cole\xE7\xF5es"),
2233
+ icon: "Layers",
2234
+ position: navPosition + 1.1,
2235
+ component: CollectionsCrudPage
2236
+ },
2237
+ {
2238
+ route: "/products/categories",
2239
+ label: label("categories", "Categorias"),
2240
+ icon: "FolderTree",
2241
+ position: navPosition + 1.2,
2242
+ component: CategoriesCrudPage
2243
+ },
2244
+ // Conteúdo — a biblioteca de arquivos (sem filhos).
2245
+ {
2246
+ route: "/content",
2247
+ label: label("content", "Conte\xFAdo"),
2248
+ icon: "Image",
2249
+ position: navPosition + 2,
2250
+ indexChild: false,
2251
+ component: ContentPageWrapped
2252
+ },
2253
+ // Descontos ▸ Descontos · Gift Cards
2254
+ {
2255
+ route: "/discounts",
2256
+ label: label("discounts", "Descontos"),
2257
+ icon: "Tag",
2258
+ position: navPosition + 3,
2259
+ component: DiscountsCrudPage
2260
+ },
2261
+ {
2262
+ route: "/discounts/gift-cards",
2263
+ label: label("giftCards", "Gift Cards"),
2264
+ icon: "CreditCard",
2265
+ position: navPosition + 3.1,
2266
+ component: GiftCardsCrudPage
2267
+ }
2268
+ ];
2269
+ const HIDDEN_ROUTES = [
2270
+ { path: "/store", component: ChannelsPageWrapped, title: label("store", "Canais de venda") },
2271
+ {
2272
+ path: "/store/shipping",
2273
+ component: ShippingZonesCrudPage,
2274
+ title: label("shipping", "Entrega"),
2275
+ parentRoute: "/store"
2276
+ },
2277
+ // Reached from the Clientes tab strip, deliberately not from the menu:
2278
+ // three sidebar entries for one destination is the duplication the tab
2279
+ // strip exists to avoid. Nesting under /orders/customers also keeps
2280
+ // "Clientes" highlighted in the rail while you are on them.
2281
+ // Detalhe do modelo de e-mail. Sem entrada no menu: é uma linha da lista,
2282
+ // não um destino. Ganha do curinga /emails/* por especificidade de rota.
2283
+ {
2284
+ path: "/orders/customers/groups",
2285
+ component: CustomerGroupsCrudPage,
2286
+ title: label("customerGroups", "Grupos"),
2287
+ parentRoute: "/orders/customers"
2288
+ },
2289
+ {
2290
+ path: "/orders/customers/segments",
2291
+ component: CustomerSegmentsCrudPage,
2292
+ title: label("customerSegments", "Segmentos"),
2293
+ parentRoute: "/orders/customers"
2294
+ }
2295
+ ];
2296
+ const redirectTo = (target) => {
2297
+ const Redirect = () => {
2298
+ React3.useEffect(() => {
2299
+ adminNavigateTo(target);
2300
+ }, []);
2301
+ return null;
2302
+ };
2303
+ Redirect.displayName = `Redirect(${target})`;
2304
+ return Redirect;
2305
+ };
2306
+ const LEGACY_ROUTES = [
2307
+ { path: "/shop", component: redirectTo("/products") },
2308
+ { path: "/shop/products", component: redirectTo("/products") },
2309
+ { path: "/shop/orders", component: redirectTo("/orders") },
2310
+ { path: "/shop/discounts", component: redirectTo("/discounts") },
2311
+ { path: "/shop/shipping", component: redirectTo("/store/shipping") },
2312
+ { path: "/shop/channels", component: redirectTo("/store") },
2313
+ { path: "/customers", component: redirectTo("/orders/customers") },
2314
+ // A mensageria da loja virou parte de Automações — mesmos eventos, agora ao
2315
+ // lado dos do CRM e da agenda, e com WhatsApp além do e-mail.
2316
+ { path: "/emails", component: redirectTo("/automations") },
2317
+ { path: "/emails/:event", component: redirectTo("/automations") }
2318
+ ];
2319
+ const READ = { feature: "shop", action: "read" };
1273
2320
  return {
1274
2321
  id: "shop",
1275
2322
  name: options?.navLabel ?? "Shop",
@@ -1282,72 +2329,27 @@ function createShopPlugin(options) {
1282
2329
  scaffolds: ["ecommerce"],
1283
2330
  defaultEnabled: true,
1284
2331
  dependencies: [],
1285
- navigation: [
1286
- {
1287
- section: navSection,
1288
- position: navPosition,
1289
- label: options?.navLabel ?? "Shop",
1290
- route: "/shop",
1291
- icon: "ShoppingBag",
1292
- permission: { feature: "shop", action: "read" },
1293
- // "Loja" and "Produtos" are the same screen — don't list both.
1294
- indexChild: false
1295
- },
1296
- ...SECTIONS.map((s, i) => ({
1297
- section: navSection,
1298
- position: navPosition + i + 1,
1299
- label: s.label,
1300
- route: `/shop/${s.id}`,
1301
- icon: s.icon,
1302
- permission: { feature: "shop", action: "read" }
1303
- })),
1304
- // Clientes — top-level (route not under /shop, so it leaves the accordion).
1305
- // Fractional position keeps it right below "Loja", above other plugins.
1306
- {
1307
- section: navSection,
1308
- position: navPosition + 0.5,
1309
- label: CustomersNavLabel,
1310
- route: "/customers",
1311
- icon: "Users",
1312
- permission: { feature: "shop", action: "read" }
1313
- },
1314
- // Canais de venda — inside the "Loja" group (route under /shop nests it in
1315
- // the accordion). The store's points of sale (Loja Online, WhatsApp, …).
1316
- {
1317
- section: navSection,
1318
- position: navPosition + SECTIONS.length + 1,
1319
- label: "Canais de venda",
1320
- route: "/shop/channels",
1321
- icon: "Radio",
1322
- permission: { feature: "shop", action: "read" }
1323
- }
1324
- ],
2332
+ navigation: NAV.map((entry) => ({
2333
+ section: navSection,
2334
+ position: entry.position,
2335
+ label: entry.label,
2336
+ route: entry.route,
2337
+ icon: entry.icon,
2338
+ permission: READ,
2339
+ ...entry.indexChild === false ? { indexChild: false } : {}
2340
+ })),
1325
2341
  routes: [
1326
- {
1327
- // Kept so existing links to /shop still resolve; renders the tabbed page.
1328
- path: "/shop",
1329
- component: pageFor(),
1330
- permission: { feature: "shop", action: "read" }
1331
- },
1332
- ...SECTIONS.map((s) => ({
1333
- path: `/shop/${s.id}`,
1334
- component: CRUD_PAGES[s.id] ?? pageFor(s.id),
1335
- permission: { feature: "shop", action: "read" }
1336
- })),
1337
- // Clientes at the top level, same CRUD page as before.
1338
- {
1339
- path: "/customers",
1340
- component: CustomersCrudPage,
1341
- permission: { feature: "shop", action: "read" }
1342
- },
1343
- // Canais de venda management page (under the Loja group).
1344
- {
1345
- path: "/shop/channels",
1346
- component: ChannelsPageWrapped,
1347
- permission: { feature: "shop", action: "read" }
1348
- }
2342
+ ...NAV.map((entry) => ({ path: entry.route, component: entry.component, permission: READ })),
2343
+ ...HIDDEN_ROUTES.map((r) => ({ ...r, permission: READ })),
2344
+ ...LEGACY_ROUTES.map((r) => ({ ...r, permission: READ }))
1349
2345
  ],
1350
2346
  widgets: [],
2347
+ // Os cards de varejo ficaram na onda parqueada: os read models deles leem
2348
+ // public.fulfillments, que faz parte do modelo de estoque não portado.
2349
+ // Os fatos que a loja emite. Lidos por três consumidores — automações,
2350
+ // integrações e auditoria — e agregados pelo runtime só quando o plugin
2351
+ // está ATIVO, para um módulo desligado não anunciar eventos que não vêm.
2352
+ events: buildShopDomainEvents(),
1351
2353
  aiTools: [
1352
2354
  {
1353
2355
  id: "shop.list-products",