diff --git a/Modules/User/Http/Controllers/UserController.php b/Modules/User/Http/Controllers/UserController.php index 979a2ef..366c9cb 100644 --- a/Modules/User/Http/Controllers/UserController.php +++ b/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/Modules/User/Repositories/Contracts/UserRepositoryInterface.php b/Modules/User/Repositories/Contracts/UserRepositoryInterface.php index c5527ee..4587a55 100644 --- a/Modules/User/Repositories/Contracts/UserRepositoryInterface.php +++ b/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/Modules/User/Repositories/UserRepository.php b/Modules/User/Repositories/UserRepository.php index b53d18f..5528484 100644 --- a/Modules/User/Repositories/UserRepository.php +++ b/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/Modules/User/Routes/api.php b/Modules/User/Routes/api.php index c4fc38b..1461c8a 100644 --- a/Modules/User/Routes/api.php +++ b/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/Modules/User/Services/UserService.php b/Modules/User/Services/UserService.php index 67116df..c4eaaac 100644 --- a/Modules/User/Services/UserService.php +++ b/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/Modules/User/Transformers/UserListResource.php b/Modules/User/Transformers/UserListResource.php index 2a9f4dd..8eaf4dd 100644 --- a/Modules/User/Transformers/UserListResource.php +++ b/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/app/Console/Commands/AutoGenerateWeeklyReportForUnit.php b/app/Console/Commands/AutoGenerateWeeklyReportForUnit.php deleted file mode 100644 index 2020eba..0000000 --- a/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/app/Console/Commands/TriggerKJCHistoricalDataCapture.php b/app/Console/Commands/TriggerKJCHistoricalDataCapture.php deleted file mode 100644 index c8380de..0000000 --- a/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/app/Console/Commands/TriggerPKJHistoricalDataCapture.php b/app/Console/Commands/TriggerPKJHistoricalDataCapture.php deleted file mode 100644 index b3c638b..0000000 --- a/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/app/Http/Controllers/BaseCrudController.php b/app/Http/Controllers/BaseCrudController.php index 8281cb6..badbade 100644 --- a/app/Http/Controllers/BaseCrudController.php +++ b/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/app/Jobs/CaptureKJCHistoricalDataJob.php b/app/Jobs/CaptureKJCHistoricalDataJob.php deleted file mode 100644 index 2d71057..0000000 --- a/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/app/Jobs/CapturePKJHistoricalDataJob.php b/app/Jobs/CapturePKJHistoricalDataJob.php deleted file mode 100644 index eee5f0d..0000000 --- a/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/routes/console.php b/routes/console.php index aa56fe4..927793c 100644 --- a/routes/console.php +++ b/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