Files
portal-api/app/Http/Controllers/Api/Admin/ActivityLog/ActivityLogController.php
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

183 lines
6.3 KiB
PHP

<?php
namespace App\Http\Controllers\Api\Admin\ActivityLog;
use App\Exports\ActivityLogsExport;
use App\Exports\BrandedReportWriter;
use App\Http\Controllers\Controller;
use App\Models\ActivityLog;
use App\Repositories\ActivityLog\ActivityLogRepositoryInterface;
use App\Support\Cache\CacheKey;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class ActivityLogController extends Controller
{
private const CACHE_TTL = 60; // seconds
public function __construct(
private readonly ActivityLogRepositoryInterface $activityLogRepository,
) {}
public function index(Request $request): JsonResponse
{
$perPage = min((int) $request->get('per_page', 20), 200);
$filters = $request->only([
'log_name',
'event',
'causer_type',
'causer_id',
'causer_name',
'subject_type',
'subject_id',
'ip_address',
'date_from',
'date_to',
'search',
'page',
]);
$cacheKey = CacheKey::build('activity_logs', 'list', array_merge(
$filters,
[
'per_page' => $perPage,
'page' => (int) $request->get('page', 1),
'version' => $this->cacheVersion(),
]
));
$data = Cache::remember($cacheKey, self::CACHE_TTL, function () use ($filters, $perPage) {
$paginator = $this->activityLogRepository->filter($filters, $perPage);
return [
'items' => $paginator->items(),
'total' => $paginator->total(),
'per_page' => $paginator->perPage(),
'current_page' => $paginator->currentPage(),
'last_page' => $paginator->lastPage(),
];
});
return response()->json(['error' => false, 'data' => $data]);
}
public function show(int $id): JsonResponse
{
$cacheKey = CacheKey::build('activity_logs', 'detail', [
'id' => $id,
'version' => $this->cacheVersion(),
]);
$log = Cache::remember($cacheKey, self::CACHE_TTL * 5, function () use ($id) {
return $this->activityLogRepository->find($id);
});
if (!$log) {
return response()->json(['error' => true, 'message' => 'Không tìm thấy log'], 404);
}
return response()->json(['error' => false, 'data' => $log]);
}
public function modules(): JsonResponse
{
$cacheKey = CacheKey::build('activity_logs', 'modules', ['version' => $this->cacheVersion()]);
$modules = Cache::remember($cacheKey, self::CACHE_TTL * 10, function () {
return ActivityLog::query()
->select('log_name')
->distinct()
->orderBy('log_name')
->pluck('log_name');
});
return response()->json(['error' => false, 'data' => $modules]);
}
public function stats(): JsonResponse
{
$cacheKey = CacheKey::build('activity_logs', 'stats', ['version' => $this->cacheVersion()]);
$stats = Cache::remember($cacheKey, self::CACHE_TTL * 2, function () {
return [
'total' => ActivityLog::count(),
'today' => ActivityLog::whereDate('created_at', today())->count(),
'by_event' => ActivityLog::selectRaw('event, COUNT(*) as count')
->groupBy('event')
->orderByDesc('count')
->limit(10)
->pluck('count', 'event'),
'by_causer' => ActivityLog::selectRaw('causer_type, COUNT(*) as count')
->groupBy('causer_type')
->pluck('count', 'causer_type'),
];
});
return response()->json(['error' => false, 'data' => $stats]);
}
public function export(Request $request, BrandedReportWriter $writer): BinaryFileResponse
{
$filters = $request->validate($this->filterRules());
$query = $this->activityLogRepository->query(array_filter(
$filters,
fn ($value) => $value !== null && $value !== '',
));
return $writer->download(
new ActivityLogsExport($query),
'lich-su-hoat-dong-' . now()->format('Y-m-d-His') . '.xlsx',
);
}
public function destroy(Request $request): JsonResponse
{
$validated = $request->validate([
'mode' => ['required', 'string', 'in:all,date_range'],
'date_from' => ['required_if:mode,date_range', 'nullable', 'date_format:Y-m-d'],
'date_to' => ['required_if:mode,date_range', 'nullable', 'date_format:Y-m-d', 'after_or_equal:date_from'],
]);
$query = ActivityLog::query();
if ($validated['mode'] === 'date_range') {
$query
->where('created_at', '>=', $validated['date_from'] . ' 00:00:00')
->where('created_at', '<=', $validated['date_to'] . ' 23:59:59');
}
$deleted = $query->delete();
Cache::forever('activity_logs:version', $this->cacheVersion() + 1);
return response()->json([
'error' => false,
'message' => "Đã xóa {$deleted} log hoạt động.",
'data' => ['deleted' => $deleted],
]);
}
private function filterRules(): array
{
return [
'log_name' => ['nullable', 'string', 'max:100'],
'event' => ['nullable', 'string', 'max:100'],
'causer_type' => ['nullable', 'string', 'max:100'],
'causer_id' => ['nullable', 'integer'],
'causer_name' => ['nullable', 'string', 'max:255'],
'subject_type' => ['nullable', 'string', 'max:255'],
'subject_id' => ['nullable', 'string', 'max:255'],
'ip_address' => ['nullable', 'ip'],
'date_from' => ['nullable', 'date_format:Y-m-d'],
'date_to' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:date_from'],
'search' => ['nullable', 'string', 'max:255'],
];
}
private function cacheVersion(): int
{
return (int) Cache::get('activity_logs:version', 1);
}
}