@fayz-ai/plugin-registry 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,8 +1,75 @@
1
- import { Briefcase, Tag, MapPin, Map } from 'lucide-react';
1
+ import { Briefcase, Tag, MapPin, Map, UserCog, Users } from 'lucide-react';
2
2
  import { createCrudPage, adminNavigateTo } from '@fayz-ai/admin';
3
- import { tl } from '@fayz-ai/core';
3
+ import { RECORD_GROUP, tl, activeFacet, recordRailFields } from '@fayz-ai/core';
4
4
 
5
5
  // src/index.ts
6
+ var PERSON_GROUPS = [
7
+ // Nome e documento NÃO entram: o arquétipo `person` os hasteia para o
8
+ // cabeçalho da ficha, ao lado da foto. O que sobra é o resto da pessoa.
9
+ { id: "identity", label: tl("Personal", "Dados pessoais"), columns: 2 },
10
+ { id: "contact", label: tl("Contact", "Contato"), columns: 2 },
11
+ { id: "where", label: tl("Address", "Endere\xE7o"), columns: 1 },
12
+ {
13
+ id: "banking",
14
+ label: tl("Banking", "Dados banc\xE1rios"),
15
+ columns: 1,
16
+ description: tl(
17
+ "Where money goes OUT to this person \u2014 not an account of the house.",
18
+ "Para onde o dinheiro SAI para esta pessoa \u2014 n\xE3o \xE9 conta da casa."
19
+ )
20
+ },
21
+ { ...RECORD_GROUP, label: tl("Record", "Situa\xE7\xE3o") }
22
+ ];
23
+ var ACTIVE_FACET = activeFacet({
24
+ active: tl("Active", "Ativos"),
25
+ inactive: tl("Inactive", "Inativos"),
26
+ all: tl("All", "Todos")
27
+ });
28
+ function contactChannelFields() {
29
+ return [
30
+ { key: "phone", label: tl("Phone", "Telefone"), type: "phone", showInTable: true, searchable: true, group: "contact" },
31
+ { key: "mobile", label: tl("Mobile", "Celular"), type: "phone", showInTable: true, group: "contact" },
32
+ { key: "email", label: "E-mail", type: "email", showInTable: true, searchable: true, group: "contact", span: 2 }
33
+ // Desligados — ver acima.
34
+ // { key: 'phone2', label: tl('Phone 2', 'Telefone 2'), type: 'phone', showInTable: false, group: 'contact' },
35
+ // { key: 'mobile2', label: tl('Mobile 2', 'Celular 2'), type: 'phone', showInTable: false, group: 'contact' },
36
+ // { key: 'email2', label: tl('E-mail 2', 'E-mail 2'), type: 'email', showInTable: false, group: 'contact' },
37
+ ];
38
+ }
39
+ function attachedFields() {
40
+ return [
41
+ { key: "address", label: tl("Address", "Endere\xE7o"), type: "address", showInTable: false, showInDetail: false, group: "where" },
42
+ { key: "payeeAccount", label: tl("Bank account", "Conta banc\xE1ria"), type: "bank", showInTable: false, showInDetail: false, group: "banking" }
43
+ ];
44
+ }
45
+ function recordFields(table = "people", options = {}) {
46
+ return recordRailFields({
47
+ tagsTable: table,
48
+ activeLabel: tl("Active", "Ativo"),
49
+ // Um cadastro que já pergunta o ESCOPO de unidades não pergunta também a
50
+ // unidade padrão: são duas perguntas parecidas e respostas diferentes, e
51
+ // na tela viravam dois seletores de unidade — um multi à esquerda e um
52
+ // único à direita — sem nada dizendo qual manda.
53
+ unit: options.unit
54
+ });
55
+ }
56
+ function genderField() {
57
+ return {
58
+ key: "gender",
59
+ label: tl("Gender", "Sexo"),
60
+ type: "select",
61
+ showInTable: false,
62
+ group: "identity",
63
+ options: [
64
+ { value: "masculino", label: tl("Male", "Masculino") },
65
+ { value: "feminino", label: tl("Female", "Feminino") },
66
+ { value: "outro", label: tl("Other", "Outro") }
67
+ ]
68
+ };
69
+ }
70
+ var PERSON_CUSTOM_FIELDS = ["gender", "mobile", "phone2", "mobile2", "email2"];
71
+
72
+ // src/lib/entities.ts
6
73
  var staffEntity = {
7
74
  name: tl("Staff", "Profissional"),
8
75
  namePlural: tl("Staff", "Profissionais"),
@@ -13,16 +80,80 @@ var staffEntity = {
13
80
  agentHidden: true,
14
81
  layout: "person",
15
82
  displayField: "name",
83
+ subtitleField: "profession",
16
84
  defaultSort: "name",
85
+ formColumns: 2,
86
+ facets: [ACTIVE_FACET],
17
87
  fieldGroups: [
18
- { id: "professional", label: tl("Professional Info", "Informa\xE7\xF5es Profissionais"), columns: 2 }
88
+ ...PERSON_GROUPS.filter((g) => g.id !== "record"),
89
+ {
90
+ id: "work",
91
+ label: tl("Work", "Trabalho"),
92
+ columns: 2,
93
+ description: tl(
94
+ "What this person does here, and how the calendar and the payroll see them.",
95
+ "O que esta pessoa faz aqui, e como a agenda e a folha a enxergam."
96
+ )
97
+ },
98
+ { ...RECORD_GROUP }
19
99
  ],
20
100
  fields: [
21
- { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
22
- { key: "email", label: "E-mail", type: "email", showInTable: true, searchable: true },
23
- { key: "phone", label: tl("Phone", "Telefone"), type: "phone", showInTable: true },
24
- { key: "profession", label: tl("Profession", "Profiss\xE3o"), type: "text", showInTable: true, group: "professional" },
25
- { key: "commissionRate", label: tl("Commission (%)", "Comiss\xE3o (%)"), type: "number", showInTable: true, group: "professional", defaultValue: 0 },
101
+ { key: "name", label: tl("Full name", "Nome completo"), type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
102
+ { key: "documentNumber", label: "CPF", type: "text", showInTable: true, searchable: true, group: "identity" },
103
+ // RG: o V1 pede os dois documentos na ficha de quem trabalha aqui, e a
104
+ // folha usa ambos.
105
+ { key: "rg", label: "RG", type: "text", showInTable: false, group: "identity" },
106
+ { key: "dateOfBirth", label: tl("Date of birth", "Data de nascimento"), type: "date", showInTable: false, group: "identity" },
107
+ genderField(),
108
+ ...contactChannelFields(),
109
+ ...attachedFields(),
110
+ { key: "profession", label: tl("Profession", "Profiss\xE3o"), type: "text", showInTable: true, group: "work" },
111
+ // O VÍNCULO muda o que a folha e o fisco esperam — um autônomo emite RPA,
112
+ // um CLT entra na folha, um PJ manda nota.
113
+ {
114
+ key: "employmentType",
115
+ label: tl("Employment type", "Tipo de v\xEDnculo"),
116
+ type: "select",
117
+ showInTable: false,
118
+ group: "work",
119
+ options: [
120
+ { value: "clt", label: "CLT" },
121
+ { value: "pj", label: "Pessoa jur\xEDdica" },
122
+ { value: "autonomo", label: "Aut\xF4nomo" },
123
+ { value: "estagio", label: "Est\xE1gio" },
124
+ { value: "socio", label: "S\xF3cio" }
125
+ ]
126
+ },
127
+ // CBO é o código que a folha e o eSocial pedem. Texto porque a tabela
128
+ // oficial tem ~2.500 entradas e um seletor delas seria pior que o campo.
129
+ {
130
+ key: "cboCode",
131
+ label: tl("CBO code", "CBO"),
132
+ type: "text",
133
+ showInTable: false,
134
+ group: "work",
135
+ hint: tl("Brazilian occupation code, used by payroll.", "Classifica\xE7\xE3o Brasileira de Ocupa\xE7\xF5es \u2014 a folha pede.")
136
+ },
137
+ {
138
+ key: "teamId",
139
+ label: tl("Team", "Equipe"),
140
+ type: "relation",
141
+ showInTable: true,
142
+ group: "work",
143
+ relation: { table: "categories", filter: { kind: "staff_team", is_active: true } },
144
+ hint: tl("Where this person works \u2014 reception, aesthetics, sales.", "Onde esta pessoa trabalha \u2014 recep\xE7\xE3o, est\xE9tica, vendas.")
145
+ },
146
+ { key: "commissionRate", label: tl("Commission (%)", "Comiss\xE3o (%)"), type: "number", showInTable: true, group: "work", defaultValue: 0 },
147
+ // O CENTRO DE CUSTO da pessoa é o que permite alocar o custo dela por
148
+ // área — é assim que a folha vira despesa de Operação e não um bolo só.
149
+ {
150
+ key: "costCenterId",
151
+ label: tl("Cost centre", "Centro de custo"),
152
+ type: "relation",
153
+ showInTable: false,
154
+ group: "work",
155
+ relation: { table: "plg_financial_cost_centers", labelField: "name" }
156
+ },
26
157
  // Trabalhar aqui não é atender cliente: a recepção e o financeiro entram
27
158
  // por esta mesma tela. Sem a escolha no cadastro, todo mundo nascia com
28
159
  // coluna na grade da semana e horário livre para marcarem em cima.
@@ -32,20 +163,20 @@ var staffEntity = {
32
163
  type: "boolean",
33
164
  showInTable: true,
34
165
  defaultValue: true,
35
- inlineToggle: true,
36
- group: "professional",
166
+ group: "work",
37
167
  hint: tl(
38
168
  "Clients can be booked with this person.",
39
169
  "Clientes podem ser marcados com esta pessoa."
40
170
  )
41
171
  },
42
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
172
+ // O que a recepção precisa saber ANTES de marcar com esta pessoa.
173
+ { key: "agendaNotes", label: tl("Calendar notes", "Informa\xE7\xF5es para a agenda"), type: "textarea", showInTable: false, group: "work", span: 2 },
174
+ ...recordFields()
43
175
  ],
44
176
  // No custom documents tab: the `person` archetype supplies the shared
45
177
  // custom_forms documents widget (same as clients / school-saas).
46
178
  data: {
47
179
  // Pessoa pura, como no resto-saas: quem é da equipe é `people.kind='staff'`.
48
- // A extensão `staff_members` e a view `v_staff` não existem mais.
49
180
  table: "people",
50
181
  tenantScoped: true,
51
182
  archetype: "person",
@@ -54,8 +185,18 @@ var staffEntity = {
54
185
  // verdade é a do motor de regras do Financeiro (plg_financial_commission_*),
55
186
  // que sabe dividir por serviço e por profissional; este número aqui é o
56
187
  // resquício de quando havia um só. Fica em custom_fields para não sumir.
57
- customFields: ["profession", "commissionRate"],
58
- searchColumns: ["name", "email", "phone"]
188
+ customFields: [
189
+ ...PERSON_CUSTOM_FIELDS,
190
+ "profession",
191
+ "commissionRate",
192
+ "teamId",
193
+ "employmentType",
194
+ "cboCode",
195
+ "agendaNotes",
196
+ "rg",
197
+ "costCenterId"
198
+ ],
199
+ searchColumns: ["name", "email", "phone", "document_number"]
59
200
  }
60
201
  };
61
202
  var supplierEntity = {
@@ -64,15 +205,15 @@ var supplierEntity = {
64
205
  icon: "Building2",
65
206
  layout: "person",
66
207
  displayField: "name",
208
+ subtitleField: "documentNumber",
67
209
  defaultSort: "name",
68
210
  formColumns: 2,
69
- fieldGroups: [
70
- { id: "identity", label: tl("Identity", "Identifica\xE7\xE3o"), columns: 2 },
71
- { id: "contact", label: tl("Contact", "Contato"), columns: 2 },
72
- { id: "record", label: tl("Record", "Cadastro"), columns: 1, column: "side" }
73
- ],
211
+ facets: [ACTIVE_FACET],
212
+ fieldGroups: PERSON_GROUPS,
74
213
  fields: [
75
214
  { key: "name", label: tl("Legal name / Name", "Raz\xE3o social / Nome"), type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
215
+ // O tipo de prestador MUDA o documento: um autônomo assina com CPF, uma
216
+ // empresa com CNPJ, e a inscrição estadual só existe para quem é inscrito.
76
217
  {
77
218
  key: "serviceProviderType",
78
219
  label: tl("Service provider type", "Tipo de prestador de servi\xE7o"),
@@ -87,22 +228,42 @@ var supplierEntity = {
87
228
  { value: "autonomo", label: tl("Self-employed", "Aut\xF4nomo") }
88
229
  ]
89
230
  },
90
- { key: "documentNumber", label: "CNPJ / CPF", type: "text", searchable: true, showInTable: true, group: "identity" },
91
- { key: "registrationNumber", label: tl("State / municipal registration", "Inscri\xE7\xE3o estadual / municipal"), type: "text", group: "identity" },
92
- { key: "email", label: "E-mail", type: "email", showInTable: true, searchable: true, group: "contact" },
93
- { key: "phone", label: tl("Phone", "Telefone"), type: "phone", showInTable: true, searchable: true, group: "contact" },
94
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true, group: "record" },
95
- { key: "notes", label: tl("Notes", "Observa\xE7\xF5es"), type: "textarea", showInTable: false, group: "record" }
231
+ {
232
+ key: "documentNumber",
233
+ type: "text",
234
+ searchable: true,
235
+ showInTable: true,
236
+ group: "identity",
237
+ label: "CNPJ / CPF",
238
+ // Rotular sempre «CNPJ / CPF» faz o operador parar para escolher qual é.
239
+ // Ele já escolheu na linha de cima.
240
+ labelWhen: (v) => v.serviceProviderType === "pj" ? "CNPJ" : "CPF"
241
+ },
242
+ {
243
+ key: "registrationNumber",
244
+ label: tl("State / municipal registration", "Inscri\xE7\xE3o estadual / municipal"),
245
+ type: "text",
246
+ group: "identity",
247
+ // Pessoa física não tem inscrição estadual. Perguntar é convidar a
248
+ // preencher o que ninguém vai ler.
249
+ visibleWhen: (v) => v.serviceProviderType !== "pf"
250
+ },
251
+ // O GRUPO do V1 («Grupo 1», «Grupo 2»): como a casa separa os fornecedores
252
+ // dela. Texto livre porque no V1 é isso mesmo — uma taxonomia por conta
253
+ // viraria um cadastro que ninguém pediu.
254
+ { key: "groupName", label: tl("Group", "Grupo"), type: "text", showInTable: false, group: "identity" },
255
+ ...contactChannelFields(),
256
+ ...attachedFields(),
257
+ ...recordFields()
96
258
  ],
97
259
  data: {
98
260
  table: "people",
99
- schema: "public",
100
261
  tenantScoped: true,
101
262
  archetype: "person",
102
263
  archetypeKind: "supplier",
103
264
  filters: { kind: "supplier" },
104
265
  defaults: { kind: "supplier" },
105
- customFields: ["serviceProviderType", "registrationNumber"],
266
+ customFields: [...PERSON_CUSTOM_FIELDS, "serviceProviderType", "registrationNumber", "groupName"],
106
267
  searchColumns: ["name", "email", "phone", "document_number"]
107
268
  }
108
269
  };
@@ -112,23 +273,31 @@ var contactEntity = {
112
273
  icon: "Contact",
113
274
  layout: "person",
114
275
  displayField: "name",
276
+ subtitleField: "documentNumber",
115
277
  defaultSort: "name",
278
+ formColumns: 2,
279
+ facets: [ACTIVE_FACET],
280
+ fieldGroups: PERSON_GROUPS,
116
281
  fields: [
117
- { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
118
- { key: "email", label: "E-mail", type: "email", showInTable: true, searchable: true },
119
- { key: "phone", label: tl("Phone", "Telefone"), type: "phone", showInTable: true, searchable: true },
120
- { key: "notes", label: tl("Notes", "Observa\xE7\xF5es"), type: "textarea", showInTable: false },
121
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
282
+ { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
283
+ { key: "documentNumber", label: "CPF / CNPJ", type: "text", showInTable: true, searchable: true, group: "identity" },
284
+ genderField(),
285
+ ...contactChannelFields(),
286
+ ...attachedFields(),
287
+ ...recordFields()
122
288
  ],
123
289
  data: {
124
290
  table: "people",
125
- schema: "public",
291
+ // Sem `schema`, de propósito: com ele a entidade resolvia para o provider
292
+ // genérico de tabela, que não sabe ler nem gravar `custom_fields` — e
293
+ // metade desta ficha mora lá. `public` já é o padrão.
126
294
  tenantScoped: true,
127
295
  archetype: "person",
128
296
  archetypeKind: "contact",
129
297
  filters: { kind: "contact" },
130
298
  defaults: { kind: "contact" },
131
- searchColumns: ["name", "email", "phone"]
299
+ customFields: PERSON_CUSTOM_FIELDS,
300
+ searchColumns: ["name", "email", "phone", "document_number"]
132
301
  }
133
302
  };
134
303
  var partnershipEntity = {
@@ -138,19 +307,50 @@ var partnershipEntity = {
138
307
  layout: "person",
139
308
  displayField: "name",
140
309
  defaultSort: "name",
310
+ formColumns: 2,
311
+ facets: [ACTIVE_FACET],
312
+ fieldGroups: PERSON_GROUPS,
141
313
  fields: [
142
- { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
143
- { key: "phone", label: tl("Phone", "Telefone"), type: "phone", showInTable: true },
144
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
314
+ { key: "name", label: tl("Partnership name", "Nome da parceria"), type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
315
+ { key: "documentNumber", label: "CNPJ / CPF", type: "text", showInTable: true, searchable: true, group: "identity" },
316
+ // O ESCOPO de unidades, que no V1 é o par «Limitar unidades? → quais».
317
+ // Escolher «Todas» e ainda ver a lista de unidades é oferecer uma resposta
318
+ // que não vale; por isso a lista só aparece depois da escolha.
319
+ {
320
+ key: "unitScope",
321
+ label: tl("Units", "Unidades"),
322
+ type: "segmented",
323
+ defaultValue: "all",
324
+ group: "identity",
325
+ span: 2,
326
+ options: [
327
+ { value: "all", label: tl("All units", "Todas as unidades") },
328
+ { value: "selected", label: tl("Pick units", "Selecionar unidades") }
329
+ ]
330
+ },
331
+ {
332
+ key: "units",
333
+ label: tl("Selected units", "Unidades selecionadas"),
334
+ type: "relation",
335
+ showInTable: false,
336
+ group: "identity",
337
+ span: 2,
338
+ relation: { table: "v_units", labelField: "name" },
339
+ visibleWhen: (v) => v.unitScope === "selected"
340
+ },
341
+ ...contactChannelFields(),
342
+ ...attachedFields(),
343
+ ...recordFields("people", { unit: false })
145
344
  ],
146
345
  data: {
147
346
  table: "people",
148
- schema: "public",
149
347
  tenantScoped: true,
150
348
  archetype: "person",
151
349
  archetypeKind: "partner",
152
350
  filters: { kind: "partner" },
153
- defaults: { kind: "partner" }
351
+ defaults: { kind: "partner" },
352
+ customFields: [...PERSON_CUSTOM_FIELDS, "unitScope", "units"],
353
+ searchColumns: ["name", "email", "phone", "document_number"]
154
354
  }
155
355
  };
156
356
  var equipmentEntity = {
@@ -160,18 +360,69 @@ var equipmentEntity = {
160
360
  layout: "product",
161
361
  displayField: "name",
162
362
  defaultSort: "name",
363
+ formColumns: 2,
364
+ facets: [ACTIVE_FACET],
365
+ fieldGroups: [
366
+ { id: "identity", label: tl("Identification", "Identifica\xE7\xE3o"), columns: 2 },
367
+ { id: "purchase", label: tl("Purchase", "Compra e garantia"), columns: 2 },
368
+ {
369
+ id: "maintenance",
370
+ label: tl("Maintenance", "Manuten\xE7\xE3o"),
371
+ columns: 2,
372
+ description: tl(
373
+ "The next date is what a list of equipment is FOR.",
374
+ "A pr\xF3xima data \xE9 para o que uma lista de equipamentos serve."
375
+ )
376
+ },
377
+ { ...RECORD_GROUP }
378
+ ],
163
379
  fields: [
164
- { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
165
- { key: "description", label: tl("Model", "Modelo"), type: "text", showInTable: true },
166
- { key: "sku", label: tl("Serial Number", "N\xFAmero de S\xE9rie"), type: "text", showInTable: true },
167
- { key: "status", label: "Status", type: "select", options: ["active", "maintenance", "retired"], showInTable: true, defaultValue: "active" }
380
+ { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
381
+ { key: "brand", label: tl("Brand", "Marca"), type: "text", showInTable: true, group: "identity" },
382
+ { key: "model", label: tl("Model", "Modelo"), type: "text", showInTable: true, group: "identity" },
383
+ // O NÚMERO DE SÉRIE é o que a assistência técnica pede e o que distingue
384
+ // dois aparelhos iguais na mesma sala.
385
+ { key: "sku", label: tl("Serial number", "N\xFAmero de s\xE9rie"), type: "text", showInTable: false, searchable: true, group: "identity" },
386
+ {
387
+ key: "status",
388
+ label: "Status",
389
+ type: "select",
390
+ showInTable: true,
391
+ defaultValue: "active",
392
+ group: "identity",
393
+ options: [
394
+ { value: "active", label: tl("Active", "Ativo") },
395
+ { value: "maintenance", label: tl("In maintenance", "Em manuten\xE7\xE3o") },
396
+ { value: "inactive", label: tl("Retired", "Inativo") }
397
+ ]
398
+ },
399
+ // A SALA onde ele está. Aponta para o cadastro de Locais.
400
+ {
401
+ key: "serviceLocationId",
402
+ label: tl("Location", "Local"),
403
+ type: "relation",
404
+ showInTable: true,
405
+ group: "identity",
406
+ relation: { table: "v_units", labelField: "name" }
407
+ },
408
+ { key: "purchaseDate", label: tl("Purchase date", "Data de compra"), type: "date", showInTable: false, group: "purchase" },
409
+ { key: "purchaseValue", label: tl("Purchase value", "Valor de compra"), type: "currency", showInTable: false, group: "purchase" },
410
+ { key: "warrantyExpiration", label: tl("Warranty ends", "Vencimento da garantia"), type: "date", showInTable: false, group: "purchase" },
411
+ { key: "lastMaintenanceDate", label: tl("Last maintenance", "\xDAltima manuten\xE7\xE3o"), type: "date", showInTable: false, group: "maintenance" },
412
+ // A COLUNA que justifica a lista: quem abre «Equipamentos» está quase
413
+ // sempre perguntando o que vence agora.
414
+ { key: "nextMaintenanceDate", label: tl("Next maintenance", "Pr\xF3xima manuten\xE7\xE3o"), type: "date", showInTable: true, group: "maintenance" },
415
+ { key: "maintenanceNotes", label: tl("Maintenance notes", "Notas de manuten\xE7\xE3o"), type: "textarea", showInTable: false, group: "maintenance", span: 2 },
416
+ { key: "description", label: tl("Description", "Descri\xE7\xE3o"), type: "textarea", showInTable: false, group: "identity", span: 2 },
417
+ ...recordRailFields({ tagsTable: "products", activeLabel: tl("Active", "Ativo") })
168
418
  ],
169
419
  data: {
170
420
  table: "products",
171
421
  schema: "public",
172
422
  tenantScoped: true,
173
423
  filters: { kind: "asset" },
174
- defaults: { kind: "asset" }
424
+ defaults: { kind: "asset" },
425
+ searchColumns: ["name", "sku"]
175
426
  }
176
427
  };
177
428
  var originEntity = {
@@ -180,16 +431,68 @@ var originEntity = {
180
431
  icon: "Globe",
181
432
  displayField: "name",
182
433
  defaultSort: "name",
434
+ formColumns: 2,
435
+ facets: [ACTIVE_FACET],
436
+ fieldGroups: [
437
+ { id: "identity", label: tl("Identification", "Identifica\xE7\xE3o"), columns: 1 },
438
+ { ...RECORD_GROUP }
439
+ ],
183
440
  fields: [
184
- { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
185
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
441
+ { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true, group: "identity" },
442
+ // «SOLICITAR PESSOA» é uma regra que vale do OUTRO lado: ligada, escolher
443
+ // esta origem no cadastro de cliente passa a exigir quem indicou. Sem ela,
444
+ // «indicação de amigo» vira uma estatística sem nome nenhum atrás.
445
+ {
446
+ key: "requiresPerson",
447
+ label: tl("Ask who referred", "Solicitar pessoa"),
448
+ type: "boolean",
449
+ showInTable: true,
450
+ defaultValue: false,
451
+ group: "identity",
452
+ hint: tl(
453
+ "Choosing this origin on a client will require naming who referred them.",
454
+ "Escolher esta origem num cliente passa a exigir dizer quem indicou."
455
+ )
456
+ },
457
+ // A DESCRIÇÃO da origem é a observação: `categories` não tem coluna
458
+ // separada, e duas colunas para uma frase é invenção. Ela vai para a
459
+ // lista porque é o que distingue «Indicação» de «Indicação de parceiro».
460
+ ...recordRailFields({ tagsTable: "categories", activeLabel: tl("Active", "Ativo"), unit: false, notesInTable: true })
186
461
  ],
187
462
  data: {
188
463
  table: "categories",
189
464
  schema: "public",
190
465
  tenantScoped: true,
191
466
  filters: { kind: "origin" },
192
- defaults: { kind: "origin" }
467
+ defaults: { kind: "origin" },
468
+ customFields: ["requiresPerson"],
469
+ searchColumns: ["name"]
470
+ }
471
+ };
472
+ var teamEntity = {
473
+ name: tl("Team", "Equipe"),
474
+ namePlural: tl("Teams", "Equipes"),
475
+ icon: "Users",
476
+ displayField: "name",
477
+ formColumns: 2,
478
+ facets: [ACTIVE_FACET],
479
+ fieldGroups: [{ ...RECORD_GROUP }],
480
+ defaultSort: "sortOrder",
481
+ fields: [
482
+ { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
483
+ // A cor é o que faz a equipe se reconhecer de relance na grade da semana e
484
+ // no quadro — é o mesmo papel que ela tem na etiqueta.
485
+ { key: "color", label: tl("Colour", "Cor"), type: "color", showInTable: true },
486
+ { key: "sortOrder", label: tl("Order", "Ordem"), type: "number", showInTable: true, defaultValue: 0 },
487
+ ...recordRailFields({ tagsTable: "categories", activeLabel: tl("Active", "Ativo"), unit: false })
488
+ ],
489
+ data: {
490
+ table: "categories",
491
+ schema: "public",
492
+ tenantScoped: true,
493
+ filters: { kind: "staff_team" },
494
+ defaults: { kind: "staff_team" },
495
+ searchColumns: ["name"]
193
496
  }
194
497
  };
195
498
  var serviceCategoryEntity = {
@@ -197,11 +500,14 @@ var serviceCategoryEntity = {
197
500
  namePlural: tl("Service Categories", "Categorias de Servi\xE7o"),
198
501
  icon: "Tag",
199
502
  displayField: "name",
503
+ formColumns: 2,
504
+ facets: [ACTIVE_FACET],
505
+ fieldGroups: [{ ...RECORD_GROUP }],
200
506
  defaultSort: "sortOrder",
201
507
  fields: [
202
508
  { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
203
509
  { key: "sortOrder", label: tl("Order", "Ordem"), type: "number", showInTable: true, defaultValue: 0 },
204
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
510
+ ...recordRailFields({ tagsTable: "categories", activeLabel: tl("Active", "Ativo"), unit: false })
205
511
  ],
206
512
  data: {
207
513
  table: "categories",
@@ -212,35 +518,57 @@ var serviceCategoryEntity = {
212
518
  }
213
519
  };
214
520
  var serviceLocationEntity = {
215
- name: tl("Service Location", "Local de Atendimento"),
216
- namePlural: tl("Service Locations", "Locais de Atendimento"),
521
+ name: tl("Location", "Local"),
522
+ namePlural: tl("Locations", "Locais"),
217
523
  icon: "MapPin",
218
- layout: "location",
219
524
  displayField: "name",
220
- subtitleField: "city",
221
525
  defaultSort: "name",
526
+ formColumns: 2,
527
+ facets: [ACTIVE_FACET],
528
+ fieldGroups: [
529
+ {
530
+ id: "identity",
531
+ label: tl("The location", "O local"),
532
+ columns: 2,
533
+ description: tl(
534
+ "Where you attend, inside a unit. Not a unit: no till, no stock, no permission of its own.",
535
+ "Onde se atende, dentro de uma unidade. N\xE3o \xE9 unidade: n\xE3o tem caixa, n\xE3o tem estoque e n\xE3o recorta permiss\xE3o."
536
+ )
537
+ },
538
+ { ...RECORD_GROUP }
539
+ ],
222
540
  fields: [
223
- { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
224
- { key: "phone", label: tl("Phone", "Telefone"), type: "phone", showInTable: true },
225
- { key: "city", label: tl("City", "Cidade"), type: "text", showInTable: true, searchable: true },
226
- { key: "state", label: tl("State", "Estado"), type: "text", showInTable: true },
227
- { key: "address", label: tl("Address", "Endere\xE7o"), type: "text" },
228
- { key: "country", label: tl("Country", "Pa\xEDs"), type: "text", defaultValue: "BR" },
229
- { key: "postalCode", label: tl("Postal Code", "CEP"), type: "text" },
230
- { key: "email", label: "E-mail", type: "email" },
231
- { key: "isHeadquarters", label: tl("Headquarters", "Matriz"), type: "boolean", showInTable: true, defaultValue: false },
232
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true },
233
- { key: "notes", label: tl("Notes", "Observa\xE7\xF5es"), type: "textarea" }
541
+ { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true, group: "identity" },
542
+ { key: "code", label: tl("Code", "C\xF3digo"), type: "text", showInTable: true, searchable: true, group: "identity" },
543
+ {
544
+ key: "groupId",
545
+ label: tl("Group", "Grupo"),
546
+ type: "relation",
547
+ showInTable: true,
548
+ group: "identity",
549
+ relation: { table: "categories", labelField: "name", filter: { kind: "location_group" } }
550
+ },
551
+ // A LOTAÇÃO é o que impede a agenda de marcar cinco pessoas às 14h onde
552
+ // cabem quatro. Sem ela o local é só um rótulo.
553
+ {
554
+ key: "capacity",
555
+ label: tl("Capacity", "M\xE1ximo de clientes simult\xE2neos"),
556
+ type: "number",
557
+ min: 1,
558
+ defaultValue: 1,
559
+ showInTable: true,
560
+ group: "identity",
561
+ hint: tl("One chair is 1. A tanning room with four beds is 4.", "Uma cadeira \xE9 1. Uma sala de bronzeamento com quatro macas \xE9 4.")
562
+ },
563
+ // A unidade vem do RAIL, com todos os outros «de quem é este registro» —
564
+ // e obrigatória, porque um local sem prédio não existe.
565
+ ...recordRailFields({ tagsTable: "rooms", activeLabel: tl("Active", "Ativo"), unitRequired: true })
234
566
  ],
235
567
  data: {
236
- // A filial É a unidade. `v_units` é a ponte pública para o schema `app`, e
237
- // traz exatamente os campos desta tela (nome, contato, endereço, matriz,
238
- // ativo, observações). A tabela `locations` não existe mais.
239
- table: "v_units",
568
+ table: "rooms",
569
+ schema: "public",
240
570
  tenantScoped: true,
241
- archetype: "location",
242
- archetypeKind: "branch",
243
- searchColumns: ["name", "city", "state"]
571
+ searchColumns: ["name", "code"]
244
572
  }
245
573
  };
246
574
  var locationGroupEntity = {
@@ -248,11 +576,14 @@ var locationGroupEntity = {
248
576
  namePlural: tl("Location Groups", "Grupos de Locais"),
249
577
  icon: "Map",
250
578
  displayField: "name",
579
+ formColumns: 2,
580
+ facets: [ACTIVE_FACET],
581
+ fieldGroups: [{ ...RECORD_GROUP }],
251
582
  defaultSort: "sortOrder",
252
583
  fields: [
253
584
  { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true },
254
585
  { key: "sortOrder", label: tl("Order", "Ordem"), type: "number", showInTable: true, defaultValue: 0 },
255
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
586
+ ...recordRailFields({ tagsTable: "categories", activeLabel: tl("Active", "Ativo"), unit: false })
256
587
  ],
257
588
  data: {
258
589
  table: "categories",
@@ -265,32 +596,65 @@ var locationGroupEntity = {
265
596
  };
266
597
  var serviceEntity = {
267
598
  name: tl("Service", "Servi\xE7o"),
599
+ namePlural: tl("Services", "Servi\xE7os"),
268
600
  icon: "Briefcase",
269
601
  layout: "service",
270
602
  displayField: "name",
271
603
  defaultSort: "name",
604
+ formColumns: 2,
605
+ facets: [ACTIVE_FACET],
606
+ fieldGroups: [
607
+ { id: "identity", label: tl("Identification", "Identifica\xE7\xE3o"), columns: 2 },
608
+ { id: "pricing", label: tl("Price and time", "Pre\xE7o e tempo"), columns: 2 },
609
+ {
610
+ id: "texts",
611
+ label: tl("What the client reads", "O que o cliente l\xEA"),
612
+ columns: 1,
613
+ description: tl(
614
+ "Written once here, sent every time the service is booked or finished.",
615
+ "Escrito uma vez aqui, enviado toda vez que o servi\xE7o \xE9 marcado ou conclu\xEDdo."
616
+ )
617
+ },
618
+ { ...RECORD_GROUP }
619
+ ],
272
620
  fields: [
273
- { key: "name", label: tl("Service Name", "Nome do Servi\xE7o"), type: "text", required: true, searchable: true, showInTable: true },
274
- { key: "description", label: tl("Description", "Descri\xE7\xE3o"), type: "textarea", showInTable: false },
275
- { key: "durationMinutes", label: tl("Duration (min)", "Dura\xE7\xE3o (min)"), type: "number", required: true, showInTable: true },
276
- { key: "price", label: tl("Price", "Pre\xE7o"), type: "currency", required: true, showInTable: true },
277
- // A pergunta que o BeautySoft responde com `service_kind`: isto se MARCA,
278
- // ou se VENDE? O tratamento entra num pacote e o que vai para a agenda é o
279
- // passo — a avaliação, a revisão, a entrega do que foi comprado antes.
280
- // Ver a migration 120 do spine.
621
+ { key: "name", label: tl("Service name", "Nome do servi\xE7o"), type: "text", required: true, searchable: true, showInTable: true, group: "identity", span: 2 },
622
+ // O CÓDIGO INTERNO é como a casa chama o serviço na comanda e no relatório
623
+ // — e é por ele que se procura quando o nome tem três variações.
624
+ { key: "internalCode", label: tl("Internal code", "C\xF3digo interno"), type: "text", showInTable: true, searchable: true, group: "identity" },
625
+ {
626
+ key: "categoryId",
627
+ label: tl("Category", "Categoria"),
628
+ type: "relation",
629
+ showInTable: true,
630
+ group: "identity",
631
+ relation: { table: "categories", labelField: "name", filter: { kind: "service_category" } }
632
+ },
633
+ // A COR identifica o serviço na grade da agenda, onde não cabe o nome.
634
+ { key: "color", label: tl("Colour", "Cor de identifica\xE7\xE3o"), type: "color", showInTable: false, group: "identity" },
635
+ { key: "description", label: tl("Description", "Descri\xE7\xE3o"), type: "textarea", showInTable: false, group: "identity", span: 2 },
636
+ { key: "price", label: tl("Price", "Pre\xE7o"), type: "currency", required: true, showInTable: true, group: "pricing" },
637
+ { key: "durationMinutes", label: tl("Duration (min)", "Dura\xE7\xE3o (min)"), type: "number", required: true, showInTable: true, group: "pricing" },
638
+ // VALIDADE em dias: quanto tempo o cliente tem para executar o que
639
+ // comprou. É o que faz um pacote expirar, e sem ela ele nunca expira.
640
+ { key: "validityDays", label: tl("Validity (days)", "Validade (dias)"), type: "number", showInTable: false, group: "pricing" },
281
641
  {
282
642
  key: "isBookable",
283
643
  label: tl("Appears in the calendar", "Aparece na agenda"),
284
644
  type: "boolean",
285
645
  showInTable: true,
286
646
  defaultValue: true,
287
- inlineToggle: true,
647
+ group: "pricing",
288
648
  hint: tl(
289
649
  "Can be picked when booking an appointment. Turn off for what is sold in a package and never booked on its own.",
290
650
  "Pode ser escolhido ao marcar um hor\xE1rio. Desligue para o que \xE9 vendido em pacote e nunca se marca sozinho."
291
651
  )
292
652
  },
293
- { key: "isActive", label: tl("Active", "Ativo"), type: "boolean", showInTable: true, defaultValue: true, inlineToggle: true }
653
+ { key: "schedulingAlertText", label: tl("Booking alert", "Alertas para agendamento"), type: "textarea", showInTable: false, group: "texts" },
654
+ { key: "reminderText", label: tl("Reminder text", "Texto do lembrete"), type: "textarea", showInTable: false, group: "texts" },
655
+ { key: "contractText", label: tl("Contract text", "Texto para contrato"), type: "textarea", showInTable: false, group: "texts" },
656
+ { key: "postServiceText", label: tl("After-service text", "Texto de p\xF3s-atendimento"), type: "textarea", showInTable: false, group: "texts" },
657
+ ...recordRailFields({ tagsTable: "services", activeLabel: tl("Active", "Ativo") })
294
658
  ],
295
659
  // Services are their own Ring-0 archetype table (public.services), not a
296
660
  // kind of product. Direct query by table+schema (no kind filter).
@@ -301,24 +665,176 @@ var serviceEntity = {
301
665
  searchColumns: ["name"]
302
666
  }
303
667
  };
668
+ var professionEntity = {
669
+ name: tl("Profession", "Profiss\xE3o"),
670
+ namePlural: tl("Professions", "Profiss\xF5es"),
671
+ icon: "BriefcaseBusiness",
672
+ displayField: "name",
673
+ defaultSort: "sortOrder",
674
+ formColumns: 2,
675
+ facets: [ACTIVE_FACET],
676
+ fieldGroups: [
677
+ {
678
+ id: "identity",
679
+ label: tl("Identification", "Identifica\xE7\xE3o"),
680
+ columns: 2,
681
+ description: tl(
682
+ "The list the house keeps, so a rota does not depend on how someone typed it.",
683
+ "A lista que a casa mant\xE9m, para a escala n\xE3o depender de como algu\xE9m digitou."
684
+ )
685
+ },
686
+ { ...RECORD_GROUP }
687
+ ],
688
+ fields: [
689
+ { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
690
+ { key: "sortOrder", label: tl("Order", "Ordem"), type: "number", showInTable: true, defaultValue: 0, group: "identity" },
691
+ ...recordRailFields({ tagsTable: "categories", activeLabel: tl("Active", "Ativo"), unit: false, notesInTable: true })
692
+ ],
693
+ data: {
694
+ table: "categories",
695
+ schema: "public",
696
+ tenantScoped: true,
697
+ filters: { kind: "profession" },
698
+ defaults: { kind: "profession" },
699
+ searchColumns: ["name"]
700
+ }
701
+ };
702
+ var petSpeciesEntity = {
703
+ name: tl("Species", "Esp\xE9cie"),
704
+ namePlural: tl("Animal species", "Tipos de animais"),
705
+ icon: "PawPrint",
706
+ displayField: "name",
707
+ defaultSort: "sortOrder",
708
+ formColumns: 2,
709
+ facets: [ACTIVE_FACET],
710
+ fieldGroups: [
711
+ { id: "identity", label: tl("Identification", "Identifica\xE7\xE3o"), columns: 2 },
712
+ { ...RECORD_GROUP }
713
+ ],
714
+ fields: [
715
+ { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true, group: "identity", span: 2 },
716
+ // A ORDEM existe porque a espécie mais atendida tem de estar em cima: um
717
+ // hotel para aves não quer «cachorro» na primeira posição.
718
+ { key: "sortOrder", label: tl("Order", "Ordem"), type: "number", showInTable: true, defaultValue: 0, group: "identity" },
719
+ ...recordRailFields({ tagsTable: "categories", activeLabel: tl("Active", "Ativo"), unit: false })
720
+ ],
721
+ data: {
722
+ table: "categories",
723
+ schema: "public",
724
+ tenantScoped: true,
725
+ filters: { kind: "pet_species" },
726
+ defaults: { kind: "pet_species" },
727
+ searchColumns: ["name"]
728
+ }
729
+ };
730
+ var servicePackageItemEntity = {
731
+ name: tl("Item", "Item"),
732
+ namePlural: tl("Items", "Servi\xE7os do pacote"),
733
+ icon: "ListChecks",
734
+ displayField: "productId",
735
+ defaultSort: "sortOrder",
736
+ fields: [
737
+ {
738
+ key: "productId",
739
+ label: tl("Service", "Servi\xE7o"),
740
+ type: "relation",
741
+ required: true,
742
+ showInTable: true,
743
+ relation: { table: "services", labelField: "name" }
744
+ },
745
+ // QUANTAS VEZES este serviço entra no pacote. «10 sessões de drenagem» é
746
+ // uma linha com quantidade 10, não dez linhas.
747
+ { key: "quantity", label: tl("Quantity", "Quantidade"), type: "number", min: 1, defaultValue: 1, showInTable: true },
748
+ { key: "sortOrder", label: tl("Order", "Ordem"), type: "number", defaultValue: 0, showInTable: true }
749
+ ],
750
+ data: { table: "service_package_items", schema: "public", tenantScoped: true }
751
+ };
752
+ var servicePackageEntity = {
753
+ name: tl("Package", "Pacote"),
754
+ namePlural: tl("Service packages", "Pacotes de servi\xE7o"),
755
+ icon: "PackageCheck",
756
+ displayField: "name",
757
+ defaultSort: "name",
758
+ formColumns: 2,
759
+ facets: [ACTIVE_FACET],
760
+ fieldGroups: [
761
+ { id: "identity", label: tl("Identification", "Identifica\xE7\xE3o"), columns: 1 },
762
+ {
763
+ id: "pricing",
764
+ label: tl("Price and validity", "Pre\xE7o e validade"),
765
+ columns: 2,
766
+ description: tl(
767
+ "What the client pays once, and until when they can use it.",
768
+ "O que o cliente paga de uma vez, e at\xE9 quando pode usar."
769
+ )
770
+ },
771
+ { ...RECORD_GROUP }
772
+ ],
773
+ fields: [
774
+ { key: "name", label: tl("Name", "Nome"), type: "text", required: true, showInTable: true, searchable: true, group: "identity" },
775
+ { key: "description", label: tl("Description", "Descri\xE7\xE3o"), type: "textarea", showInTable: false, group: "identity" },
776
+ { key: "price", label: tl("Price", "Pre\xE7o"), type: "currency", required: true, showInTable: true, group: "pricing" },
777
+ // O TOTAL DE SESSÕES é o que o cliente comprou; os itens dizem de QUÊ.
778
+ // Os dois existem porque um pacote pode vender «10 sessões» escolhendo o
779
+ // serviço na hora, e outro pode listar exatamente o que entra.
780
+ { key: "sessionsTotal", label: tl("Sessions", "Total de sess\xF5es"), type: "number", min: 1, showInTable: true, group: "pricing" },
781
+ // SEM VALIDADE o pacote nunca expira, e o saldo de sessões vira uma dívida
782
+ // sem prazo — é este número que faz o pacote vencer.
783
+ {
784
+ key: "validityDays",
785
+ label: tl("Validity (days)", "Validade (dias)"),
786
+ type: "number",
787
+ showInTable: true,
788
+ group: "pricing",
789
+ hint: tl("Empty means it never expires.", "Vazio significa que nunca vence.")
790
+ },
791
+ ...recordRailFields({ tagsTable: "service_packages", activeLabel: tl("Active", "Ativo") })
792
+ ],
793
+ relatedLists: [
794
+ { entity: servicePackageItemEntity, foreignKey: "packageId", icon: "ListChecks" }
795
+ ],
796
+ data: {
797
+ table: "service_packages",
798
+ schema: "public",
799
+ tenantScoped: true,
800
+ searchColumns: ["name"]
801
+ }
802
+ };
304
803
 
305
804
  // src/index.ts
306
805
  var SCREENS = [
307
806
  { id: "services", path: "/registry/services", entity: serviceEntity, icon: "Briefcase", feature: "services" },
308
807
  { id: "serviceCategories", path: "/registry/categories", entity: serviceCategoryEntity, icon: "Tag", feature: "services", nested: true },
808
+ // O pacote é catálogo como o serviço — aninhado nele, porque é um jeito de
809
+ // vender serviço e não um assunto novo no menu.
810
+ { id: "servicePackages", path: "/registry/service-packages", entity: servicePackageEntity, icon: "PackageCheck", feature: "services", nested: true },
309
811
  { id: "locations", path: "/registry/locations", entity: serviceLocationEntity, icon: "MapPin", feature: "services" },
812
+ // A SALA, aninhada na unidade: ela é onde se atende dentro do prédio, e uma
813
+ // linha solta no menu ao lado de «Unidades» repetiria a confusão que a 153
814
+ // acabou de desfazer.
310
815
  { id: "locationGroups", path: "/registry/location-groups", entity: locationGroupEntity, icon: "Map", feature: "services", nested: true },
311
816
  { id: "staff", path: "/registry/staff", entity: staffEntity, icon: "UserCog" },
817
+ { id: "teams", path: "/registry/teams", entity: teamEntity, icon: "Users", nested: true },
818
+ // Aninhada em Profissionais: a profissão é o vocabulário DELES, e uma linha
819
+ // solta no menu para uma lista de cinco palavras custa mais do que vale.
820
+ { id: "professions", path: "/registry/professions", entity: professionEntity, icon: "BriefcaseBusiness", nested: true },
312
821
  { id: "contacts", path: "/registry/contacts", entity: contactEntity, icon: "Contact" },
313
822
  { id: "suppliers", path: "/registry/suppliers", entity: supplierEntity, icon: "Building2" },
314
823
  { id: "partnerships", path: "/registry/partnerships", entity: partnershipEntity, icon: "Handshake" },
315
824
  { id: "equipment", path: "/registry/equipment", entity: equipmentEntity, icon: "Wrench" },
316
- { id: "origins", path: "/registry/origins", entity: originEntity, icon: "Globe" }
825
+ { id: "origins", path: "/registry/origins", entity: originEntity, icon: "Globe" },
826
+ // Só faz sentido em casa que atende pet — ver OPT_IN.
827
+ { id: "petSpecies", path: "/registry/pet-species", entity: petSpeciesEntity, icon: "PawPrint", feature: "pets" }
317
828
  ];
829
+ var OPT_IN = /* @__PURE__ */ new Set(["petSpecies"]);
318
830
  var serviceTabs = () => [
319
831
  { id: "services", label: serviceEntity.namePlural ?? serviceEntity.name, icon: Briefcase, onSelect: () => adminNavigateTo("/registry/services") },
320
832
  { id: "serviceCategories", label: serviceCategoryEntity.namePlural ?? serviceCategoryEntity.name, icon: Tag, onSelect: () => adminNavigateTo("/registry/categories") }
321
833
  ];
834
+ var staffTabs = () => [
835
+ { id: "staff", label: staffEntity.namePlural ?? staffEntity.name, icon: UserCog, onSelect: () => adminNavigateTo("/registry/staff") },
836
+ { id: "teams", label: teamEntity.namePlural ?? teamEntity.name, icon: Users, onSelect: () => adminNavigateTo("/registry/teams") }
837
+ ];
322
838
  var locationTabs = () => [
323
839
  { id: "locations", label: serviceLocationEntity.namePlural ?? serviceLocationEntity.name, icon: MapPin, onSelect: () => adminNavigateTo("/registry/locations") },
324
840
  { id: "locationGroups", label: locationGroupEntity.namePlural ?? locationGroupEntity.name, icon: Map, onSelect: () => adminNavigateTo("/registry/location-groups") }
@@ -326,11 +842,11 @@ var locationTabs = () => [
326
842
  function createRegistryPlugin(options) {
327
843
  const section = options?.navSection ?? "main";
328
844
  const base = options?.navPosition ?? 80;
329
- const active = SCREENS.filter((screen) => options?.modules?.[screen.id] !== false);
845
+ const active = SCREENS.filter((screen) => OPT_IN.has(screen.id) ? options?.modules?.[screen.id] === true : options?.modules?.[screen.id] !== false);
330
846
  const pages = active.map((screen, index) => {
331
847
  const entity = options?.entities?.[screen.id] ?? screen.entity;
332
848
  const feature = screen.feature;
333
- const grouped = screen.id === "services" || screen.id === "serviceCategories" ? { navTabs: serviceTabs, activeNavTab: screen.id } : screen.id === "locations" || screen.id === "locationGroups" ? { navTabs: locationTabs, activeNavTab: screen.id } : {};
849
+ const grouped = screen.id === "services" || screen.id === "serviceCategories" ? { navTabs: serviceTabs, activeNavTab: screen.id } : screen.id === "locations" || screen.id === "locationGroups" ? { navTabs: locationTabs, activeNavTab: screen.id } : screen.id === "staff" || screen.id === "teams" ? { navTabs: staffTabs, activeNavTab: screen.id } : {};
334
850
  return {
335
851
  screen,
336
852
  entity,
@@ -369,6 +885,6 @@ function createRegistryPlugin(options) {
369
885
  };
370
886
  }
371
887
 
372
- export { contactEntity, createRegistryPlugin, equipmentEntity, locationGroupEntity, originEntity, partnershipEntity, serviceCategoryEntity, serviceEntity, serviceLocationEntity, staffEntity, supplierEntity };
888
+ export { contactEntity, createRegistryPlugin, equipmentEntity, locationGroupEntity, originEntity, partnershipEntity, petSpeciesEntity, professionEntity, serviceCategoryEntity, serviceEntity, serviceLocationEntity, servicePackageEntity, staffEntity, supplierEntity, teamEntity };
373
889
  //# sourceMappingURL=index.js.map
374
890
  //# sourceMappingURL=index.js.map