diff --git a/be/Modules/User/Http/Controllers/UserController.php b/be/Modules/User/Http/Controllers/UserController.php
index 979a2ef..366c9cb 100644
--- a/be/Modules/User/Http/Controllers/UserController.php
+++ b/be/Modules/User/Http/Controllers/UserController.php
@@ -269,6 +269,43 @@ class UserController extends BaseCrudController
}
}
+ public function stats(Request $request): JsonResponse
+ {
+ $this->authorize('viewAny', $this->modelClass);
+
+ try {
+ $request->validate([
+ 'join_date_from' => 'nullable|date',
+ 'join_date_to' => 'nullable|date',
+ 'leave_date_from' => 'nullable|date',
+ 'leave_date_to' => 'nullable|date',
+ ]);
+
+ $dateFilters = array_filter([
+ 'join_date_from' => $request->get('join_date_from'),
+ 'join_date_to' => $request->get('join_date_to'),
+ 'leave_date_from' => $request->get('leave_date_from'),
+ 'leave_date_to' => $request->get('leave_date_to'),
+ ]);
+
+ $stats = $this->userService->getListStats(
+ $request->get('search', ''),
+ $request->get('status', ''),
+ $dateFilters
+ );
+
+ return response()->json([
+ 'success' => true,
+ 'data' => $stats,
+ 'message' => 'User stats retrieved successfully.',
+ ]);
+ } catch (Exception $e) {
+ Log::error("Error fetching {$this->resourceNamePlural} stats: ".$e->getMessage());
+
+ return $this->errorResponse('Failed to retrieve user stats.', 500);
+ }
+ }
+
public function restore(string $id): JsonResponse
{
$this->authorize('restore', $this->modelClass);
diff --git a/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php b/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php
index c5527ee..4587a55 100644
--- a/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php
+++ b/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php
@@ -55,6 +55,17 @@ interface UserRepositoryInterface
string $sortOrder = 'desc'
);
+ /**
+ * Get stats for the Users list (filtered).
+ *
+ * @return array{total: int, joined_this_month: int}
+ */
+ public function getListStats(
+ string $search = '',
+ string $status = '',
+ array $dateFilters = []
+ ): array;
+
/**
* Find soft-deleted User by ID
*/
diff --git a/be/Modules/User/Repositories/UserRepository.php b/be/Modules/User/Repositories/UserRepository.php
index b53d18f..5528484 100644
--- a/be/Modules/User/Repositories/UserRepository.php
+++ b/be/Modules/User/Repositories/UserRepository.php
@@ -3,6 +3,7 @@
namespace Modules\User\Repositories;
use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Support\Carbon;
use Modules\Auth\Entities\User;
use Modules\User\Repositories\Contracts\UserRepositoryInterface;
@@ -85,7 +86,18 @@ class UserRepository implements UserRepositoryInterface
$query = User::excludeDevelopersUnlessDeveloper()
->with([
- 'roles:id,name,guard_name'
+ 'roles:id,name,guard_name',
+ 'employments' => function ($q) {
+ $q->select([
+ 'id',
+ 'user_id',
+ 'company_name',
+ 'is_current',
+ 'start_date',
+ ])
+ ->orderByDesc('is_current')
+ ->orderByDesc('start_date');
+ },
])
->orderBy($sortBy, $sortOrder);
@@ -129,6 +141,17 @@ class UserRepository implements UserRepositoryInterface
->excludeDevelopersUnlessDeveloper()
->with([
'roles:id,name,guard_name',
+ 'employments' => function ($q) {
+ $q->select([
+ 'id',
+ 'user_id',
+ 'company_name',
+ 'is_current',
+ 'start_date',
+ ])
+ ->orderByDesc('is_current')
+ ->orderByDesc('start_date');
+ },
])
->orderBy($sortBy, $sortOrder);
@@ -141,6 +164,36 @@ class UserRepository implements UserRepositoryInterface
return $query->paginate($perPage);
}
+ public function getListStats(
+ string $search = '',
+ string $status = '',
+ array $dateFilters = []
+ ): array {
+ $baseQuery = User::excludeDevelopersUnlessDeveloper();
+
+ $this->applySearch($baseQuery, $search);
+
+ if (! empty($status)) {
+ $baseQuery->where('status', $status);
+ }
+
+ $this->applyDateRangeFilters($baseQuery, $dateFilters);
+
+ $total = (int) (clone $baseQuery)->count();
+
+ $monthStart = Carbon::now()->startOfMonth()->toDateString();
+ $monthEnd = Carbon::now()->endOfMonth()->toDateString();
+ $joinedThisMonth = (int) (clone $baseQuery)
+ ->whereDate('join_date', '>=', $monthStart)
+ ->whereDate('join_date', '<=', $monthEnd)
+ ->count();
+
+ return [
+ 'total' => $total,
+ 'joined_this_month' => $joinedThisMonth,
+ ];
+ }
+
/**
* Get all Users with their relationships and search
*/
diff --git a/be/Modules/User/Routes/api.php b/be/Modules/User/Routes/api.php
index c4fc38b..1461c8a 100644
--- a/be/Modules/User/Routes/api.php
+++ b/be/Modules/User/Routes/api.php
@@ -19,6 +19,7 @@ Route::prefix('v1/public')->group(function () {
Route::middleware(['auth:sanctum', 'single.session'])->prefix('v1')->group(function () {
Route::get('users/deleted', [UserController::class, 'deletedIndex'])->name('users.deleted.index');
+ Route::get('users/stats', [UserController::class, 'stats'])->name('users.stats');
Route::post('users/{id}/restore', [UserController::class, 'restore'])->name('users.restore');
Route::apiResource('users', UserController::class)->names('user');
Route::apiResource('addresses', AddressController::class)->names('address');
diff --git a/be/Modules/User/Services/UserService.php b/be/Modules/User/Services/UserService.php
index 67116df..c4eaaac 100644
--- a/be/Modules/User/Services/UserService.php
+++ b/be/Modules/User/Services/UserService.php
@@ -50,6 +50,14 @@ class UserService
);
}
+ /**
+ * @return array{total: int, joined_this_month: int}
+ */
+ public function getListStats(string $search, string $status, array $dateFilters = []): array
+ {
+ return $this->repository->getListStats($search, $status, $dateFilters);
+ }
+
public function restoreUser(string $id): ?User
{
if (! $this->repository->restore($id)) {
diff --git a/be/Modules/User/Transformers/UserListResource.php b/be/Modules/User/Transformers/UserListResource.php
index 2a9f4dd..8eaf4dd 100644
--- a/be/Modules/User/Transformers/UserListResource.php
+++ b/be/Modules/User/Transformers/UserListResource.php
@@ -5,6 +5,7 @@ namespace Modules\User\Transformers;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
+use Modules\User\Transformers\EmploymentResource;
class UserListResource extends JsonResource
{
@@ -31,6 +32,7 @@ class UserListResource extends JsonResource
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'deleted_at' => $this->deleted_at,
+ 'employments' => EmploymentResource::collection($this->whenLoaded('employments')),
'roles' => $this->roles->map(function ($role) {
return [
'id' => $role->id,
diff --git a/be/app/Console/Commands/AutoGenerateWeeklyReportForUnit.php b/be/app/Console/Commands/AutoGenerateWeeklyReportForUnit.php
deleted file mode 100644
index 2020eba..0000000
--- a/be/app/Console/Commands/AutoGenerateWeeklyReportForUnit.php
+++ /dev/null
@@ -1,56 +0,0 @@
-argument('unit');
-
- $unit = Unit::where('name', $unitName)->first();
-
- if (! $unit) {
- $this->error("Unit '{$unitName}' not found.");
-
- return 1;
- }
-
- $month = strtolower(now()->format('F'));
- $week = (int) ceil(now()->day / 7);
-
- if (! $this->option('force')) {
- $existingReports = KJCReport::where('unit_id', $unit->id)
- ->where('report_type', 'weekly')
- ->where('report_month', $month)
- ->where('report_week', $week)
- ->exists();
-
- if ($existingReports) {
- $this->info("Reports for {$unitName} - {$month} week {$week} already exist. Skipping.");
-
- return 0;
- }
- }
-
- $user = User::where('unit_id', $unit->id)->first();
-
- GenerateTeamWeeklyReports::dispatch($unit->id, $month, $week, $user?->id);
-
- $this->info("✓ Queued weekly report generation for {$unitName} - {$month} week {$week}");
-
- return 0;
- }
-}
diff --git a/be/app/Console/Commands/TriggerKJCHistoricalDataCapture.php b/be/app/Console/Commands/TriggerKJCHistoricalDataCapture.php
deleted file mode 100644
index c8380de..0000000
--- a/be/app/Console/Commands/TriggerKJCHistoricalDataCapture.php
+++ /dev/null
@@ -1,68 +0,0 @@
-option('month');
- $year = $this->option('year');
- $useQueue = $this->option('queue');
-
- // If no month/year provided, use previous month
- if (! $month || ! $year) {
- $lastMonth = Carbon::now()->subMonth();
- $month = $month ?: $lastMonth->format('m');
- $year = $year ?: $lastMonth->format('Y');
- }
-
- $this->info("Triggering KJC historical data capture for {$month}/{$year}...");
-
- try {
- if ($useQueue) {
- // Dispatch to queue
- CaptureKJCHistoricalDataJob::dispatch($month, $year);
- $this->info('✓ KJC historical data capture job dispatched to queue');
- $this->info('You can monitor the job in Horizon dashboard');
- } else {
- // Run immediately
- $job = new CaptureKJCHistoricalDataJob($month, $year);
- $job->handle(app(KJCHistoricalDataService::class));
- $this->info('✓ KJC historical data capture completed immediately');
- }
-
- } catch (\Exception $e) {
- $this->error('Error: '.$e->getMessage());
-
- return 1;
- }
-
- return 0;
- }
-}
diff --git a/be/app/Console/Commands/TriggerPKJHistoricalDataCapture.php b/be/app/Console/Commands/TriggerPKJHistoricalDataCapture.php
deleted file mode 100644
index b3c638b..0000000
--- a/be/app/Console/Commands/TriggerPKJHistoricalDataCapture.php
+++ /dev/null
@@ -1,55 +0,0 @@
-option('month');
- $year = $this->option('year');
- $useQueue = $this->option('queue');
-
- // If no month/year provided, use previous month
- if (! $month || ! $year) {
- $lastMonth = Carbon::now()->subMonth();
- $month = $month ?: $lastMonth->format('m');
- $year = $year ?: $lastMonth->format('Y');
- }
-
- $this->info("Triggering PKJ historical data capture for {$month}/{$year}...");
-
- try {
- if ($useQueue) {
- // Dispatch to queue
- CapturePKJHistoricalDataJob::dispatch($month, $year);
- $this->info('✓ PKJ historical data capture job dispatched to queue');
- $this->info('You can monitor the job in Horizon dashboard');
- } else {
- // Run immediately
- $job = new CapturePKJHistoricalDataJob($month, $year);
- $job->handle(app(PKJHistoricalDataService::class));
- $this->info('✓ PKJ historical data capture completed immediately');
- }
-
- } catch (\Exception $e) {
- $this->error('Error: '.$e->getMessage());
-
- return 1;
- }
-
- return 0;
- }
-}
diff --git a/be/app/Http/Controllers/BaseCrudController.php b/be/app/Http/Controllers/BaseCrudController.php
index 8281cb6..badbade 100644
--- a/be/app/Http/Controllers/BaseCrudController.php
+++ b/be/app/Http/Controllers/BaseCrudController.php
@@ -113,8 +113,6 @@ abstract class BaseCrudController extends Controller
$item = $this->repository->create($data);
- ActivityLogger::log("Created {$this->resourceName}: {$this->getItemName($item)}", $item);
-
return response()->json([
'success' => true,
'data' => new $this->resourceClass($item),
@@ -175,8 +173,6 @@ abstract class BaseCrudController extends Controller
$item->update($data);
- ActivityLogger::log("Updated {$this->resourceName}: {$this->getItemName($item)}", $item);
-
return response()->json([
'success' => true,
'data' => new $this->resourceClass($item),
@@ -221,8 +217,6 @@ abstract class BaseCrudController extends Controller
$this->repository->delete($id);
- ActivityLogger::log("Deleted {$this->resourceName}: {$this->getItemName($item)}", $item);
-
return response()->json([
'success' => true,
'message' => $this->getSuccessMessage('destroy'),
diff --git a/be/app/Jobs/CaptureKJCHistoricalDataJob.php b/be/app/Jobs/CaptureKJCHistoricalDataJob.php
deleted file mode 100644
index 2d71057..0000000
--- a/be/app/Jobs/CaptureKJCHistoricalDataJob.php
+++ /dev/null
@@ -1,102 +0,0 @@
-month = $month;
- $this->year = $year;
-
- // Set queue name for Horizon monitoring
- $this->onQueue('kjc-historical-data');
- }
-
- /**
- * Execute the job.
- */
- public function handle(KJCHistoricalDataService $kjcHistoricalService)
- {
- try {
- Log::info('Starting KJC historical data capture job', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- ]);
-
- $success = $kjcHistoricalService->captureMonthlySnapshots($this->month, $this->year);
-
- if ($success) {
- Log::info('KJC historical data capture job completed successfully', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- ]);
- } else {
- Log::error('KJC historical data capture job failed', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- ]);
-
- throw new \Exception('KJC historical data capture failed');
- }
-
- } catch (\Exception $e) {
- Log::error('KJC historical data capture job exception', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- ]);
-
- throw $e;
- }
- }
-
- /**
- * Handle a job failure.
- */
- public function failed(\Throwable $exception)
- {
- Log::error('KJC historical data capture job failed permanently', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- 'error' => $exception->getMessage(),
- ]);
- }
-
- /**
- * Get the tags that should be assigned to the job.
- */
- public function tags()
- {
- return ['kjc-historical-data', "month-{$this->month}", "year-{$this->year}"];
- }
-}
diff --git a/be/app/Jobs/CapturePKJHistoricalDataJob.php b/be/app/Jobs/CapturePKJHistoricalDataJob.php
deleted file mode 100644
index eee5f0d..0000000
--- a/be/app/Jobs/CapturePKJHistoricalDataJob.php
+++ /dev/null
@@ -1,102 +0,0 @@
-month = $month;
- $this->year = $year;
-
- // Set queue name for Horizon monitoring
- $this->onQueue('pkj-historical-data');
- }
-
- /**
- * Execute the job.
- */
- public function handle(PKJHistoricalDataService $historicalService)
- {
- try {
- Log::info('Starting PKJ historical data capture job', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- ]);
-
- $success = $historicalService->captureMonthlySnapshots($this->month, $this->year);
-
- if ($success) {
- Log::info('PKJ historical data capture job completed successfully', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- ]);
- } else {
- Log::error('PKJ historical data capture job failed', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- ]);
-
- throw new \Exception('PKJ historical data capture failed');
- }
-
- } catch (\Exception $e) {
- Log::error('PKJ historical data capture job exception', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- 'error' => $e->getMessage(),
- 'trace' => $e->getTraceAsString(),
- ]);
-
- throw $e;
- }
- }
-
- /**
- * Handle a job failure.
- */
- public function failed(\Throwable $exception)
- {
- Log::error('PKJ historical data capture job failed permanently', [
- 'month' => $this->month,
- 'year' => $this->year,
- 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution',
- 'error' => $exception->getMessage(),
- ]);
- }
-
- /**
- * Get the tags that should be assigned to the job.
- */
- public function tags()
- {
- return ['pkj-historical-data', "month-{$this->month}", "year-{$this->year}"];
- }
-}
diff --git a/be/routes/console.php b/be/routes/console.php
index aa56fe4..927793c 100644
--- a/be/routes/console.php
+++ b/be/routes/console.php
@@ -3,27 +3,7 @@
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;
-use App\Jobs\CaptureKJCHistoricalDataJob;
-use App\Jobs\CapturePKJHistoricalDataJob;
Artisan::command('inspire', function () {
$this->comment(Inspiring::quote());
-})->purpose('Display an inspiring quote');
-
-// Schedule KJC Historical Data Capture to run monthly on the 1st day at 2:00 AM
-Schedule::job(new CaptureKJCHistoricalDataJob())
- ->monthlyOn(1, '02:00')
- ->withoutOverlapping()
- ->name('kjc-historical-data-capture');
-
-// Schedule PKJ Historical Data Capture to run monthly on the 1st day at 2:30 AM
-Schedule::job(new CapturePKJHistoricalDataJob())
- ->monthlyOn(1, '02:30')
- ->withoutOverlapping()
- ->name('pkj-historical-data-capture');
-
-// Auto-generate weekly KJC reports for Jabatan Arah RAJD every Monday at 3:00 AM
-Schedule::command('kjc:auto-generate-weekly-report "Jabatan Arah RAJD"')
- ->weeklyOn(1, '03:00') // Monday at 3:00 AM
- ->withoutOverlapping()
- ->name('kjc-auto-weekly-report');
\ No newline at end of file
+})->purpose('Display an inspiring quote');
\ No newline at end of file
diff --git a/fe/src/composables/useRoleSwitcher.ts b/fe/src/composables/useRoleSwitcher.ts
index 65b6fde..8fe06ed 100644
--- a/fe/src/composables/useRoleSwitcher.ts
+++ b/fe/src/composables/useRoleSwitcher.ts
@@ -86,9 +86,9 @@ export function useRoleSwitcher() {
toast: true,
position: 'top-end',
icon: 'success',
- title: res.message || 'Peranan telah ditukar.',
+ title: res.message,
showConfirmButton: false,
- timer: 3000,
+ timer: 500,
})
await router.push(resolvePostLoginRoute(res.redirect_path))
diff --git a/fe/src/modules/auth/pages/Login.vue b/fe/src/modules/auth/pages/Login.vue
index 89b6cad..317940c 100644
--- a/fe/src/modules/auth/pages/Login.vue
+++ b/fe/src/modules/auth/pages/Login.vue
@@ -117,7 +117,7 @@ const appVersion = import.meta.env.VITE_APP_VERSION