first
This commit is contained in:
89
app/Http/Controllers/Admin/AuditLogController.php
Normal file
89
app/Http/Controllers/Admin/AuditLogController.php
Normal file
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\View\View;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class AuditLogController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$logs = AuditLog::with(['actor', 'targetUser'])
|
||||
->when($request->action, fn ($q, $a) => $q->where('action', $a))
|
||||
->when($request->actor_id, fn ($q, $id) => $q->where('actor_user_id', $id))
|
||||
->when($request->target_id, fn ($q, $id) => $q->where('target_user_id', $id))
|
||||
->when($request->date_from, fn ($q, $d) => $q->whereDate('created_at', '>=', $d))
|
||||
->when($request->date_to, fn ($q, $d) => $q->whereDate('created_at', '<=', $d))
|
||||
->when($request->search, fn ($q, $s) =>
|
||||
$q->where('action', 'like', "%{$s}%")
|
||||
->orWhere('justification', 'like', "%{$s}%")
|
||||
->orWhere('ip_address', 'like', "%{$s}%")
|
||||
)
|
||||
->orderByDesc('created_at')
|
||||
->paginate(30)
|
||||
->withQueryString();
|
||||
|
||||
$actions = AuditLog::select('action')->distinct()->pluck('action')->sort()->values();
|
||||
$admins = User::where('role', 'admin')->orderBy('name')->get(['id', 'name']);
|
||||
|
||||
return view('admin.audit-logs.index', compact('logs', 'actions', 'admins'));
|
||||
}
|
||||
|
||||
public function export(Request $request): StreamedResponse
|
||||
{
|
||||
$query = AuditLog::with(['actor', 'targetUser'])
|
||||
->when($request->action, fn ($q, $a) => $q->where('action', $a))
|
||||
->when($request->actor_id, fn ($q, $id) => $q->where('actor_user_id', $id))
|
||||
->when($request->target_id, fn ($q, $id) => $q->where('target_user_id', $id))
|
||||
->when($request->date_from, fn ($q, $d) => $q->whereDate('created_at', '>=', $d))
|
||||
->when($request->date_to, fn ($q, $d) => $q->whereDate('created_at', '<=', $d))
|
||||
->when($request->search, fn ($q, $s) =>
|
||||
$q->where('action', 'like', "%{$s}%")
|
||||
->orWhere('justification', 'like', "%{$s}%")
|
||||
->orWhere('ip_address', 'like', "%{$s}%")
|
||||
)
|
||||
->orderByDesc('created_at');
|
||||
|
||||
$filename = 'audit-log-' . now()->format('Ymd-His') . '.csv';
|
||||
|
||||
return response()->streamDownload(function () use ($query) {
|
||||
$handle = fopen('php://output', 'w');
|
||||
|
||||
// UTF-8 BOM untuk Excel
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
|
||||
fputcsv($handle, [
|
||||
'Masa', 'Oleh', 'Peranan', 'Tindakan',
|
||||
'Sasaran', 'Projek ID', 'IP', 'Justifikasi',
|
||||
]);
|
||||
|
||||
$query->chunk(500, function ($logs) use ($handle) {
|
||||
foreach ($logs as $log) {
|
||||
fputcsv($handle, [
|
||||
$log->created_at?->format('d/m/Y H:i:s'),
|
||||
$log->actor?->name ?? 'Sistem',
|
||||
$log->actor_role ?? '',
|
||||
$log->action,
|
||||
$log->targetUser?->name ?? ($log->subject_type
|
||||
? class_basename($log->subject_type) . '#' . $log->subject_id
|
||||
: ''),
|
||||
$log->project_id ?? '',
|
||||
$log->ip_address ?? '',
|
||||
$log->justification ?? '',
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
fclose($handle);
|
||||
}, $filename, [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||
]);
|
||||
}
|
||||
}
|
||||
65
app/Http/Controllers/Admin/DashboardController.php
Normal file
65
app/Http/Controllers/Admin/DashboardController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Department;
|
||||
use App\Models\TranscriptionProject;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function index(): View
|
||||
{
|
||||
$stats = [
|
||||
'users_active' => User::where('is_active', true)->where('role', 'user')->count(),
|
||||
'users_inactive' => User::where('is_active', false)->where('role', 'user')->count(),
|
||||
'users_total' => User::where('role', 'user')->count(),
|
||||
'projects_total' => TranscriptionProject::count(),
|
||||
'projects_pending' => TranscriptionProject::where('transcription_status', 'pending')->count(),
|
||||
'projects_processing' => TranscriptionProject::where('transcription_status', 'processing')->count(),
|
||||
'projects_completed' => TranscriptionProject::where('transcription_status', 'completed')->count(),
|
||||
'projects_failed' => TranscriptionProject::where('transcription_status', 'failed')->count(),
|
||||
'projects_deleted' => TranscriptionProject::onlyTrashed()->count(),
|
||||
'projects_last_7' => TranscriptionProject::where('created_at', '>=', now()->subDays(7))->count(),
|
||||
'projects_last_30' => TranscriptionProject::where('created_at', '>=', now()->subDays(30))->count(),
|
||||
'storage_bytes' => (int) TranscriptionProject::sum('file_size'),
|
||||
'storage_completed_bytes' => (int) TranscriptionProject::where('transcription_status', 'completed')->sum('file_size'),
|
||||
'duration_total_seconds' => (int) TranscriptionProject::whereNotNull('duration_seconds')->sum('duration_seconds'),
|
||||
];
|
||||
|
||||
// Top 5 pengguna paling aktif (by project count)
|
||||
$topUsers = User::where('role', 'user')
|
||||
->withCount('ownedProjects')
|
||||
->orderByDesc('owned_projects_count')
|
||||
->limit(5)
|
||||
->get(['id', 'name', 'email']);
|
||||
|
||||
// Trend projek 30 hari lepas (group by date)
|
||||
$trendRaw = TranscriptionProject::select(
|
||||
DB::raw('DATE(created_at) as date'),
|
||||
DB::raw('COUNT(*) as total'),
|
||||
DB::raw('SUM(CASE WHEN transcription_status = "completed" THEN 1 ELSE 0 END) as completed'),
|
||||
DB::raw('SUM(CASE WHEN transcription_status = "failed" THEN 1 ELSE 0 END) as failed')
|
||||
)
|
||||
->where('created_at', '>=', now()->subDays(29)->startOfDay())
|
||||
->groupBy(DB::raw('DATE(created_at)'))
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
// Projek mengikut jabatan (metadata sahaja)
|
||||
$deptStats = Department::select('departments.name')
|
||||
->selectRaw('COUNT(tp.id) as project_count')
|
||||
->leftJoin('users', 'users.department_id', '=', 'departments.id')
|
||||
->leftJoin('transcription_projects as tp', 'tp.owner_user_id', '=', 'users.id')
|
||||
->where('departments.is_active', true)
|
||||
->groupBy('departments.id', 'departments.name')
|
||||
->orderByDesc('project_count')
|
||||
->limit(8)
|
||||
->get();
|
||||
|
||||
return view('admin.dashboard', compact('stats', 'topUsers', 'trendRaw', 'deptStats'));
|
||||
}
|
||||
}
|
||||
65
app/Http/Controllers/Admin/ProjectMetadataController.php
Normal file
65
app/Http/Controllers/Admin/ProjectMetadataController.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Actions\TransferOwnershipAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\TransferOwnershipRequest;
|
||||
use App\Models\TranscriptionProject;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class ProjectMetadataController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
// Admin hanya mendapat metadata — tiada transcript_text, tiada stored_audio_path
|
||||
$projects = TranscriptionProject::withTrashed()
|
||||
->with(['owner:id,name,email', 'projectCollaborators'])
|
||||
->select([
|
||||
'id', 'uuid', 'title', 'owner_user_id',
|
||||
'transcription_status', 'transcription_engine',
|
||||
'file_size', 'duration_seconds',
|
||||
'language',
|
||||
'created_at', 'deleted_at',
|
||||
// Sengaja TIDAK ambil: transcript_text, stored_audio_path
|
||||
])
|
||||
->when($request->status, fn ($q, $s) => $q->where('transcription_status', $s))
|
||||
->when($request->search, fn ($q, $s) =>
|
||||
$q->where('title', 'like', "%{$s}%")
|
||||
)
|
||||
->when($request->owner_id, fn ($q, $id) => $q->where('owner_user_id', $id))
|
||||
->orderByDesc('created_at')
|
||||
->paginate(25)
|
||||
->withQueryString();
|
||||
|
||||
$owners = User::where('role', 'user')
|
||||
->where('is_active', true)
|
||||
->orderBy('name')
|
||||
->get(['id', 'name']);
|
||||
|
||||
return view('admin.projects.index', compact('projects', 'owners'));
|
||||
}
|
||||
|
||||
public function transferOwner(
|
||||
TransferOwnershipRequest $request,
|
||||
TranscriptionProject $project,
|
||||
TransferOwnershipAction $action
|
||||
): RedirectResponse {
|
||||
$this->authorize('transferOwner', $project);
|
||||
|
||||
$newOwner = User::findOrFail($request->new_owner_id);
|
||||
|
||||
// Pastikan pemilik baru adalah pengguna aktif (bukan admin)
|
||||
abort_if($newOwner->isAdmin(), 403, 'Admin tidak boleh menjadi pemilik projek.');
|
||||
abort_if(! $newOwner->isActive(), 422, 'Pengguna tidak aktif tidak boleh menerima pemindahan projek.');
|
||||
abort_if($project->owner_user_id === $newOwner->id, 422, 'Projek sudah dimiliki oleh pengguna ini.');
|
||||
|
||||
$action->execute($project, $newOwner, $request->justification);
|
||||
|
||||
return redirect()->route('admin.projects.index')
|
||||
->with('success', "Pemilikan projek \"{$project->title}\" berjaya dipindahkan kepada {$newOwner->name}.");
|
||||
}
|
||||
}
|
||||
106
app/Http/Controllers/Admin/UserController.php
Normal file
106
app/Http/Controllers/Admin/UserController.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Actions\ActivateUserAction;
|
||||
use App\Actions\ChangeUserEmailAction;
|
||||
use App\Actions\CreateUserAction;
|
||||
use App\Actions\DeactivateUserAction;
|
||||
use App\Actions\DeleteUserAction;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\CreateUserRequest;
|
||||
use App\Http\Requests\Admin\UpdateUserEmailRequest;
|
||||
use App\Models\Department;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$this->authorize('viewAny', User::class);
|
||||
|
||||
$users = User::with('department')
|
||||
->where('role', 'user')
|
||||
->when($request->search, fn ($q, $s) =>
|
||||
$q->where('name', 'like', "%{$s}%")
|
||||
->orWhere('email', 'like', "%{$s}%")
|
||||
)
|
||||
->when($request->status, fn ($q, $s) =>
|
||||
$q->where('is_active', $s === 'active')
|
||||
)
|
||||
->orderBy('name')
|
||||
->paginate(20)
|
||||
->withQueryString();
|
||||
|
||||
return view('admin.users.index', compact('users'));
|
||||
}
|
||||
|
||||
public function create(): View
|
||||
{
|
||||
$this->authorize('create', User::class);
|
||||
|
||||
$departments = Department::where('is_active', true)->orderBy('name')->get();
|
||||
|
||||
return view('admin.users.create', compact('departments'));
|
||||
}
|
||||
|
||||
public function store(CreateUserRequest $request, CreateUserAction $action): RedirectResponse
|
||||
{
|
||||
$action->execute($request->validated());
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', 'Pengguna berjaya didaftarkan.');
|
||||
}
|
||||
|
||||
public function edit(User $user): View
|
||||
{
|
||||
$this->authorize('changeEmail', $user);
|
||||
|
||||
$departments = Department::where('is_active', true)->orderBy('name')->get();
|
||||
|
||||
return view('admin.users.edit', compact('user', 'departments'));
|
||||
}
|
||||
|
||||
public function updateEmail(UpdateUserEmailRequest $request, User $user, ChangeUserEmailAction $action): RedirectResponse
|
||||
{
|
||||
$this->authorize('changeEmail', $user);
|
||||
|
||||
$action->execute($user, $request->email, $request->justification);
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', "E-mel pengguna {$user->name} berjaya dikemaskini.");
|
||||
}
|
||||
|
||||
public function activate(Request $request, User $user, ActivateUserAction $action): RedirectResponse
|
||||
{
|
||||
$this->authorize('activate', $user);
|
||||
|
||||
$action->execute($user);
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', "Akaun {$user->name} berjaya diaktifkan.");
|
||||
}
|
||||
|
||||
public function deactivate(Request $request, User $user, DeactivateUserAction $action): RedirectResponse
|
||||
{
|
||||
$this->authorize('deactivate', $user);
|
||||
|
||||
$action->execute($user, $request->justification);
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', "Akaun {$user->name} berjaya dinyahaktifkan.");
|
||||
}
|
||||
|
||||
public function destroy(Request $request, User $user, DeleteUserAction $action): RedirectResponse
|
||||
{
|
||||
$this->authorize('delete', $user);
|
||||
|
||||
$action->execute($user, $request->justification);
|
||||
|
||||
return redirect()->route('admin.users.index')
|
||||
->with('success', "Pengguna {$user->name} berjaya dipadam.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user