*/ private array $networkCache = []; public function model(array $row) { $network = $this->resolveNetwork($row['ten_mang'] ?? null, $row['cidr'] ?? null); return new IpPool([ // uuid has no DB default and must be set on insert; excluded from upsertColumns() so it's never overwritten 'uuid' => (string) Str::uuid(), 'ip_address' => $row['ip_address'], 'module_network_id' => $network?->id, // ip_pools.cidr is NOT NULL; fall back to the network's own prefix (or 24) when the row omits it 'cidr' => $this->parsePrefixLength($row['cidr'] ?? null) ?? $this->parsePrefixLength($network?->network) ?? 24, 'gateway' => $row['gateway'] ?? null, 'netmask' => $row['subnet_mask'] ?? null, 'owner_type' => $row['owner_type'] ?? null, 'ip_role' => $row['ip_role'] ?? null, 'parent_ip' => $row['parent_ip'] ?? null, 'customer_name' => $row['ten_khach_hang'] ?? null, 'service_name' => $row['ten_dich_vu'] ?? null, 'allocated_at' => $this->parseDate($row['ngay_cap_phat'] ?? null), 'revoked_at' => $this->parseDate($row['ngay_thu_hoi'] ?? null), 'allocation_status' => $row['trang_thai'] ?? null, 'contact_email' => $row['contact_email'] ?? null, 'contact_cc' => $row['contact_cc'] ?? null, 'contact_phone' => $row['contact_phone'] ?? null, 'monthly_cost' => $row['monthly_cost'] ?: null, 'selling_price' => $row['selling_price'] ?: null, 'additional_info' => $row['thong_tin_bo_sung'] ?? null, 'last_email_type' => $row['last_email_type'] ?? null, 'last_email_at' => $this->parseDate($row['last_email_at'] ?? null), 'updated_by' => $row['updated_by'] ?? null, 'note' => $row['ghi_chu'] ?? null, ]); } public function rules(): array { $multiEmail = function (string $attribute, mixed $value, \Closure $fail) { foreach (preg_split('/[,;]+/', (string) $value) as $email) { $email = trim($email); if ($email !== '' && !filter_var($email, FILTER_VALIDATE_EMAIL)) { $fail("Email không hợp lệ: {$email}"); } } }; return [ 'ten_mang' => 'required|string|max:255', 'cidr' => 'nullable|string|max:45', 'ip_address' => 'required|ip', 'contact_email' => ['nullable', 'string', $multiEmail], 'contact_cc' => ['nullable', 'string', $multiEmail], 'monthly_cost' => 'nullable|numeric', 'selling_price' => 'nullable|numeric', ]; } /** * CSV dates come as "dd/mm/yyyy" or "dd/mm/yyyy H:i:s" (Vietnamese sheet export). */ private function parseDate(?string $value): ?string { $value = trim((string) $value); if ($value === '') { return null; } try { return Carbon::createFromFormat('d/m/Y H:i:s', $value)->toDateTimeString(); } catch (\Throwable) { // not a datetime, try date-only below } try { return Carbon::createFromFormat('d/m/Y', $value)->startOfDay()->toDateTimeString(); } catch (\Throwable) { return null; } } /** * CSV gives CIDR as "/22"; the ip_pools.cidr column only stores the prefix length. */ private function parsePrefixLength(?string $cidr): ?int { // "/22" or "111.111.111.0/22" -> 22 return preg_match('/(\d+)$/', (string) $cidr, $m) ? (int) $m[1] : null; } public function uniqueBy(): array { return ['ip_address', 'module_network_id']; } public function upsertColumns(): array { return [ 'cidr', 'gateway', 'netmask', 'owner_type', 'ip_role', 'parent_ip', 'customer_name', 'service_name', 'allocated_at', 'revoked_at', 'allocation_status', 'contact_email', 'contact_cc', 'contact_phone', 'monthly_cost', 'selling_price', 'additional_info', 'last_email_type', 'last_email_at', 'updated_by', 'note', ]; } private function resolveNetwork(?string $name, ?string $cidr): ?ModuleNetwork { if (!$name) { return null; } $key = $name . '|' . $cidr; if (isset($this->networkCache[$key])) { return $this->networkCache[$key]; } $network = ModuleNetwork::where('name', $name)->first() ?? ($cidr ? ModuleNetwork::where('network', $cidr)->first() : null) ?? ModuleNetwork::create(['name' => $name, 'network' => $cidr]); return $this->networkCache[$key] = $network; } }