@hed-hog/contact 0.0.304 → 0.0.306

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 (44) hide show
  1. package/README.md +225 -17
  2. package/dist/person/dto/account.dto.d.ts +5 -0
  3. package/dist/person/dto/account.dto.d.ts.map +1 -1
  4. package/dist/person/dto/account.dto.js +29 -0
  5. package/dist/person/dto/account.dto.js.map +1 -1
  6. package/dist/person/dto/import-preview.dto.d.ts +7 -0
  7. package/dist/person/dto/import-preview.dto.d.ts.map +1 -0
  8. package/dist/person/dto/import-preview.dto.js +7 -0
  9. package/dist/person/dto/import-preview.dto.js.map +1 -0
  10. package/dist/person/dto/import.dto.d.ts +15 -0
  11. package/dist/person/dto/import.dto.d.ts.map +1 -0
  12. package/dist/person/dto/import.dto.js +51 -0
  13. package/dist/person/dto/import.dto.js.map +1 -0
  14. package/dist/person/person.controller.d.ts +14 -0
  15. package/dist/person/person.controller.d.ts.map +1 -1
  16. package/dist/person/person.controller.js +53 -0
  17. package/dist/person/person.controller.js.map +1 -1
  18. package/dist/person/person.service.d.ts +19 -0
  19. package/dist/person/person.service.d.ts.map +1 -1
  20. package/dist/person/person.service.js +481 -67
  21. package/dist/person/person.service.js.map +1 -1
  22. package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
  23. package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
  24. package/hedhog/data/route.yaml +6 -0
  25. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +2242 -484
  26. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +51 -0
  27. package/hedhog/frontend/app/accounts/page.tsx.ejs +181 -16
  28. package/hedhog/frontend/app/contact-type/page.tsx.ejs +223 -29
  29. package/hedhog/frontend/app/document-type/page.tsx.ejs +248 -37
  30. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +129 -19
  31. package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +78 -212
  32. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +760 -178
  33. package/hedhog/frontend/app/person/_components/person-import-sheet.tsx.ejs +1120 -0
  34. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +171 -4
  35. package/hedhog/frontend/app/person/page.tsx.ejs +17 -0
  36. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +160 -35
  37. package/hedhog/frontend/messages/en.json +104 -2
  38. package/hedhog/frontend/messages/pt.json +111 -9
  39. package/package.json +4 -4
  40. package/src/person/dto/account.dto.ts +31 -0
  41. package/src/person/dto/import-preview.dto.ts +6 -0
  42. package/src/person/dto/import.dto.ts +61 -0
  43. package/src/person/person.controller.ts +74 -12
  44. package/src/person/person.service.ts +615 -68
@@ -230,7 +230,80 @@
230
230
  "interactionError": "Failed to register interaction",
231
231
  "followupDate": "Follow-up Date",
232
232
  "followupSuccess": "Follow-up scheduled successfully",
