diff --git a/backend/src/Controller/EmployeeImportController.php b/backend/src/Controller/EmployeeImportController.php new file mode 100644 index 0000000..4222d69 --- /dev/null +++ b/backend/src/Controller/EmployeeImportController.php @@ -0,0 +1,166 @@ +tenant->getCompany(); + if (!$company instanceof Company) { + throw new BadRequestHttpException('Import bitte im Firmenkontext ausführen.'); + } + + $data = json_decode($request->getContent(), true) ?? []; + $rows = \is_array($data['rows'] ?? null) ? $data['rows'] : []; + $uniqueField = \in_array($data['uniqueField'] ?? null, self::UNIQUE_ALLOWED, true) ? $data['uniqueField'] : null; + + $created = 0; + $updated = 0; + $skipped = 0; + $errors = []; + + foreach ($rows as $i => $row) { + if (!\is_array($row)) { + continue; + } + $values = $this->extract($row); + + $employee = null; + if (null !== $uniqueField && '' !== (string) ($values[$uniqueField] ?? '')) { + $employee = $this->em->getRepository(Employee::class) + ->findOneBy(['company' => $company, $uniqueField => $values[$uniqueField]]); + } + + if (null === $employee) { + if ('' === (string) ($values['firstName'] ?? '') || '' === (string) ($values['lastName'] ?? '')) { + ++$skipped; + $errors[] = ['row' => $i + 1, 'message' => 'Vor- und Nachname nötig zum Anlegen.']; + continue; + } + $employee = (new Employee()) + ->setCompany($company) + ->setStatus('active') + ->setSlug($this->uniqueSlug($company, $values['firstName'], $values['lastName'])); + $this->apply($employee, $values, true); + $this->em->persist($employee); + ++$created; + } else { + $this->apply($employee, $values, false); + $employee->touch(); + ++$updated; + } + } + + $this->em->flush(); + + return new JsonResponse([ + 'created' => $created, + 'updated' => $updated, + 'skipped' => $skipped, + 'errors' => \array_slice($errors, 0, 50), + ]); + } + + /** + * @param array $row + * + * @return array + */ + private function extract(array $row): array + { + $out = []; + foreach ([...self::SCALAR, ...self::ADDRESS] as $key) { + if (\array_key_exists($key, $row)) { + $out[$key] = trim((string) $row[$key]); + } + } + + return $out; + } + + /** @param array $v */ + private function apply(Employee $e, array $v, bool $isNew): void + { + $setters = [ + 'salutation' => 'setSalutation', 'title' => 'setTitle', 'firstName' => 'setFirstName', + 'lastName' => 'setLastName', 'position' => 'setPosition', 'department' => 'setDepartment', + 'email' => 'setEmail', 'emailPrivate' => 'setEmailPrivate', 'phone' => 'setPhone', + 'mobile' => 'setMobile', 'fax' => 'setFax', 'phoneCentral' => 'setPhoneCentral', + 'website' => 'setWebsite', 'bio' => 'setBio', + ]; + foreach ($setters as $key => $setter) { + if (\array_key_exists($key, $v) && '' !== $v[$key]) { + $e->{$setter}($v[$key]); + } + } + + // Geschäftsadresse: vorhandene Werte beibehalten, neue überschreiben + $addr = $isNew ? [] : ($e->getAddressBusiness() ?? []); + $touched = false; + foreach (self::ADDRESS as $key) { + if (\array_key_exists($key, $v) && '' !== $v[$key]) { + $addr[$key] = $v[$key]; + $touched = true; + } + } + if ($touched) { + $e->setAddressBusiness($addr); + } + } + + private function uniqueSlug(Company $company, string $first, string $last): string + { + $base = $this->slugify($first.'-'.$last) ?: 'mitarbeiter'; + $repo = $this->em->getRepository(Employee::class); + $slug = $base; + $n = 1; + while (null !== $repo->findOneBy(['company' => $company, 'slug' => $slug])) { + $slug = $base.'-'.(++$n); + } + + return $slug; + } + + private function slugify(string $s): string + { + $s = strtolower(trim($s)); + $s = str_replace(['ä', 'ö', 'ü', 'ß'], ['ae', 'oe', 'ue', 'ss'], $s); + $s = preg_replace('/[^a-z0-9]+/', '-', $s) ?? ''; + + return trim($s, '-'); + } +} diff --git a/frontend/src/components/EmployeeImport.vue b/frontend/src/components/EmployeeImport.vue new file mode 100644 index 0000000..f558674 --- /dev/null +++ b/frontend/src/components/EmployeeImport.vue @@ -0,0 +1,286 @@ + + + + + diff --git a/frontend/src/views/EmployeesView.vue b/frontend/src/views/EmployeesView.vue index e14cdf4..3f4a9a6 100644 --- a/frontend/src/views/EmployeesView.vue +++ b/frontend/src/views/EmployeesView.vue @@ -7,6 +7,7 @@ import { useAuthStore } from '@/stores/auth' import Modal from '@/components/Modal.vue' import PhoneInput from '@/components/PhoneInput.vue' import CountrySelect from '@/components/CountrySelect.vue' +import EmployeeImport from '@/components/EmployeeImport.vue' const GROUP_LABEL: Record = { platform_admin: 'Plattform-Admin', reseller_admin: 'Reseller-Admin', @@ -142,6 +143,9 @@ async function removeLogin(e: Employee) { editing.value = employees.value.find((x) => x.id === e.id) ?? null } +// --- Import --- +const showImport = ref(false) + // --- Anlegen / Bearbeiten --- const showForm = ref(false) const saving = ref(false) @@ -341,7 +345,10 @@ onMounted(load)

Mitarbeiter

{{ portalMode ? 'Alle einloggbaren Mitarbeiter der Plattform' : 'Profile als Single Source of Truth für alle Kanäle' }}

- +
+ + +
@@ -550,12 +557,15 @@ onMounted(load)
+ +