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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ea5697cb81
commit
46aa138eec
@@ -2,6 +2,8 @@
|
||||
|
||||
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;
|
||||
@@ -9,6 +11,7 @@ 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
|
||||
{
|
||||
@@ -39,7 +42,11 @@ class ActivityLogController extends Controller
|
||||
|
||||
$cacheKey = CacheKey::build('activity_logs', 'list', array_merge(
|
||||
$filters,
|
||||
['per_page' => $perPage, 'page' => (int) $request->get('page', 1)]
|
||||
[
|
||||
'per_page' => $perPage,
|
||||
'page' => (int) $request->get('page', 1),
|
||||
'version' => $this->cacheVersion(),
|
||||
]
|
||||
));
|
||||
|
||||
$data = Cache::remember($cacheKey, self::CACHE_TTL, function () use ($filters, $perPage) {
|
||||
@@ -59,7 +66,10 @@ class ActivityLogController extends Controller
|
||||
|
||||
public function show(int $id): JsonResponse
|
||||
{
|
||||
$cacheKey = CacheKey::build('activity_logs', 'detail', ['id' => $id]);
|
||||
$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);
|
||||
@@ -74,7 +84,7 @@ class ActivityLogController extends Controller
|
||||
|
||||
public function modules(): JsonResponse
|
||||
{
|
||||
$cacheKey = CacheKey::build('activity_logs', 'modules');
|
||||
$cacheKey = CacheKey::build('activity_logs', 'modules', ['version' => $this->cacheVersion()]);
|
||||
|
||||
$modules = Cache::remember($cacheKey, self::CACHE_TTL * 10, function () {
|
||||
return ActivityLog::query()
|
||||
@@ -89,7 +99,7 @@ class ActivityLogController extends Controller
|
||||
|
||||
public function stats(): JsonResponse
|
||||
{
|
||||
$cacheKey = CacheKey::build('activity_logs', 'stats');
|
||||
$cacheKey = CacheKey::build('activity_logs', 'stats', ['version' => $this->cacheVersion()]);
|
||||
|
||||
$stats = Cache::remember($cacheKey, self::CACHE_TTL * 2, function () {
|
||||
return [
|
||||
@@ -108,4 +118,65 @@ class ActivityLogController extends Controller
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user