233
- "followupError": "Failed to schedule follow-up"
233
+ "followupError": "Failed to schedule follow-up",
234
+ "importLeads": "Import Leads",
235
+ "importStepUpload": "Upload",
236
+ "importStepPreview": "Preview",
237
+ "importStepMapping": "Mapping",
238
+ "importStepConfirm": "Confirmation",
239
+ "importStepResult": "Result",
240
+ "importSheetTitle": "Import Leads via CSV",
241
+ "importSheetDescription": "Upload a CSV file, map the columns and import contacts into the CRM.",
242
+ "importDropzoneLabel": "Click or drag a CSV file here",
243
+ "importDropzoneHint": "Accepted format: .csv — max. 5,000 rows",
244
+ "importDropzoneChange": "Change file",
245
+ "importFileSelected": "File selected",
246
+ "importPreviewTitle": "File Preview",
247
+ "importPreviewDescription": "Showing the first rows of the uploaded file. Check whether the data looks correct before proceeding.",
248
+ "importTotalEstimated": "Estimated rows",
249
+ "importColumnsDetected": "Columns detected",
250
+ "importMappingTitle": "Column Mapping",
251
+ "importMappingDescription": "Map each CSV column to the corresponding CRM field. Choose \"Ignore\" to skip a column.",
252
+ "importMappingColumnLabel": "CSV Column",
253
+ "importMappingFieldLabel": "CRM Field",
254
+ "importMappingIgnore": "Ignore",
255
+ "importMappingDuplicateWarning": "The field {field} is mapped more than once.",
256
+ "importMappingNameRequired": "You must map at least one column to the \"Name\" field.",
257
+ "importFieldName": "Name",
258
+ "importFieldType": "Type",
259
+ "importFieldStatus": "Status",
260
+ "importFieldEmail": "Email",
261
+ "importFieldPhone": "Phone",
262
+ "importFieldMobile": "Mobile",
263
+ "importFieldCpf": "CPF",
264
+ "importFieldCnpj": "CNPJ",
265
+ "importFieldJobTitle": "Job Title",
266
+ "importFieldCompanyName": "Company (employer)",
267
+ "importFieldTradeName": "Trade Name",
268
+ "importFieldWebsite": "Website",
269
+ "importFieldNotes": "Notes",
270
+ "importFieldSource": "Source",
271
+ "importFieldAddressStreet": "Address — Street",
272
+ "importFieldAddressCity": "Address — City",
273
+ "importFieldAddressState": "Address — State",
274
+ "importFieldAddressZip": "Address — ZIP Code",
275
+ "importFieldAddressCountry": "Address — Country",
276
+ "importConfirmTitle": "Review and Confirm",
277
+ "importConfirmDescription": "Review the import settings before proceeding.",
278
+ "importConfirmFile": "File",
279
+ "importConfirmRows": "Estimated rows",
280
+ "importConfirmMappedFields": "Mapped fields",
281
+ "importConfirmCompanyLabel": "Associate all contacts with a company (optional)",
282
+ "importConfirmCompanyPlaceholder": "Search for a company...",
283
+ "importConfirmNoCompany": "No company (import as individual contacts)",
284
+ "importCreateCompanyTitle": "New Company",
285
+ "importCreateCompanyDescription": "Quickly create a company to associate with this import.",
286
+ "importCreateCompanyName": "Company name",
287
+ "importCreateCompanyNamePlaceholder": "e.g. Acme Corp",
288
+ "importCreateCompanyTradeName": "Trade name (optional)",
289
+ "importCreateCompanyTradeNamePlaceholder": "e.g. Acme",
290
+ "importCreateCompanySave": "Create company",
291
+ "importResultTitle": "Import Complete",
292
+ "importResultDescription": "Import finished. Review the results below.",
293
+ "importResultImported": "Imported",
294
+ "importResultSkipped": "Skipped",
295
+ "importResultErrors": "Errors",
296
+ "importResultErrorsLabel": "Error details",
297
+ "importResultRow": "Row {row}",
298
+ "importResultSuccess": "Leads imported successfully!",
299
+ "importResultPartial": "{imported} leads imported with {errors} errors.",
300
+ "importBack": "Back",
301
+ "importNext": "Next",
302
+ "importStart": "Import",
303
+ "importClose": "Close",
304
+ "importErrorFileRequired": "Select a CSV file to continue.",
305
+ "importErrorFileTooLarge": "File exceeds the 10 MB limit. Split the file and try again.",
306
+ "importErrorGeneric": "An error occurred during import. Try again."
234
307
  },
