From a5e342f0f5149d40c644d69472e42a2afaeb3a35 Mon Sep 17 00:00:00 2001 From: baodev0 Date: Thu, 6 Aug 2026 11:13:42 +0700 Subject: [PATCH] update ippool --- .../Api/Admin/IpPool/IpPoolController.php | 57 +++++++++- app/Imports/IpPoolImport.php | 104 ++++++++++++++++++ app/Models/IpPool.php | 31 ++++++ ...0_add_import_columns_to_ip_pools_table.php | 51 +++++++++ ...430_add_unique_index_to_ip_pools_table.php | 28 +++++ routes/api.php | 1 + 6 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 app/Imports/IpPoolImport.php create mode 100644 database/migrations/2026_08_06_102950_add_import_columns_to_ip_pools_table.php create mode 100644 database/migrations/2026_08_06_104430_add_unique_index_to_ip_pools_table.php diff --git a/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php b/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php index 1376b2a..3128b9c 100644 --- a/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php +++ b/app/Http/Controllers/Api/Admin/IpPool/IpPoolController.php @@ -3,15 +3,27 @@ namespace App\Http\Controllers\Api\Admin\IpPool; use App\Http\Controllers\Controller; +use App\Imports\IpPoolImport; use App\Models\IpPool; use App\Models\ModuleIpAddress; use App\Models\ModuleNetwork; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; +use Maatwebsite\Excel\Facades\Excel; +use Maatwebsite\Excel\Validators\ValidationException; class IpPoolController extends Controller { + private const EDITABLE_FIELDS = [ + 'gateway', 'netmask', 'service_id', 'status', + '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', + ]; + public function index(Request $request): JsonResponse { $perPage = min((int) $request->get('per_page', 15), 500); @@ -116,6 +128,9 @@ class IpPoolController extends Controller $request->validate([ 'ip_address' => 'required|string', 'module_network_id' => 'required|integer|exists:module_network,id', + 'contact_email' => 'nullable|email', + 'monthly_cost' => 'nullable|numeric', + 'selling_price' => 'nullable|numeric', ]); $networkId = (int) $request->module_network_id; @@ -154,6 +169,8 @@ class IpPoolController extends Controller ], 422); } + $extra = $request->only(self::EDITABLE_FIELDS); + DB::beginTransaction(); try { $created = []; @@ -161,6 +178,7 @@ class IpPoolController extends Controller $created[] = IpPool::create([ 'ip_address' => $ip, 'module_network_id' => $networkId, + ...$extra, ]); } DB::commit(); @@ -183,6 +201,9 @@ class IpPoolController extends Controller $request->validate([ 'ip_address' => 'required|string|max:45', 'module_network_id' => 'required|integer|exists:module_network,id', + 'contact_email' => 'nullable|email', + 'monthly_cost' => 'nullable|numeric', + 'selling_price' => 'nullable|numeric', ]); if (!filter_var($request->ip_address, FILTER_VALIDATE_IP)) { @@ -204,7 +225,7 @@ class IpPoolController extends Controller DB::beginTransaction(); try { - $pool->update($request->only(['ip_address', 'module_network_id'])); + $pool->update($request->only(['ip_address', 'module_network_id', ...self::EDITABLE_FIELDS])); DB::commit(); return response()->json(['error' => false, 'data' => $pool->fresh('network')]); @@ -276,6 +297,40 @@ class IpPoolController extends Controller ]); } + public function import(Request $request): JsonResponse + { + $request->validate([ + 'file' => 'required|file|mimes:csv,txt|max:10240', + ]); + + $countBefore = IpPool::withTrashed()->count(); + + try { + Excel::import(new IpPoolImport(), $request->file('file')); + } catch (ValidationException $e) { + $errors = []; + foreach ($e->failures() as $failure) { + $errors[] = 'Dòng ' . $failure->row() . ': ' . implode(', ', $failure->errors()); + } + + return response()->json([ + 'error' => true, + 'message' => 'Import thất bại', + 'errors' => $errors, + ], 422); + } catch (\Exception $e) { + return response()->json(['error' => true, 'message' => 'Import thất bại: ' . $e->getMessage()], 500); + } + + $countAfter = IpPool::withTrashed()->count(); + + return response()->json([ + 'error' => false, + 'message' => 'Import thành công', + 'created' => max(0, $countAfter - $countBefore), + ]); + } + public function networks(): JsonResponse { $networks = ModuleNetwork::select(['id', 'name', 'type', 'network']) diff --git a/app/Imports/IpPoolImport.php b/app/Imports/IpPoolImport.php new file mode 100644 index 0000000..154365d --- /dev/null +++ b/app/Imports/IpPoolImport.php @@ -0,0 +1,104 @@ + */ + private array $networkCache = []; + + public function model(array $row) + { + 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' => $this->resolveNetworkId($row['ten_mang'] ?? null, $row['cidr'] ?? null), + 'cidr' => $this->parsePrefixLength($row['cidr'] ?? null), + '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' => $row['ngay_cap_phat'] ?: null, + 'revoked_at' => $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' => $row['last_email_at'] ?: null, + 'updated_by' => $row['updated_by'] ?? null, + 'note' => $row['ghi_chu'] ?? null, + ]); + } + + public function rules(): array + { + return [ + 'ten_mang' => 'required|string|max:255', + 'cidr' => 'nullable|string|max:45', + 'ip_address' => 'required|ip', + 'contact_email' => 'nullable|email', + 'monthly_cost' => 'nullable|numeric', + 'selling_price' => 'nullable|numeric', + ]; + } + + /** + * 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 resolveNetworkId(?string $name, ?string $cidr): ?int + { + 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->id; + } +} diff --git a/app/Models/IpPool.php b/app/Models/IpPool.php index 1d898f2..bb04345 100644 --- a/app/Models/IpPool.php +++ b/app/Models/IpPool.php @@ -12,8 +12,39 @@ class IpPool extends Model use HasFactory, SoftDeletes, LogsActivity; protected $fillable = [ + 'uuid', 'ip_address', 'module_network_id', + 'gateway', + 'cidr', + 'netmask', + 'status', + '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', + ]; + + protected $casts = [ + 'allocated_at' => 'date', + 'revoked_at' => 'date', + 'last_email_at' => 'datetime', + 'monthly_cost' => 'decimal:2', + 'selling_price' => 'decimal:2', ]; public function network() diff --git a/database/migrations/2026_08_06_102950_add_import_columns_to_ip_pools_table.php b/database/migrations/2026_08_06_102950_add_import_columns_to_ip_pools_table.php new file mode 100644 index 0000000..2fdda70 --- /dev/null +++ b/database/migrations/2026_08_06_102950_add_import_columns_to_ip_pools_table.php @@ -0,0 +1,51 @@ +string('owner_type')->nullable()->after('module_network_id'); + $table->string('ip_role')->nullable()->after('owner_type'); + $table->string('parent_ip')->nullable()->after('ip_role'); + $table->string('customer_name')->nullable()->after('parent_ip'); + $table->string('service_name')->nullable()->after('customer_name'); + $table->date('allocated_at')->nullable()->after('service_name'); + $table->date('revoked_at')->nullable()->after('allocated_at'); + $table->string('allocation_status')->nullable()->after('revoked_at'); + $table->string('contact_email')->nullable()->after('allocation_status'); + $table->string('contact_cc')->nullable()->after('contact_email'); + $table->string('contact_phone')->nullable()->after('contact_cc'); + $table->decimal('monthly_cost', 14, 2)->nullable()->after('contact_phone'); + $table->decimal('selling_price', 14, 2)->nullable()->after('monthly_cost'); + $table->text('additional_info')->nullable()->after('selling_price'); + $table->string('last_email_type')->nullable()->after('additional_info'); + $table->dateTime('last_email_at')->nullable()->after('last_email_type'); + $table->string('updated_by')->nullable()->after('last_email_at'); + $table->text('note')->nullable()->after('updated_by'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('ip_pools', function (Blueprint $table) { + $table->dropColumn([ + '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', + ]); + }); + } +}; diff --git a/database/migrations/2026_08_06_104430_add_unique_index_to_ip_pools_table.php b/database/migrations/2026_08_06_104430_add_unique_index_to_ip_pools_table.php new file mode 100644 index 0000000..f7facbb --- /dev/null +++ b/database/migrations/2026_08_06_104430_add_unique_index_to_ip_pools_table.php @@ -0,0 +1,28 @@ +unique(['ip_address', 'module_network_id'], 'ip_pools_ip_network_unique'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('ip_pools', function (Blueprint $table) { + $table->dropUnique('ip_pools_ip_network_unique'); + }); + } +}; diff --git a/routes/api.php b/routes/api.php index 0066507..5a01bc7 100644 --- a/routes/api.php +++ b/routes/api.php @@ -449,6 +449,7 @@ Route::prefix('v1') Route::get('networks', 'networks')->middleware('perm:module-vcenter.view'); Route::get('/', 'index')->middleware('perm:module-vcenter.view'); Route::post('/', 'store')->middleware('perm:module-vcenter.manager'); + Route::post('import', 'import')->middleware('perm:module-vcenter.manager'); Route::delete('bulk', 'bulkDelete')->middleware('perm:module-vcenter.manager'); Route::get('{id}', 'show')->middleware('perm:module-vcenter.view'); Route::put('{id}', 'update')->middleware('perm:module-vcenter.manager');