$definitions */ $definitions = CustomFieldDefinition::query() ->whereIn('key', self::FIELD_KEYS) ->get() ->keyBy('key'); $missing = array_diff(self::FIELD_KEYS, $definitions->keys()->all()); if ($missing !== []) { throw new RuntimeException( 'Fehlende Custom-Field-Definitionen: '.implode(', ', $missing). '. Bitte den CustomFieldDefinitionSeeder ausführen.' ); } $importUser = $this->systemUser(); $rows = $this->readRows($path); if ($rows === []) { return new ImportResult(0, 0, 0, 0, []); } $header = array_map(static fn (string $h): string => trim($h), array_shift($rows)); /** @var array $index header name → column offset */ $index = array_flip($header); $created = 0; $updated = 0; $skipped = 0; /** @var array $errors */ $errors = []; foreach ($rows as $offset => $row) { $rowNumber = (int) $offset + 2; // +1 for header, +1 for 1-based $clientNumber = null; try { $clientNumber = $this->value($row, $index, 'Mandantennr.'); if ($clientNumber === null) { $errors[] = ['row' => $rowNumber, 'client_number' => null, 'message' => 'Keine Mandantennummer.']; continue; } $mapped = $this->mapFields($row, $index); $client = Client::query()->where('client_number', $clientNumber)->first(); if ($client !== null) { if (! $this->isOverwrite($row, $index)) { $skipped++; continue; } foreach (self::FIELD_KEYS as $key) { if ($mapped[$key] === null) { continue; // skip blanks — never wipe existing data } $this->updateValue->handle($client, $definitions[$key], $mapped[$key], $importUser); } $updated++; continue; } $client = Client::create([ 'client_number' => $clientNumber, 'name' => $this->value($row, $index, 'Mandant') ?? $clientNumber, ]); foreach (self::FIELD_KEYS as $key) { if ($mapped[$key] === null) { continue; } CustomFieldValue::create([ 'client_id' => $client->id, 'custom_field_definition_id' => $definitions[$key]->id, 'value' => $mapped[$key], 'author_id' => $importUser->id, ]); } $created++; } catch (Throwable $e) { $errors[] = ['row' => $rowNumber, 'client_number' => $clientNumber, 'message' => $e->getMessage()]; } } return new ImportResult(count($rows), $created, $updated, $skipped, $errors); } /** * Read the CSV into numeric record arrays (no header offset) so ragged data * rows — far shorter than the ~66-column header — never throw. Decodes the * legacy ISO-8859-1 encoding to UTF-8 and strips a UTF-8 BOM if present. * * @return array> */ private function readRows(string $path): array { $contents = (string) file_get_contents($path); $contents = preg_replace('/^\xEF\xBB\xBF/', '', $contents) ?? $contents; if (! mb_check_encoding($contents, 'UTF-8')) { $contents = mb_convert_encoding($contents, 'UTF-8', 'Windows-1252'); } $csv = Reader::createFromString($contents); $csv->setDelimiter(';'); return array_values(iterator_to_array($csv->getRecords(), false)); } /** * Map the four target custom fields from a row. Anschrift 1–8 are joined * with newlines, blank parts dropped. * * @param array $row * @param array $index * @return array */ private function mapFields(array $row, array $index): array { $address = []; for ($n = 1; $n <= 8; $n++) { $part = $this->value($row, $index, "Anschrift {$n}"); if ($part !== null) { $address[] = $part; } } return [ 'telefonnummer' => $this->value($row, $index, 'Telefon'), 'e_mail' => $this->value($row, $index, 'E-Mail'), 'ansprechpartner' => $this->value($row, $index, 'Ansprechpartner 1'), 'anschrift' => $address === [] ? null : implode("\n", $address), ]; } /** * @param array $row * @param array $index */ private function isOverwrite(array $row, array $index): bool { $flag = $this->value($row, $index, 'Ueberschreiben'); return $flag !== null && mb_strtolower($flag) === self::OVERWRITE_TRIGGER; } /** * Resolve a cell by header name (bounds-safe for ragged rows), trim it, * strip a single leading Excel apostrophe, and normalise empty → null. * * @param array $row * @param array $index */ private function value(array $row, array $index, string $name): ?string { if (! array_key_exists($name, $index)) { return null; } $raw = $row[$index[$name]] ?? ''; $raw = trim($raw); if ($raw !== '' && $raw[0] === "'") { $raw = trim(substr($raw, 1)); } return $raw === '' ? null : $raw; } private function systemUser(): User { return User::firstOrCreate( ['email' => self::IMPORT_USER_EMAIL], [ 'name' => self::IMPORT_USER_NAME, 'password' => Hash::make(Str::random(40)), 'role' => 'staff', ], ); } }