235
308
  "PersonFieldWithCreate": {
236
309
  "sheet": {
@@ -1162,6 +1235,7 @@
1162
1235
  "edit": "Edit",
1163
1236
  "delete": "Delete",
1164
1237
  "cancel": "Cancel",
1238
+ "importContacts": "Import contacts",
1165
1239
  "deleting": "Deleting...",
1166
1240
  "searchPlaceholder": "Search accounts by name, trade name, email, or city...",
1167
1241
  "viewMode": "View",
@@ -1240,12 +1314,40 @@
1240
1314
  "locationTitle": "Location",
1241
1315
  "locationDescription": "City and state for quick context.",
1242
1316
  "additionalTitle": "Additional details",
1243
- "additionalDescription": "Complementary information and account metrics."
1317
+ "additionalDescription": "Complementary information and account metrics.",
1318
+ "contactsTitle": "Contacts",
1319
+ "contactsDescription": "Add email, phone, and other outreach channels.",
1320
+ "addressesTitle": "Addresses",
1321
+ "addressesDescription": "Manage headquarters, billing, or delivery locations.",
1322
+ "documentsTitle": "Documents",
1323
+ "documentsDescription": "Register company identifiers and legal records.",
1324
+ "collaboratorsTitle": "Collaborators",
1325
+ "collaboratorsDescription": "Manage people linked to this company."
1244
1326
  },
1245
1327
  "additional": {
1246
1328
  "show": "Expand",
1247
1329
  "hide": "Hide"
1248
1330
  },
1331
+ "collaborators": {
1332
+ "title": "Collaborators",
1333
+ "description": "People linked to this company through the employer field.",
1334
+ "empty": "No collaborators linked yet.",
1335
+ "saveFirst": "You can add collaborators before the first save.",
1336
+ "saveFirstHint": "Their person ids will be linked automatically when the company is saved.",
1337
+ "ready": "Company saved. You can now manage collaborators here.",
1338
+ "addAction": "Add collaborator",
1339
+ "editAction": "Edit collaborator",
1340
+ "editActionLabel": "Edit collaborator",
1341
+ "viewMode": "View mode",
1342
+ "viewModeCards": "Cards",
1343
+ "viewModeTable": "Table",
1344
+ "tableColName": "Name",
1345
+ "tableColJobTitle": "Job Title",
1346
+ "tableColStatus": "Status",
1347
+ "tableColEmail": "E-mail",
1348
+ "tableColPhone": "Phone",
1349
+ "tableColActions": "Actions"
1350
+ },
1249
1351
  "createSubmit": "Create Account",
1250
1352
  "updateSubmit": "Save Changes",
1251
1353
  "saving": "Saving...",
@@ -229,7 +229,80 @@
229
229
  "interactionError": "Falha ao registrar interação",
230
230
  "followupDate": "Data do Follow-up",
231
231
  "followupSuccess": "Follow-up agendado com sucesso",
232
- "followupError": "Falha ao agendar follow-up"
232
+ "followupError": "Falha ao agendar follow-up",
233
+ "importLeads": "Importar Leads",
234
+ "importStepUpload": "Upload",
235
+ "importStepPreview": "Prévia",
236
+ "importStepMapping": "Mapeamento",
237
+ "importStepConfirm": "Confirmação",
238
+ "importStepResult": "Resultado",
239
+ "importSheetTitle": "Importar Leads via CSV",
240
+ "importSheetDescription": "Faça upload de um arquivo CSV, mapeie as colunas e importe contatos para o CRM.",
241
+ "importDropzoneLabel": "Clique ou arraste um arquivo CSV aqui",
242
+ "importDropzoneHint": "Formato aceito: .csv — máx. 5.000 linhas",
243
+ "importDropzoneChange": "Trocar arquivo",
244
+ "importFileSelected": "Arquivo selecionado",
245
+ "importPreviewTitle": "Prévia do Arquivo",
246
+ "importPreviewDescription": "Exibindo as primeiras linhas do arquivo enviado. Verifique se os dados estão corretos antes de avançar.",
247
+ "importTotalEstimated": "Linhas estimadas",
248
+ "importColumnsDetected": "Colunas detectadas",
249
+ "importMappingTitle": "Mapeamento de Colunas",
250
+ "importMappingDescription": "Mapeie cada coluna do CSV para o campo correspondente do CRM. Escolha \"Ignorar\" para pular uma coluna.",
251
+ "importMappingColumnLabel": "Coluna CSV",
252
+ "importMappingFieldLabel": "Campo CRM",
253
+ "importMappingIgnore": "Ignorar",
254
+ "importMappingDuplicateWarning": "O campo {field} está mapeado mais de uma vez.",
255
+ "importMappingNameRequired": "Mapeie ao menos uma coluna para o campo \"Nome\".",
256
+ "importFieldName": "Nome",
257
+ "importFieldType": "Tipo",
258
+ "importFieldStatus": "Status",
259
+ "importFieldEmail": "E-mail",
260
+ "importFieldPhone": "Telefone",
261
+ "importFieldMobile": "Celular",
262
+ "importFieldCpf": "CPF",
263
+ "importFieldCnpj": "CNPJ",
264
+ "importFieldJobTitle": "Cargo",
265
+ "importFieldCompanyName": "Empresa (empregadora)",
266
+ "importFieldTradeName": "Nome Fantasia",
267
+ "importFieldWebsite": "Website",
268
+ "importFieldNotes": "Observações",
269
+ "importFieldSource": "Origem",
270
+ "importFieldAddressStreet": "Endereço — Rua",
271
+ "importFieldAddressCity": "Endereço — Cidade",
272
+ "importFieldAddressState": "Endereço — Estado",
273
+ "importFieldAddressZip": "Endereço — CEP",
274
+ "importFieldAddressCountry": "Endereço — País",
275
+ "importConfirmTitle": "Revisar e Confirmar",
276
+ "importConfirmDescription": "Revise as configurações de importação antes de prosseguir.",
277
+ "importConfirmFile": "Arquivo",
278
+ "importConfirmRows": "Linhas estimadas",
279
+ "importConfirmMappedFields": "Campos mapeados",
280
+ "importConfirmCompanyLabel": "Associar todos os contatos a uma empresa (opcional)",
281
+ "importConfirmCompanyPlaceholder": "Buscar empresa...",
282
+ "importConfirmNoCompany": "Sem empresa (importar como contatos individuais)",
283
+ "importCreateCompanyTitle": "Nova empresa",
284
+ "importCreateCompanyDescription": "Crie rapidamente uma empresa para associar a esta importação.",
285
+ "importCreateCompanyName": "Nome da empresa",
286
+ "importCreateCompanyNamePlaceholder": "ex: Acme Ltda",
287
+ "importCreateCompanyTradeName": "Nome fantasia (opcional)",
288
+ "importCreateCompanyTradeNamePlaceholder": "ex: Acme",
289
+ "importCreateCompanySave": "Criar empresa",
290
+ "importResultTitle": "Importação Concluída",
291
+ "importResultDescription": "A importação foi finalizada. Veja os resultados abaixo.",
292
+ "importResultImported": "Importados",
293
+ "importResultSkipped": "Ignorados",
294
+ "importResultErrors": "Erros",
295
+ "importResultErrorsLabel": "Detalhes dos erros",
296
+ "importResultRow": "Linha {row}",
297
+ "importResultSuccess": "Leads importados com sucesso!",
298
+ "importResultPartial": "{imported} leads importados com {errors} erros.",
299
+ "importBack": "Voltar",
300
+ "importNext": "Avançar",
301
+ "importStart": "Importar",
302
+ "importClose": "Fechar",
303
+ "importErrorFileRequired": "Selecione um arquivo CSV para continuar.",
304
+ "importErrorFileTooLarge": "O arquivo excede o limite de 10 MB. Divida o arquivo e tente novamente.",
305
+ "importErrorGeneric": "Ocorreu um erro durante a importação. Tente novamente."
233
306
  },
