Files
baodev0andClaude Opus 5.5 46aa138eec feat(admin): IP pool allocation, billing exports, service attachments
- IP pool: sheet import (xlsx/csv) matched by address, bulk allocate/revoke
  with notification email (ip_allocated/ip_revoked), ip_pool_logs history.
- Orders/invoices: Excel export on branded template (BrandedReportWriter),
  shared billing filters, order item service info in list responses.
- Web design quote Excel export; activity log export.
- Service attachments with copy-to-invoice; deployment invoice attachments.
- Customer email attachments stored on disk and attached by path.
- Admin password change endpoint; user.fullname backfill migration;
  orders.user_id backfill from logs.
- Disable deploy workflow until CI/CD is set up.
- Ignore stored customer email and service attachment uploads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:58:36 +07:00

166 lines
5.9 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin\IpPool\Concerns;
use App\Services\IpPool\IpPoolSheetImporter;
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;
trait ManagesIpPoolBulkActions
{
public function bulkUpdateGateway(Request $request): JsonResponse
{
$request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'integer',
'gateway' => 'required|string|max:45',
]);
$pools = IpPool::whereIn('id', $request->ids)->get(['id', 'ip_address', 'module_network_id']);
if ($pools->isEmpty()) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy IP Pool nào'], 404);
}
DB::beginTransaction();
try {
IpPool::whereIn('id', $pools->pluck('id'))->update(['gateway' => $request->gateway]);
// Mirror into module_ip_address, grouped by network to keep this to a handful of queries.
foreach ($pools->groupBy('module_network_id') as $networkId => $group) {
ModuleIpAddress::whereIn('ip', $group->pluck('ip_address'))
->where('module_network_id', $networkId)
->update(['gateway' => $request->gateway]);
}
DB::commit();
} catch (\Exception $e) {
DB::rollBack();
return response()->json(['error' => true, 'message' => $e->getMessage()], 500);
}
return response()->json([
'error' => false,
'message' => "Đã cập nhật gateway cho {$pools->count()} IP",
'updated' => $pools->count(),
]);
}
public function bulkDelete(Request $request): JsonResponse
{
$request->validate([
'ids' => 'required|array|min:1',
'ids.*' => 'integer',
]);
$ids = $request->ids;
$pools = IpPool::whereIn('id', $ids)->get();
if ($pools->isEmpty()) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy IP Pool nào'], 404);
}
// Block deletion if any IP is currently assigned
$inUseIps = ModuleIpAddress::whereIn('ip', $pools->pluck('ip_address'))
->where('status', 1)
->pluck('ip')
->toArray();
if (!empty($inUseIps)) {
return response()->json([
'error' => true,
'message' => 'Không thể xoá — các IP sau đang được gán cho máy chủ: ' . implode(', ', $inUseIps),
], 422);
}
foreach ($pools as $p) {
ModuleIpAddress::where('ip', $p->ip_address)
->where('module_network_id', $p->module_network_id)
->delete();
}
$deleted = IpPool::whereIn('id', $ids)->delete();
return response()->json([
'error' => false,
'message' => "Đã xoá {$deleted} IP Pool thành công",
'deleted' => $deleted,
]);
}
public function importPreview(Request $request, IpPoolSheetImporter $importer): JsonResponse
{
$request->validate(['file' => self::IMPORT_FILE_RULE]);
return response()->json(['error' => false, 'data' => $importer->preview($request->file('file'))]);
}
public function import(Request $request, IpPoolSheetImporter $importer): JsonResponse
{
$request->validate(['file' => self::IMPORT_FILE_RULE]);
$summary = $importer->import($request->file('file'));
return response()->json([
'error' => false,
'message' => "Đã thêm {$summary['create']}, cập nhật {$summary['update']} IP"
. ($summary['error_count'] ? ", bỏ qua {$summary['error_count']} dòng lỗi" : ''),
'data' => $summary,
]);
}
/**
* Backfill/repair the module_ip_address mirror for every ip_pools row.
* Existing mirror rows only get gateway/netmask refreshed — status/vm_name
* (assigned VM state) is left untouched. Missing rows are created with
* status derived from the current ip_pools.status.
*/
public function sync(): JsonResponse
{
$created = 0;
$updated = 0;
$hostByNet = ModuleNetwork::pluck('module_host_id', 'id');
IpPool::whereNotNull('module_network_id')->chunkById(500, function ($pools) use (&$created, &$updated, $hostByNet) {
foreach ($pools as $pool) {
$hostId = $hostByNet[$pool->module_network_id] ?? null;
$netmask = $pool->netmask ?: '255.255.255.0';
$mia = ModuleIpAddress::where('ip', $pool->ip_address)
->where('module_network_id', $pool->module_network_id)
->first();
if ($mia) {
$mia->update([
'module_host_id' => $hostId,
'gateway' => $pool->gateway,
'netmask' => $netmask,
]);
$updated++;
} else {
ModuleIpAddress::create([
'ip' => $pool->ip_address,
'module_network_id' => $pool->module_network_id,
'module_host_id' => $hostId,
'gateway' => $pool->gateway,
'netmask' => $netmask,
'status' => $pool->status === 'assigned' ? 1 : 0,
]);
$created++;
}
}
});
return response()->json([
'error' => false,
'message' => "Đã đồng bộ: tạo mới {$created}, cập nhật {$updated}",
'created' => $created,
'updated' => $updated,
]);
}
}