stream('clienti-' . date('Y-m-d') . '.csv', self::CLIENT_COLUMNS, function ($out) { Client::orderBy('name')->chunk(500, function ($rows) use ($out) { foreach ($rows as $row) { fputcsv($out, [ $row->name, $row->phone, $row->phone_alt, $row->email, $row->company_name, $row->type, $row->status, $row->source, $row->marketing_channel, $row->discount_pct, $row->balance, $row->notes, ]); } }); }); } public function exportVehicles(): StreamedResponse { return $this->stream('automobile-' . date('Y-m-d') . '.csv', self::VEHICLE_COLUMNS, function ($out) { Vehicle::with('client:id,phone')->orderBy('plate')->chunk(500, function ($rows) use ($out) { foreach ($rows as $row) { fputcsv($out, [ $row->plate, $row->vin, $row->make, $row->model, $row->year, $row->engine, $row->gearbox, $row->fuel, $row->mileage, $row->color, $row->notes, $row->client?->phone, ]); } }); }); } /** * Import clients from CSV. Returns ['imported' => N, 'skipped' => M, 'errors' => [...]]. */ public function importClients(string $absolutePath): array { return $this->import($absolutePath, self::CLIENT_COLUMNS, function (array $row) { $phone = $row['phone'] ?? null; if (empty($phone) || empty($row['name'])) { return ['skip', 'phone & name required']; } $existing = Client::where('phone', $phone)->first(); if ($existing) { return ['skip', 'phone already exists']; } Client::create([ 'name' => $row['name'], 'phone' => $phone, 'phone_alt' => $row['phone_alt'] ?? null, 'email' => $row['email'] ?? null, 'company_name' => $row['company_name'] ?? null, 'type' => $row['type'] ?? 'individual', 'status' => $row['status'] ?? 'active', 'source' => $row['source'] ?? null, 'marketing_channel' => $row['marketing_channel'] ?? null, 'discount_pct' => (float) ($row['discount_pct'] ?? 0), 'balance' => (float) ($row['balance'] ?? 0), 'notes' => $row['notes'] ?? null, ]); return ['ok', null]; }); } public function importVehicles(string $absolutePath): array { return $this->import($absolutePath, self::VEHICLE_COLUMNS, function (array $row) { if (empty($row['plate'])) { return ['skip', 'plate required']; } $client = null; if (! empty($row['client_phone'])) { $client = Client::where('phone', $row['client_phone'])->first(); } if (! $client) { return ['skip', 'client_phone not found']; } if (Vehicle::where('plate', $row['plate'])->exists()) { return ['skip', 'plate already exists']; } Vehicle::create([ 'client_id' => $client->id, 'plate' => $row['plate'], 'vin' => $row['vin'] ?? null, // CSV header keeps the user-friendly "brand" name, but the column is `make`. 'make' => $row['brand'] ?? null, 'model' => $row['model'] ?? null, 'year' => (int) ($row['year'] ?? 0) ?: null, 'engine' => $row['engine'] ?? null, 'gearbox' => $row['gearbox'] ?? null, 'fuel' => $row['fuel'] ?? null, 'mileage' => (int) ($row['mileage'] ?? 0) ?: null, 'color' => $row['color'] ?? null, 'notes' => $row['notes'] ?? null, ]); return ['ok', null]; }); } private function stream(string $filename, array $columns, \Closure $writer): StreamedResponse { return response()->streamDownload(function () use ($columns, $writer) { $out = fopen('php://output', 'w'); // UTF-8 BOM so Excel opens with correct diacritics. fwrite($out, "\xEF\xBB\xBF"); fputcsv($out, $columns); $writer($out); fclose($out); }, $filename, ['Content-Type' => 'text/csv; charset=utf-8']); } private function import(string $absolutePath, array $columns, \Closure $rowHandler): array { if (! file_exists($absolutePath)) { return ['imported' => 0, 'skipped' => 0, 'errors' => ['file not found']]; } $h = fopen($absolutePath, 'r'); // Strip UTF-8 BOM if present. $bom = fread($h, 3); if ($bom !== "\xEF\xBB\xBF") { rewind($h); } $header = fgetcsv($h); if (! $header) { fclose($h); return ['imported' => 0, 'skipped' => 0, 'errors' => ['empty file']]; } $header = array_map('trim', $header); $imported = 0; $skipped = 0; $errors = []; $line = 1; DB::beginTransaction(); try { while (($cells = fgetcsv($h)) !== false) { $line++; if (count($cells) !== count($header)) { $errors[] = "L{$line}: column count mismatch"; $skipped++; continue; } $row = array_combine($header, array_map(fn ($c) => $c === '' ? null : $c, $cells)); [$status, $err] = $rowHandler($row); if ($status === 'ok') { $imported++; } else { $skipped++; if ($err) $errors[] = "L{$line}: {$err}"; } } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); $errors[] = 'fatal: ' . $e->getMessage(); } fclose($h); return ['imported' => $imported, 'skipped' => $skipped, 'errors' => array_slice($errors, 0, 50)]; } }