234
307
  "PersonFieldWithCreate": {
235
308
  "sheet": {
@@ -1161,6 +1234,7 @@
1161
1234
  "edit": "Editar",
1162
1235
  "delete": "Excluir",
1163
1236
  "cancel": "Cancelar",
1237
+ "importContacts": "Importar contatos",
1164
1238
  "deleting": "Excluindo...",
1165
1239
  "searchPlaceholder": "Pesquisar contas por nome, razão social, email ou cidade...",
1166
1240
  "viewMode": "Visualização",
@@ -1214,7 +1288,7 @@
1214
1288
  "companyName": "Nome da empresa",
1215
1289
  "tradeName": "Nome fantasia",
1216
1290
  "status": "Status",
1217
- "lifecycleStage": "Estagio",
1291
+ "lifecycleStage": "Estágio",
1218
1292
  "email": "Email",
1219
1293
  "phone": "Telefone",
1220
1294
  "website": "Site",
@@ -1223,28 +1297,56 @@
1223
1297
  "employeeCount": "Colaboradores",
1224
1298
  "city": "Cidade",
1225
1299
  "state": "Estado",
1226
- "owner": "Responsavel",
1300
+ "owner": "Responsável",
1227
1301
  "summary": {
1228
- "newCompany": "Nova conta em preparacao",
1302
+ "newCompany": "Nova conta em preparação",
1229
1303
  "createHint": "Preencha os dados essenciais para cadastrar a conta rapidamente.",
1230
1304
  "editHint": "Revise os dados principais e atualize o relacionamento comercial."
1231
1305
  },
1232
1306
  "sections": {
1233
- "identityTitle": "Identificacao",
1234
- "identityDescription": "Como a conta sera reconhecida no CRM.",
1307
+ "identityTitle": "Identificação",
1308
+ "identityDescription": "Como a conta será reconhecida no CRM.",
1235
1309
  "relationshipTitle": "Relacionamento",
1236
1310
  "relationshipDescription": "Status, etapa e ownership da conta.",
1237
1311
  "contactTitle": "Contato",
1238
1312
  "contactDescription": "Canais principais para abordagem comercial.",
1239
- "locationTitle": "Localizacao",
1240
- "locationDescription": "Cidade e estado para contexto rapido.",
1313
+ "locationTitle": "Localização",
1314
+ "locationDescription": "Cidade e estado para contexto rápido.",
1241
1315
  "additionalTitle": "Detalhes adicionais",
1242
- "additionalDescription": "Informacoes complementares e metricas da conta."
1316
+ "additionalDescription": "Informações complementares e métricas da conta.",
1317
+ "contactsTitle": "Contatos",
1318
+ "contactsDescription": "Adicione email, telefone e outros canais da empresa.",
1319
+ "addressesTitle": "Endereços",
1320
+ "addressesDescription": "Gerencie sede, cobrança ou locais de entrega.",
1321
+ "documentsTitle": "Documentos",
1322
+ "documentsDescription": "Cadastre identificadores e registros legais da empresa.",
1323
+ "collaboratorsTitle": "Colaboradores",
1324
+ "collaboratorsDescription": "Gerencie as pessoas vinculadas a esta empresa."
1243
1325
  },
1244
1326
  "additional": {
1245
1327
  "show": "Expandir",
1246
1328
  "hide": "Ocultar"
1247
1329
  },
1330
+ "collaborators": {
1331
+ "title": "Colaboradores",
1332
+ "description": "Pessoas vinculadas a esta empresa pelo campo de empregador.",
1333
+ "empty": "Nenhum colaborador vinculado ainda.",
1334
+ "saveFirst": "Voce pode adicionar colaboradores antes do primeiro salvamento.",
1335
+ "saveFirstHint": "Os ids das pessoas serao vinculados automaticamente quando a empresa for salva.",
1336
+ "ready": "Empresa salva. Agora voce pode gerenciar os colaboradores aqui.",
1337
+ "addAction": "Adicionar colaborador",
1338
+ "editAction": "Editar colaborador",
1339
+ "editActionLabel": "Editar colaborador",
1340
+ "viewMode": "Modo de visualização",
1341
+ "viewModeCards": "Cards",
1342
+ "viewModeTable": "Tabela",
1343
+ "tableColName": "Nome",
1344
+ "tableColJobTitle": "Cargo",
1345
+ "tableColStatus": "Status",
1346
+ "tableColEmail": "E-mail",
1347
+ "tableColPhone": "Telefone",
1348
+ "tableColActions": "Ações"
1349
+ },
1248
1350
  "createSubmit": "Criar conta",
1249
1351
  "updateSubmit": "Salvar alteracoes",
1250
1352
  "saving": "Salvando...",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hed-hog/contact",
3
- "version": "0.0.304",
3
+ "version": "0.0.306",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "dependencies": {
@@ -9,12 +9,12 @@
9
9
  "@nestjs/core": "^11",
10
10
  "@nestjs/jwt": "^11",
11
11
  "@nestjs/mapped-types": "*",
12
+ "@hed-hog/core": "0.0.306",
13
+ "@hed-hog/address": "0.0.306",
12
14
  "@hed-hog/api-prisma": "0.0.6",
15
+ "@hed-hog/api": "0.0.6",
13
16
  "@hed-hog/api-locale": "0.0.14",
14
- "@hed-hog/address": "0.0.304",
15
17
  "@hed-hog/api-mail": "0.0.9",
16
- "@hed-hog/api": "0.0.6",
17
- "@hed-hog/core": "0.0.304",
18
18
  "@hed-hog/api-pagination": "0.0.7"
19
19
  },
20
20
  "exports": {
@@ -1,5 +1,6 @@
1
1
  import { Type } from 'class-transformer';
2
2
  import {
3
+ IsArray,
3
4
  IsEmail,
4
5
  IsIn,
5
6
  IsInt,
@@ -7,8 +8,14 @@ import {
7
8
  IsOptional,
8
9
  IsString,
9
10
  MaxLength,
11
+ ValidateNested,
10
12
  } from 'class-validator';
11
13
  import { PersonStatus } from './create.dto';
14
+ import {
15
+ UpdateAllAddressDTO,
16
+ UpdateAllContactDTO,
17
+ UpdateAllDocumentDTO,
18
+ } from './update.dto';
12
19
 
13
20
  export const ACCOUNT_LIFECYCLE_STAGES = [
14
21
  'prospect',
@@ -95,6 +102,30 @@ export class CreateAccountDTO {
95
102
  @IsString()
96
103
  @MaxLength(2)
97
104
  state?: string | null;
105
+
106
+ @IsOptional()
107
+ @IsArray()
108
+ @Type(() => Number)
109
+ @IsInt({ each: true })
110
+ collaborator_person_ids?: number[];
111
+
112
+ @IsOptional()
113
+ @IsArray()
114
+ @ValidateNested({ each: true })
115
+ @Type(() => UpdateAllContactDTO)
116
+ contacts?: UpdateAllContactDTO[];
117
+
118
+ @IsOptional()
119
+ @IsArray()
120
+ @ValidateNested({ each: true })
121
+ @Type(() => UpdateAllAddressDTO)
122
+ addresses?: UpdateAllAddressDTO[];
123
+
124
+ @IsOptional()
125
+ @IsArray()
126
+ @ValidateNested({ each: true })
127
+ @Type(() => UpdateAllDocumentDTO)
128
+ documents?: UpdateAllDocumentDTO[];
98
129
  }
99
130
 
100
131
  export class UpdateAccountDTO extends CreateAccountDTO {}
@@ -0,0 +1,6 @@
1
+ export class ImportPreviewResponseDTO {
2
+ fileName: string;
3
+ totalEstimated: number;
4
+ columns: string[];
5
+ preview: Record<string, string>[];
6
+ }
@@ -0,0 +1,61 @@
1
+ import { IsNumber, IsObject, IsOptional } from 'class-validator';
2
+
3
+ export class ImportPersonDTO {
4
+ @IsObject()
5
+ mapping: Record<string, string>;
6
+
7
+ @IsOptional()
8
+ @IsNumber()
9
+ company_id?: number;
10
+ }
11
+
12
+ export type CrmImportField =
13
+ | 'name'
14
+ | 'type'
15
+ | 'status'
16
+ | 'email'
17
+ | 'phone'
18
+ | 'mobile'
19
+ | 'cpf'
20
+ | 'cnpj'
21
+ | 'job_title'
22
+ | 'company_name'
23
+ | 'trade_name'
24
+ | 'website'
25
+ | 'notes'
26
+ | 'source'
27
+ | 'address_street'
28
+ | 'address_city'
29
+ | 'address_state'
30
+ | 'address_zip'
31
+ | 'address_country'
32
+ | '_ignore';
33
+
34
+ export const CRM_IMPORT_FIELDS: CrmImportField[] = [
35
+ 'name',
36
+ 'type',
37
+ 'status',
38
+ 'email',
39
+ 'phone',
40
+ 'mobile',
41
+ 'cpf',
42
+ 'cnpj',
43
+ 'job_title',
44
+ 'company_name',
45
+ 'trade_name',
46
+ 'website',
47
+ 'notes',
48
+ 'source',
49
+ 'address_street',
50
+ 'address_city',
51
+ 'address_state',
52
+ 'address_zip',
53
+ 'address_country',
54
+ '_ignore',
55
+ ];
56
+
57
+ export class ImportPersonResponseDTO {
58
+ imported: number;
59
+ skipped: number;
60
+ errors: Array<{ row: number; message: string }>;
61
+ }
@@ -2,19 +2,23 @@ import { DeleteDTO, Public, Role, User } from '@hed-hog/api';
2
2
  import { Locale } from '@hed-hog/api-locale';
3
3
  import { Pagination } from '@hed-hog/api-pagination';
4
4
  import {
5
- Body,
6
- Controller,
7
- Delete,
8
- Get,
9
- Inject,
10
- Param,
11
- ParseIntPipe,
12
- Patch,
13
- Post,
14
- Query,
15
- Res,
16
- forwardRef
5
+ BadRequestException,
6
+ Body,
7
+ Controller,
8
+ Delete,
9
+ Get,
10
+ Inject,
11
+ Param,
12
+ ParseIntPipe,
13
+ Patch,
14
+ Post,
15
+ Query,
16
+ Res,
17
+ UploadedFile,
18
+ UseInterceptors,
19
+ forwardRef
17
20
  } from '@nestjs/common';
21
+ import { FileInterceptor } from '@nestjs/platform-express';
18
22
  import { Response } from 'express';
19
23
  import {
20
24
  AccountListQueryDTO,
@@ -37,6 +41,24 @@ import { UpdateLifecycleStageDTO } from './dto/update-lifecycle-stage.dto';
37
41
  import { UpdateAllPersonDTO } from './dto/update.dto';
38
42
  import { PersonService } from './person.service';
39
43
 
44
+ const CSV_FILE_FILTER = (
45
+ _req: any,
46
+ file: MulterFile,
47
+ cb: (error: Error | null, acceptFile: boolean) => void,
48
+ ) => {
49
+ const isAllowedMime = /text\/(csv|plain)/.test(file.mimetype) || file.mimetype === 'application/vnd.ms-excel';
50
+ const isAllowedExt = file.originalname.toLowerCase().endsWith('.csv');
51
+ if (!isAllowedMime && !isAllowedExt) {
52
+ return cb(new BadRequestException('Only CSV files are allowed'), false);
53
+ }
54
+ cb(null, true);
55
+ };
56
+
57
+ const CSV_UPLOAD_OPTIONS = {
58
+ limits: { fileSize: 10 * 1024 * 1024 }, // 10 MB
59
+ fileFilter: CSV_FILE_FILTER,
60
+ };
61
+
40
62
  @Role()
41
63
  @Controller('person')
42
64
  export class PersonController {
@@ -196,6 +218,46 @@ export class PersonController {
196
218
  return this.personService.get(locale, id);
197
219
  }
198
220
 
221
+ @Post('import/preview')
222
+ @UseInterceptors(FileInterceptor('file', CSV_UPLOAD_OPTIONS))
223
+ async previewCsvImport(@UploadedFile() file: MulterFile) {
224
+ if (!file) {
225
+ throw new BadRequestException('No file uploaded');
226
+ }
227
+ return this.personService.previewCsvImport(file);
228
+ }
229
+
230
+ @Post('import')
231
+ @UseInterceptors(FileInterceptor('file', CSV_UPLOAD_OPTIONS))
232
+ async importFromCsv(
233
+ @UploadedFile() file: MulterFile,
234
+ @Body('mapping') mappingJson: string,
235
+ @Body('company_id') companyIdRaw: string,
236
+ @Locale() locale: string,
237
+ @User() user,
238
+ ) {
239
+ if (!file) {
240
+ throw new BadRequestException('No file uploaded');
241
+ }
242
+
243
+ let mapping: Record<string, string>;
244
+ try {
245
+ mapping = JSON.parse(mappingJson);
246
+ } catch {
247
+ throw new BadRequestException('Invalid mapping JSON');
248
+ }
249
+
250
+ const companyId = companyIdRaw ? Number(companyIdRaw) : undefined;
251
+
252
+ return this.personService.importFromCsv(
253
+ file,
254
+ mapping,
255
+ Number.isNaN(companyId) ? undefined : companyId,
256
+ locale,
257
+ Number(user?.id || 0),
258
+ );
259
+ }
260
+
199
261
  @Post()
200
262
  async create(@Body() data: CreateDTO, @Locale() locale: string) {
201
263
  return this.personService.create(data, locale);