220 lines
6.9 KiB
PHP
220 lines
6.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Import;
|
|
|
|
use App\DTOs\Import\BonusRowDto;
|
|
use App\DTOs\Import\EmployeeRowDto;
|
|
use App\DTOs\Import\ImportResultDto;
|
|
use App\DTOs\Import\ReportRowDto;
|
|
use App\DTOs\Import\VacationRowDto;
|
|
use App\Enums\ImportType;
|
|
use App\Models\Bonus;
|
|
use App\Models\DisciplinaryReport;
|
|
use App\Models\Employee;
|
|
use App\Models\Vacation;
|
|
use App\Services\Employee\EmployeeService;
|
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
|
|
|
class ImportService
|
|
{
|
|
public function __construct(
|
|
private readonly EmployeeImportTransformer $employeeTransformer,
|
|
private readonly VacationImportTransformer $vacationTransformer,
|
|
private readonly BonusImportTransformer $bonusTransformer,
|
|
private readonly ReportImportTransformer $reportTransformer,
|
|
private readonly EmployeeService $employeeService,
|
|
) {}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
public function readHeaders(string $path): array
|
|
{
|
|
$sheet = IOFactory::load($path)->getActiveSheet();
|
|
$headers = [];
|
|
|
|
foreach ($sheet->getRowIterator(1, 1) as $row) {
|
|
foreach ($row->getCellIterator() as $cell) {
|
|
$value = trim((string) $cell->getValue());
|
|
|
|
if ($value !== '') {
|
|
$headers[] = $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $headers;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function readRows(string $path, ?int $limit = null): array
|
|
{
|
|
$sheet = IOFactory::load($path)->getActiveSheet();
|
|
$headers = $this->readHeaders($path);
|
|
$rows = [];
|
|
$highestRow = $sheet->getHighestDataRow();
|
|
|
|
for ($rowIndex = 2; $rowIndex <= $highestRow; $rowIndex++) {
|
|
$rowData = [];
|
|
$isEmpty = true;
|
|
|
|
foreach ($headers as $columnIndex => $header) {
|
|
$columnLetter = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($columnIndex + 1);
|
|
$value = $sheet->getCell($columnLetter . $rowIndex)->getFormattedValue();
|
|
$rowData[$header] = is_string($value) ? trim($value) : $value;
|
|
|
|
if ($rowData[$header] !== null && $rowData[$header] !== '') {
|
|
$isEmpty = false;
|
|
}
|
|
}
|
|
|
|
if (! $isEmpty) {
|
|
$rows[] = $rowData;
|
|
}
|
|
|
|
if ($limit !== null && count($rows) >= $limit) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string|null> $columnMapping
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function mapRows(array $rows, array $columnMapping): array
|
|
{
|
|
return array_map(function (array $row) use ($columnMapping): array {
|
|
$mapped = [];
|
|
|
|
foreach ($columnMapping as $field => $header) {
|
|
if ($header === null || $header === '') {
|
|
continue;
|
|
}
|
|
|
|
$mapped[$field] = $row[$header] ?? null;
|
|
}
|
|
|
|
return $mapped;
|
|
}, $rows);
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $mappedRows
|
|
* @return array<int, array{row: int, errors: array<int, string>}>
|
|
*/
|
|
public function validatePreview(ImportType $type, array $mappedRows, int $limit = 10): array
|
|
{
|
|
$previewRows = array_slice($mappedRows, 0, $limit);
|
|
$results = [];
|
|
|
|
foreach ($previewRows as $index => $row) {
|
|
$results[] = [
|
|
'row' => $index + 2,
|
|
'errors' => $this->validateRow($type, $row),
|
|
];
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $mappedRows
|
|
*/
|
|
public function import(ImportType $type, array $mappedRows, ?int $userId = null): ImportResultDto
|
|
{
|
|
$imported = 0;
|
|
$skipped = 0;
|
|
$failed = 0;
|
|
$errors = [];
|
|
|
|
foreach ($mappedRows as $index => $row) {
|
|
$rowNumber = $index + 2;
|
|
$validationErrors = $this->validateRow($type, $row);
|
|
|
|
if ($validationErrors !== []) {
|
|
$failed++;
|
|
$errors[] = "Row {$rowNumber}: " . implode(' ', $validationErrors);
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($this->isDuplicate($type, $row)) {
|
|
$skipped++;
|
|
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$this->persistRow($type, $row, $userId);
|
|
$imported++;
|
|
} catch (\Throwable $exception) {
|
|
$failed++;
|
|
$errors[] = "Row {$rowNumber}: {$exception->getMessage()}";
|
|
}
|
|
}
|
|
|
|
return new ImportResultDto(
|
|
total: count($mappedRows),
|
|
imported: $imported,
|
|
skipped: $skipped,
|
|
failed: $failed,
|
|
errors: array_slice($errors, 0, 50),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $row
|
|
* @return array<int, string>
|
|
*/
|
|
private function validateRow(ImportType $type, array $row): array
|
|
{
|
|
return match ($type) {
|
|
ImportType::Employees => $this->employeeTransformer->validate(EmployeeRowDto::fromMappedRow($row)),
|
|
ImportType::Vacations => $this->vacationTransformer->validate(VacationRowDto::fromMappedRow($row)),
|
|
ImportType::Bonuses => $this->bonusTransformer->validate(BonusRowDto::fromMappedRow($row)),
|
|
ImportType::Reports => $this->reportTransformer->validate(ReportRowDto::fromMappedRow($row)),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $row
|
|
*/
|
|
private function isDuplicate(ImportType $type, array $row): bool
|
|
{
|
|
return match ($type) {
|
|
ImportType::Employees => $this->employeeTransformer->isDuplicate(EmployeeRowDto::fromMappedRow($row)),
|
|
ImportType::Vacations => $this->vacationTransformer->isDuplicate(VacationRowDto::fromMappedRow($row)),
|
|
ImportType::Bonuses => $this->bonusTransformer->isDuplicate(BonusRowDto::fromMappedRow($row)),
|
|
ImportType::Reports => $this->reportTransformer->isDuplicate(ReportRowDto::fromMappedRow($row)),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $row
|
|
*/
|
|
private function persistRow(ImportType $type, array $row, ?int $userId): void
|
|
{
|
|
match ($type) {
|
|
ImportType::Employees => $this->employeeService->create(
|
|
$this->employeeTransformer->toAttributes(EmployeeRowDto::fromMappedRow($row)),
|
|
),
|
|
ImportType::Vacations => Vacation::create(
|
|
$this->vacationTransformer->toAttributes(VacationRowDto::fromMappedRow($row)),
|
|
),
|
|
ImportType::Bonuses => Bonus::create(
|
|
$this->bonusTransformer->toAttributes(BonusRowDto::fromMappedRow($row)),
|
|
),
|
|
ImportType::Reports => DisciplinaryReport::create(
|
|
$this->reportTransformer->toAttributes(ReportRowDto::fromMappedRow($row), $userId),
|
|
),
|
|
};
|
|
}
|
|
}
|