diff --git a/bruno/propify/DataImport/Auth Bruno.bru b/bruno/propify/DataImport/Auth Bruno.bru index aec34d0..d24f723 100644 --- a/bruno/propify/DataImport/Auth Bruno.bru +++ b/bruno/propify/DataImport/Auth Bruno.bru @@ -1,7 +1,7 @@ meta { name: Auth Bruno type: http - seq: 2 + seq: 1 } post { diff --git a/bruno/propify/DataImport/Upload Organizations.bru b/bruno/propify/DataImport/Upload Organizations.bru new file mode 100644 index 0000000..2934ebb --- /dev/null +++ b/bruno/propify/DataImport/Upload Organizations.bru @@ -0,0 +1,39 @@ +meta { + name: Upload Organizations + type: http + seq: 3 +} + +post { + url: {{API_BASE_URL}}/api/{{API_VERSION}}/data-import/organizations + body: multipartForm + auth: bearer +} + +auth:bearer { + token: {{BEARER_TOKEN}} +} + +body:json { + { + "name": "Bungalow", + "street": "Hebbelstraße", + "houseNumber": "30", + "zipCode": "55127", + "city": "Mainz", + "country": "DE", + "notes": "Lorem ipsum" + } +} + +body:multipart-form { + file: @file(/Users/muratoezkorkmaz/projects/misc/skamp/init_data/organizations-17911945-23.xlsx) +} + +body:file { + file: @file(/Users/murat/Pictures/IMG_0229.jpeg) @contentType(image/jpeg) +} + +settings { + encodeUrl: true +} diff --git a/bruno/propify/DataImport/Upload files.bru b/bruno/propify/DataImport/Upload Persons.bru similarity index 95% rename from bruno/propify/DataImport/Upload files.bru rename to bruno/propify/DataImport/Upload Persons.bru index 92023f3..2a42436 100644 --- a/bruno/propify/DataImport/Upload files.bru +++ b/bruno/propify/DataImport/Upload Persons.bru @@ -1,7 +1,7 @@ meta { - name: Upload files + name: Upload Persons type: http - seq: 1 + seq: 2 } post { diff --git a/init_data/organizations-17911945-23.xlsx b/init_data/organizations-17911945-23.xlsx new file mode 100644 index 0000000..96d24db Binary files /dev/null and b/init_data/organizations-17911945-23.xlsx differ diff --git a/src/main/java/de/iwomm/propify_api/controller/DataImportController.java b/src/main/java/de/iwomm/propify_api/controller/DataImportController.java index 612d340..2429394 100644 --- a/src/main/java/de/iwomm/propify_api/controller/DataImportController.java +++ b/src/main/java/de/iwomm/propify_api/controller/DataImportController.java @@ -1,5 +1,6 @@ package de.iwomm.propify_api.controller; +import de.iwomm.propify_api.service.OrganizationImportService; import de.iwomm.propify_api.service.PersonImportService; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -13,9 +14,12 @@ import java.io.IOException; public class DataImportController { private final PersonImportService personImportService; + private final OrganizationImportService organizationImportService; - public DataImportController(PersonImportService personImportService) { + public DataImportController(PersonImportService personImportService, + OrganizationImportService organizationImportService) { this.personImportService = personImportService; + this.organizationImportService = organizationImportService; } @PostMapping("/persons") @@ -39,4 +43,26 @@ public class DataImportController { .body("Unexpected error: " + e.getMessage()); } } + + @PostMapping("/organizations") + public ResponseEntity importOrganizations(@RequestParam("file") MultipartFile file) { + if (file.isEmpty()) { + return ResponseEntity.badRequest().body("Please upload a file"); + } + + if (!file.getOriginalFilename().endsWith(".xlsx")) { + return ResponseEntity.badRequest().body("Only .xlsx files are supported"); + } + + try { + OrganizationImportService.ImportResult result = organizationImportService.importOrganizationsFromExcel(file); + return ResponseEntity.ok(result); + } catch (IOException e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Error processing file: " + e.getMessage()); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body("Unexpected error: " + e.getMessage()); + } + } } diff --git a/src/main/java/de/iwomm/propify_api/dto/OrganizationImportDTO.java b/src/main/java/de/iwomm/propify_api/dto/OrganizationImportDTO.java new file mode 100644 index 0000000..2b9e9e4 --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/dto/OrganizationImportDTO.java @@ -0,0 +1,84 @@ +package de.iwomm.propify_api.dto; + +import com.alibaba.excel.annotation.ExcelProperty; + +public class OrganizationImportDTO { + + @ExcelProperty(index = 0, value = "Organisation - Name") + private String name; + + @ExcelProperty(index = 1, value = "Organisation - Branche Intern") + private String industry; + + @ExcelProperty(index = 2, value = "Organisation - Label") + private String labels; + + @ExcelProperty(index = 3, value = "Organisation - Abgeschlossene Deals") + private String completedDeals; + + @ExcelProperty(index = 4, value = "Organisation - Offene Deals") + private String openDeals; + + @ExcelProperty(index = 5, value = "Organisation - Datum nächste Aktivität") + private String nextActivityDate; + + @ExcelProperty(index = 6, value = "Organisation - Besitzer") + private String owner; + + // Getters and Setters + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getIndustry() { + return industry; + } + + public void setIndustry(String industry) { + this.industry = industry; + } + + public String getLabels() { + return labels; + } + + public void setLabels(String labels) { + this.labels = labels; + } + + public String getCompletedDeals() { + return completedDeals; + } + + public void setCompletedDeals(String completedDeals) { + this.completedDeals = completedDeals; + } + + public String getOpenDeals() { + return openDeals; + } + + public void setOpenDeals(String openDeals) { + this.openDeals = openDeals; + } + + public String getNextActivityDate() { + return nextActivityDate; + } + + public void setNextActivityDate(String nextActivityDate) { + this.nextActivityDate = nextActivityDate; + } + + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } +} diff --git a/src/main/java/de/iwomm/propify_api/entity/Organization.java b/src/main/java/de/iwomm/propify_api/entity/Organization.java index 4162215..6a1bf7a 100644 --- a/src/main/java/de/iwomm/propify_api/entity/Organization.java +++ b/src/main/java/de/iwomm/propify_api/entity/Organization.java @@ -18,6 +18,9 @@ public class Organization { @JoinColumn(name = "industry_id") private Industry industry; + @Column + private String owner; + @Column(nullable = false) private LocalDateTime createdAt; @@ -50,6 +53,14 @@ public class Organization { this.industry = industry; } + public String getOwner() { + return owner; + } + + public void setOwner(String owner) { + this.owner = owner; + } + public LocalDateTime getCreatedAt() { return createdAt; } diff --git a/src/main/java/de/iwomm/propify_api/entity/Person.java b/src/main/java/de/iwomm/propify_api/entity/Person.java index 4f8138a..89182be 100644 --- a/src/main/java/de/iwomm/propify_api/entity/Person.java +++ b/src/main/java/de/iwomm/propify_api/entity/Person.java @@ -1,4 +1,199 @@ package de.iwomm.propify_api.entity; -import jakarta.persistence.Entity; +import jakarta.persistence.*; +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; +@Entity +public class Person { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + @Column(nullable = false) + private UUID id; + + @Column + private String firstName; + + @Column + private String lastName; + + @Column + private String emailOffice; + + @Column + private String emailPrivate; + + @Column + private String emailOther; + + @Column + private String phoneOfficeCountryCode; + + @Column + private String phoneOfficeAreaCode; + + @Column + private String phoneOfficeNumber; + + @Column + private String phonePrivateCountryCode; + + @Column + private String phonePrivateAreaCode; + + @Column + private String phonePrivateNumber; + + @Column + private String phoneMobile; + + @Column(unique = true) + private String keycloakId; + + @Column(nullable = false) + private LocalDateTime createdAt; + + @PrePersist + protected void onCreate() { + this.createdAt = LocalDateTime.now(); + } + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}) + @JoinTable( + name = "person_labels", + joinColumns = @JoinColumn(name = "person_id"), + inverseJoinColumns = @JoinColumn(name = "label_id"), + foreignKey = @ForeignKey(name = "fk_person_labels_person", foreignKeyDefinition = "FOREIGN KEY (person_id) REFERENCES person(id) ON DELETE CASCADE") + ) + private Set labels = new HashSet<>(); + + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmailOffice() { + return emailOffice; + } + + public void setEmailOffice(String emailOffice) { + this.emailOffice = emailOffice; + } + + public String getEmailPrivate() { + return emailPrivate; + } + + public void setEmailPrivate(String emailPrivate) { + this.emailPrivate = emailPrivate; + } + + public String getEmailOther() { + return emailOther; + } + + public void setEmailOther(String emailOther) { + this.emailOther = emailOther; + } + + public String getPhoneOfficeCountryCode() { + return phoneOfficeCountryCode; + } + + public void setPhoneOfficeCountryCode(String phoneOfficeCountryCode) { + this.phoneOfficeCountryCode = phoneOfficeCountryCode; + } + + public String getPhoneOfficeAreaCode() { + return phoneOfficeAreaCode; + } + + public void setPhoneOfficeAreaCode(String phoneOfficeAreaCode) { + this.phoneOfficeAreaCode = phoneOfficeAreaCode; + } + + public String getPhoneOfficeNumber() { + return phoneOfficeNumber; + } + + public void setPhoneOfficeNumber(String phoneOfficeNumber) { + this.phoneOfficeNumber = phoneOfficeNumber; + } + + public String getPhonePrivateCountryCode() { + return phonePrivateCountryCode; + } + + public void setPhonePrivateCountryCode(String phonePrivateCountryCode) { + this.phonePrivateCountryCode = phonePrivateCountryCode; + } + + public String getPhonePrivateAreaCode() { + return phonePrivateAreaCode; + } + + public void setPhonePrivateAreaCode(String phonePrivateAreaCode) { + this.phonePrivateAreaCode = phonePrivateAreaCode; + } + + public String getPhonePrivateNumber() { + return phonePrivateNumber; + } + + public void setPhonePrivateNumber(String phonePrivateNumber) { + this.phonePrivateNumber = phonePrivateNumber; + } + + public String getPhoneMobile() { + return phoneMobile; + } + + public void setPhoneMobile(String phoneMobile) { + this.phoneMobile = phoneMobile; + } + + public String getKeycloakId() { + return keycloakId; + } + + public void setKeycloakId(String keycloakId) { + this.keycloakId = keycloakId; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public Set getLabels() { + return labels; + } + + public void setLabels(Set labels) { + this.labels = labels; + } +} diff --git a/src/main/java/de/iwomm/propify_api/service/OrganizationImportService.java b/src/main/java/de/iwomm/propify_api/service/OrganizationImportService.java new file mode 100644 index 0000000..2dd2f8a --- /dev/null +++ b/src/main/java/de/iwomm/propify_api/service/OrganizationImportService.java @@ -0,0 +1,137 @@ +package de.iwomm.propify_api.service; + +import com.alibaba.excel.EasyExcel; +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.read.listener.ReadListener; +import de.iwomm.propify_api.dto.OrganizationImportDTO; +import de.iwomm.propify_api.entity.Industry; +import de.iwomm.propify_api.entity.Organization; +import de.iwomm.propify_api.repository.IndustryRepository; +import de.iwomm.propify_api.repository.OrganizationRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.*; + +@Service +public class OrganizationImportService { + + private final OrganizationRepository organizationRepository; + private final IndustryRepository industryRepository; + + public OrganizationImportService(OrganizationRepository organizationRepository, + IndustryRepository industryRepository) { + this.organizationRepository = organizationRepository; + this.industryRepository = industryRepository; + } + + @Transactional + public ImportResult importOrganizationsFromExcel(MultipartFile file) throws IOException { + ImportResult result = new ImportResult(); + + EasyExcel.read(file.getInputStream(), OrganizationImportDTO.class, new ReadListener() { + private int currentRow = 1; // Start at 1 to account for header + + @Override + public void invoke(OrganizationImportDTO data, AnalysisContext context) { + currentRow++; + try { + Organization organization = convertToOrganization(data); + organizationRepository.save(organization); + result.incrementSuccess(); + } catch (Exception e) { + result.addError(currentRow, e.getMessage()); + } + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // Called after all data has been analyzed + } + }).sheet().doRead(); + + return result; + } + + private Organization convertToOrganization(OrganizationImportDTO dto) { + Organization organization = new Organization(); + + // Name + if (dto.getName() != null && !dto.getName().trim().isEmpty()) { + organization.setName(dto.getName().trim()); + } else { + throw new IllegalArgumentException("Name is required"); + } + + // Industry (Branche) - create or lookup + if (dto.getIndustry() != null && !dto.getIndustry().trim().isEmpty()) { + String industryName = dto.getIndustry().trim(); + Industry industry = industryRepository.findByName(industryName) + .orElseGet(() -> { + Industry newIndustry = new Industry(); + newIndustry.setName(industryName); + return industryRepository.save(newIndustry); + }); + organization.setIndustry(industry); + } + + // Owner + organization.setOwner(trimOrNull(dto.getOwner())); + + return organization; + } + + private String trimOrNull(String value) { + if (value == null || value.trim().isEmpty()) { + return null; + } + return value.trim(); + } + + public static class ImportResult { + private int successCount = 0; + private int errorCount = 0; + private List errors = new ArrayList<>(); + + public void incrementSuccess() { + successCount++; + } + + public void addError(int row, String message) { + errorCount++; + errors.add(new ImportError(row, message)); + } + + public int getSuccessCount() { + return successCount; + } + + public int getErrorCount() { + return errorCount; + } + + public List getErrors() { + return errors; + } + } + + public static class ImportError { + private int row; + private String message; + + public ImportError(int row, String message) { + this.row = row; + this.message = message; + } + + public int getRow() { + return row; + } + + public String getMessage() { + return message; + } + } +}