first init

This commit is contained in:
ISMAIL MASSERAN
2026-06-08 11:37:14 +08:00
commit 94ecbe5887
1058 changed files with 87732 additions and 0 deletions
+131
View File
@@ -0,0 +1,131 @@
<?php
namespace App\Services;
use App\Models\PersonalAccessToken;
use Modules\Auth\Entities\User;
use Modules\Role\Entities\Role;
class ActiveRoleService
{
public static function resolveDefaultRole(User $user): ?Role
{
$roles = $user->roles;
if ($roles->isEmpty()) {
return null;
}
if ($roles->count() === 1) {
return $roles->first();
}
if (config('active_role.prefer_member_on_login', true)) {
$memberRole = $roles->first(fn (Role $role) => self::roleContext($role) === 'member');
if ($memberRole) {
return $memberRole;
}
}
return $roles->first();
}
public static function assignToCurrentToken(User $user, Role $role): void
{
$token = $user->currentAccessToken();
if ($token instanceof PersonalAccessToken) {
$token->forceFill(['active_role_id' => $role->id])->save();
}
}
public static function assignDefaultToToken(User $user, PersonalAccessToken $accessToken): void
{
$user->loadMissing('roles');
$role = self::resolveDefaultRole($user);
if ($role) {
$accessToken->forceFill(['active_role_id' => $role->id])->save();
}
}
public static function getActiveRole(User $user): ?Role
{
$user->loadMissing(['roles.permissions']);
$token = $user->currentAccessToken();
if ($token instanceof PersonalAccessToken && $token->active_role_id) {
$role = $user->roles->firstWhere('id', $token->active_role_id);
if ($role) {
return $role;
}
}
return self::resolveDefaultRole($user);
}
public static function switchRole(User $user, string $roleId): ?Role
{
$role = $user->roles()->where('roles.id', $roleId)->first();
if (! $role) {
return null;
}
self::assignToCurrentToken($user, $role);
return $role->load('permissions');
}
public static function roleContext(Role $role): string
{
$context = $role->context ?? 'member';
return in_array($context, ['admin', 'member'], true) ? $context : 'member';
}
public static function redirectPathForRole(Role $role): string
{
return self::roleContext($role) === 'admin'
? config('active_role.admin_redirect', '/profile')
: config('active_role.member_redirect', '/profile');
}
/**
* @return array<string, mixed>|null
*/
public static function formatRole(?Role $role): ?array
{
if (! $role) {
return null;
}
return [
'id' => $role->id,
'name' => $role->name,
'fullname' => $role->fullname ?? null,
'guard_name' => $role->guard_name,
'context' => self::roleContext($role),
];
}
/**
* @return array<string, mixed>
*/
public static function sessionMeta(User $user): array
{
$activeRole = self::getActiveRole($user);
return [
'active_role' => self::formatRole($activeRole),
'can_switch_role' => $user->roles->count() > 1,
'redirect_path' => $activeRole
? self::redirectPathForRole($activeRole)
: config('active_role.member_redirect', '/profile'),
];
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Services;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
use Spatie\Activitylog\Models\Activity;
class ActivityLogger
{
public static function log(string $description, ?Model $subject = null, array $properties = [], string $logName = 'default'): Activity
{
$activity = activity($logName)
->causedBy(Auth::user())
->withProperties($properties)
->log($description);
if ($subject) {
$activity->update(['subject_type' => get_class($subject), 'subject_id' => $subject->getKey()]);
}
return $activity;
}
public static function logLogin(string $email): void
{
self::log("User logged in with email: {$email}", null, [
'email' => $email,
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
], 'authentication');
}
public static function logLogout(): void
{
self::log('User logged out', null, [
'ip_address' => request()->ip(),
'user_agent' => request()->userAgent(),
], 'authentication');
}
public static function logView(Model $model, ?string $customDescription = null): void
{
$modelName = class_basename($model);
$description = $customDescription ?: "Viewed {$modelName}";
self::log($description, $model, [
'action' => 'view',
'url' => request()->fullUrl(),
'method' => request()->method(),
], 'view');
}
public static function logCustomAction(string $action, string $description, ?Model $subject = null, array $properties = []): void
{
$properties = array_merge($properties, [
'action' => $action,
'url' => request()->fullUrl(),
'method' => request()->method(),
'ip_address' => request()->ip(),
]);
self::log($description, $subject, $properties, 'custom');
}
public static function logSearch(string $query, string $module): void
{
self::log("Searched for '{$query}' in {$module}", null, [
'search_query' => $query,
'module' => $module,
'results_count' => 0, // You can update this if needed
], 'search');
}
public static function logExport(string $type, ?string $filename = null): void
{
self::log("Exported {$type} data", null, [
'export_type' => $type,
'filename' => $filename,
'format' => pathinfo($filename, PATHINFO_EXTENSION) ?? 'unknown',
], 'export');
}
public static function logError(string $error, ?Model $subject = null): void
{
self::log("Error occurred: {$error}", $subject, [
'error_message' => $error,
'url' => request()->fullUrl(),
'method' => request()->method(),
], 'error');
}
}
+198
View File
@@ -0,0 +1,198 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class ContactService
{
/**
* Get all active contact persons
*/
public function getActiveContacts(): array
{
try {
$contacts = DB::table('contact_settings')
->where('is_active', true)
->orderBy('sort_order', 'asc')
->orderBy('name', 'asc')
->get()
->toArray();
return array_map(function ($item) {
return [
'id' => $item->id,
'name' => $item->name,
'position' => $item->position,
'email' => $item->email,
'phone' => $item->phone,
'department' => $item->department,
'sort_order' => $item->sort_order,
];
}, $contacts);
} catch (\Exception $e) {
Log::error('Error fetching active contacts: ' . $e->getMessage());
throw $e;
}
}
/**
* Get all contact settings (including inactive) for admin
*/
public function getAllContactSettings(): array
{
try {
$settings = DB::table('contact_settings')
->orderBy('sort_order', 'asc')
->orderBy('name', 'asc')
->get()
->toArray();
return array_map(function ($item) {
return [
'id' => $item->id,
'name' => $item->name,
'position' => $item->position,
'email' => $item->email,
'phone' => $item->phone,
'department' => $item->department,
'is_active' => (bool) $item->is_active,
'sort_order' => $item->sort_order,
'created_at' => $item->created_at,
'updated_at' => $item->updated_at,
];
}, $settings);
} catch (\Exception $e) {
Log::error('Error fetching all contact settings: ' . $e->getMessage());
throw $e;
}
}
/**
* Update contact settings (bulk update)
*/
public function updateContactSettings(array $contactData): array
{
try {
// Clear existing settings
DB::table('contact_settings')->truncate();
// Insert new settings
$insertData = [];
foreach ($contactData as $index => $item) {
$insertData[] = [
'name' => $item['name'],
'position' => $item['position'] ?? null,
'email' => $item['email'] ?? null,
'phone' => $item['phone'] ?? null,
'department' => $item['department'] ?? null,
'is_active' => $item['is_active'] ?? true,
'sort_order' => $item['sort_order'] ?? $index,
'created_at' => now(),
'updated_at' => now(),
];
}
DB::table('contact_settings')->insert($insertData);
// Return updated settings
return $this->getAllContactSettings();
} catch (\Exception $e) {
Log::error('Error updating contact settings: ' . $e->getMessage());
throw $e;
}
}
/**
* Add a single contact person
*/
public function addContactPerson(array $data): array
{
try {
$id = DB::table('contact_settings')->insertGetId([
'name' => $data['name'],
'position' => $data['position'] ?? null,
'email' => $data['email'] ?? null,
'phone' => $data['phone'] ?? null,
'department' => $data['department'] ?? null,
'is_active' => $data['is_active'] ?? true,
'sort_order' => $data['sort_order'] ?? 0,
'created_at' => now(),
'updated_at' => now(),
]);
return $this->getAllContactSettings();
} catch (\Exception $e) {
Log::error('Error adding contact person: ' . $e->getMessage());
throw $e;
}
}
/**
* Update a single contact person
*/
public function updateContactPerson(int $id, array $data): array
{
try {
DB::table('contact_settings')
->where('id', $id)
->update([
'name' => $data['name'],
'position' => $data['position'] ?? null,
'email' => $data['email'] ?? null,
'phone' => $data['phone'] ?? null,
'department' => $data['department'] ?? null,
'is_active' => $data['is_active'] ?? true,
'sort_order' => $data['sort_order'] ?? 0,
'updated_at' => now(),
]);
return $this->getAllContactSettings();
} catch (\Exception $e) {
Log::error('Error updating contact person: ' . $e->getMessage());
throw $e;
}
}
/**
* Delete a contact person
*/
public function deleteContactPerson(int $id): array
{
try {
DB::table('contact_settings')->where('id', $id)->delete();
return $this->getAllContactSettings();
} catch (\Exception $e) {
Log::error('Error deleting contact person: ' . $e->getMessage());
throw $e;
}
}
/**
* Toggle active status of a contact person
*/
public function toggleContactPerson(int $id): array
{
try {
$contact = DB::table('contact_settings')->where('id', $id)->first();
if (!$contact) {
throw new \Exception('Contact person not found');
}
DB::table('contact_settings')
->where('id', $id)
->update([
'is_active' => !$contact->is_active,
'updated_at' => now(),
]);
return $this->getAllContactSettings();
} catch (\Exception $e) {
Log::error('Error toggling contact person: ' . $e->getMessage());
throw $e;
}
}
}
+174
View File
@@ -0,0 +1,174 @@
<?php
namespace App\Services;
use App\Models\Document;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
class DocumentService
{
/**
* Upload a document for any model.
*/
public function uploadDocument(
Model $model,
UploadedFile $file,
string $documentType = 'general',
?string $description = null
): Document {
// Generate unique filename
$fileName = time().'_'.$file->getClientOriginalName();
// Store file in a folder named after the model
$folderName = strtolower(class_basename($model));
$filePath = $file->storeAs("documents/{$folderName}", $fileName, 'public');
// Create document record
return Document::create([
'documentable_type' => get_class($model),
'documentable_id' => $model->id,
'document_name' => $file->getClientOriginalName(),
'document_path' => $filePath,
'file_size' => $file->getSize(),
'mime_type' => $file->getClientMimeType(),
'document_type' => $documentType,
'description' => $description,
'uploaded_by' => auth()->id(),
]);
}
/**
* Delete a document.
*/
public function deleteDocument(int $documentId): bool
{
$document = Document::findOrFail($documentId);
return $document->delete();
}
/**
* Get all documents for a model.
*/
public function getDocuments(Model $model, ?string $documentType = null)
{
$query = $model->documents();
if ($documentType) {
$query->where('document_type', $documentType);
}
return $query->with('uploadedBy')->get();
}
/**
* Get document by ID with validation.
*/
public function getDocument(int $documentId): Document
{
return Document::with('uploadedBy')->findOrFail($documentId);
}
/**
* Download a document.
*/
public function downloadDocument(int $documentId)
{
$document = $this->getDocument($documentId);
if (! Storage::exists($document->document_path)) {
throw new Exception('File not found');
}
return Storage::download($document->document_path, $document->document_name);
}
/**
* Get documents count for a model.
*/
public function getDocumentsCount(Model $model, ?string $documentType = null): int
{
$query = $model->documents();
if ($documentType) {
$query->where('document_type', $documentType);
}
return $query->count();
}
/**
* Check if model has documents.
*/
public function hasDocuments(Model $model, ?string $documentType = null): bool
{
return $this->getDocumentsCount($model, $documentType) > 0;
}
/**
* Get supported file types.
*/
public function getSupportedFileTypes(): array
{
return [
'pdf' => 'application/pdf',
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'gif' => 'image/gif',
];
}
/**
* Get max file size in KB.
*/
public function getMaxFileSize(): int
{
return 10240; // 10MB
}
/**
* Validate file upload.
*/
public function validateFile(UploadedFile $file): bool
{
$supportedTypes = $this->getSupportedFileTypes();
$maxSize = $this->getMaxFileSize() * 1024; // Convert to bytes
// Check file size
if ($file->getSize() > $maxSize) {
throw new Exception('File size exceeds maximum limit of '.$this->getMaxFileSize().'KB');
}
// Check mime type
if (! in_array($file->getClientMimeType(), $supportedTypes)) {
throw new Exception('File type not supported. Supported types: '.implode(', ', array_keys($supportedTypes)));
}
return true;
}
/**
* Bulk delete documents for a model.
*/
public function bulkDeleteDocuments(Model $model, array $documentIds): int
{
$deletedCount = 0;
foreach ($documentIds as $documentId) {
$document = $model->documents()->find($documentId);
if ($document) {
$document->delete();
$deletedCount++;
}
}
return $deletedCount;
}
}
+212
View File
@@ -0,0 +1,212 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Storage;
use Modules\Auth\Entities\User;
use Carbon\Carbon;
use Laravel\Sanctum\PersonalAccessToken;
class OnlineUsersService
{
/**
* Get all currently online users
*
* @param int $timeoutMinutes Minutes of inactivity to consider user offline
* @return \Illuminate\Support\Collection
*/
public function getOnlineUsers(int $timeoutMinutes = 5)
{
$cacheKey = "online_users_{$timeoutMinutes}";
return Cache::remember($cacheKey, 30, function () use ($timeoutMinutes) {
$timeoutSeconds = $timeoutMinutes * 60;
$cutoffTime = now()->subSeconds($timeoutSeconds);
// Get active Sanctum tokens with user information
$onlineTokens = PersonalAccessToken::with(['tokenable.unit', 'tokenable.rank', 'tokenable.position'])
->where('tokenable_type', User::class)
->where('last_used_at', '>', $cutoffTime)
->where(function ($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->whereHas('tokenable', function ($query) {
$query->whereNull('deleted_at');
})
->orderBy('last_used_at', 'desc')
->get();
// Group by user to handle multiple tokens
$onlineUsers = $onlineTokens->groupBy('tokenable_id')->map(function ($tokens) {
$token = $tokens->first();
$user = $token->tokenable;
$latestToken = $tokens->sortByDesc('last_used_at')->first();
return [
'id' => $user->id,
'name' => $user->name,
'email' => $user->email,
'army_number' => $user->army_number,
'image_url' => $user->image_url ? Storage::disk('public')->url($user->image_url) : null,
'status' => $user->status,
'unit_name' => $user->unit?->name,
'rank_name' => $user->rank?->name,
'position_name' => $user->position?->name,
'ip_address' => 'N/A', // Sanctum doesn't store IP by default
'user_agent' => $this->parseUserAgent('Sanctum Token'),
'last_activity' => $latestToken->last_used_at?->timestamp ?? $latestToken->created_at->timestamp,
'last_activity_human' => $latestToken->last_used_at?->diffForHumans() ?? $latestToken->created_at->diffForHumans(),
'session_count' => $tokens->count(),
'is_online' => true
];
});
return $onlineUsers->values();
});
}
/**
* Get online users count
*
* @param int $timeoutMinutes Minutes of inactivity to consider user offline
* @return int
*/
public function getOnlineUsersCount(int $timeoutMinutes = 5): int
{
$cacheKey = "online_users_count_{$timeoutMinutes}";
return Cache::remember($cacheKey, 30, function () use ($timeoutMinutes) {
$timeoutSeconds = $timeoutMinutes * 60;
$cutoffTime = now()->subSeconds($timeoutSeconds);
return PersonalAccessToken::where('tokenable_type', User::class)
->where('last_used_at', '>', $cutoffTime)
->where(function ($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->whereHas('tokenable', function ($query) {
$query->whereNull('deleted_at');
})
->distinct('tokenable_id')
->count('tokenable_id');
});
}
/**
* Get user's current session information
*
* @param int $userId
* @return array|null
*/
public function getUserSessionInfo(int $userId): ?array
{
$token = PersonalAccessToken::where('tokenable_type', User::class)
->where('tokenable_id', $userId)
->where(function ($query) {
$query->whereNull('expires_at')
->orWhere('expires_at', '>', now());
})
->orderBy('last_used_at', 'desc')
->first();
if (!$token) {
return null;
}
return [
'session_id' => $token->id,
'ip_address' => 'N/A', // Sanctum doesn't store IP by default
'user_agent' => $this->parseUserAgent('Sanctum Token'),
'last_activity' => $token->last_used_at?->timestamp ?? $token->created_at->timestamp,
'last_activity_human' => $token->last_used_at?->diffForHumans() ?? $token->created_at->diffForHumans(),
'is_online' => $token->last_used_at && $token->last_used_at->gt(now()->subMinutes(5))
];
}
/**
* Parse user agent string to extract browser and OS info
*
* @param string $userAgent
* @return array
*/
private function parseUserAgent(string $userAgent): array
{
$browser = 'Unknown';
$os = 'Unknown';
// Simple browser detection
if (strpos($userAgent, 'Chrome') !== false) {
$browser = 'Chrome';
} elseif (strpos($userAgent, 'Firefox') !== false) {
$browser = 'Firefox';
} elseif (strpos($userAgent, 'Safari') !== false) {
$browser = 'Safari';
} elseif (strpos($userAgent, 'Edge') !== false) {
$browser = 'Edge';
}
// Simple OS detection
if (strpos($userAgent, 'Windows') !== false) {
$os = 'Windows';
} elseif (strpos($userAgent, 'Mac') !== false) {
$os = 'macOS';
} elseif (strpos($userAgent, 'Linux') !== false) {
$os = 'Linux';
} elseif (strpos($userAgent, 'Android') !== false) {
$os = 'Android';
} elseif (strpos($userAgent, 'iOS') !== false) {
$os = 'iOS';
}
return [
'browser' => $browser,
'os' => $os,
'raw' => $userAgent
];
}
/**
* Clear online users cache
*/
public function clearCache(): void
{
Cache::forget('online_users_5');
Cache::forget('online_users_10');
Cache::forget('online_users_15');
Cache::forget('online_users_30');
Cache::forget('online_users_count_5');
Cache::forget('online_users_count_10');
Cache::forget('online_users_count_15');
Cache::forget('online_users_count_30');
}
/**
* Get online users statistics
*
* @return array
*/
public function getOnlineUsersStats(): array
{
$stats = [];
// Get stats for different timeout periods
$timeouts = [5, 10, 15, 30];
foreach ($timeouts as $timeout) {
$stats["online_{$timeout}min"] = $this->getOnlineUsersCount($timeout);
}
// Get total registered users
$stats['total_users'] = User::count();
// Get users by status
$stats['active_users'] = User::where('status', 'active')->count();
$stats['inactive_users'] = User::where('status', 'inactive')->count();
return $stats;
}
}
+193
View File
@@ -0,0 +1,193 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
class SocialMediaService
{
/**
* Get all active social media platforms
*/
public function getActiveSocialMedia(): array
{
try {
$socialMedia = DB::table('social_media_settings')
->where('is_active', true)
->orderBy('sort_order', 'asc')
->orderBy('name', 'asc')
->get()
->toArray();
return array_map(function ($item) {
return [
'id' => $item->id,
'platform' => $item->platform,
'name' => $item->name,
'url' => $item->url,
'icon' => $item->icon,
'sort_order' => $item->sort_order,
];
}, $socialMedia);
} catch (\Exception $e) {
Log::error('Error fetching active social media: ' . $e->getMessage());
throw $e;
}
}
/**
* Get all social media settings (including inactive) for admin
*/
public function getAllSocialMediaSettings(): array
{
try {
$settings = DB::table('social_media_settings')
->orderBy('sort_order', 'asc')
->orderBy('name', 'asc')
->get()
->toArray();
return array_map(function ($item) {
return [
'id' => $item->id,
'platform' => $item->platform,
'name' => $item->name,
'url' => $item->url,
'icon' => $item->icon,
'is_active' => (bool) $item->is_active,
'sort_order' => $item->sort_order,
'created_at' => $item->created_at,
'updated_at' => $item->updated_at,
];
}, $settings);
} catch (\Exception $e) {
Log::error('Error fetching all social media settings: ' . $e->getMessage());
throw $e;
}
}
/**
* Update social media settings (bulk update)
*/
public function updateSocialMediaSettings(array $socialMediaData): array
{
try {
// Clear existing settings
DB::table('social_media_settings')->truncate();
// Insert new settings
$insertData = [];
foreach ($socialMediaData as $index => $item) {
$insertData[] = [
'platform' => $item['platform'],
'name' => $item['name'],
'url' => $item['url'],
'icon' => $item['icon'] ?? null,
'is_active' => $item['is_active'] ?? true,
'sort_order' => $item['sort_order'] ?? $index,
'created_at' => now(),
'updated_at' => now(),
];
}
DB::table('social_media_settings')->insert($insertData);
// Return updated settings
return $this->getAllSocialMediaSettings();
} catch (\Exception $e) {
Log::error('Error updating social media settings: ' . $e->getMessage());
throw $e;
}
}
/**
* Add a single social media platform
*/
public function addSocialMediaPlatform(array $data): array
{
try {
$id = DB::table('social_media_settings')->insertGetId([
'platform' => $data['platform'],
'name' => $data['name'],
'url' => $data['url'],
'icon' => $data['icon'] ?? null,
'is_active' => $data['is_active'] ?? true,
'sort_order' => $data['sort_order'] ?? 0,
'created_at' => now(),
'updated_at' => now(),
]);
return $this->getAllSocialMediaSettings();
} catch (\Exception $e) {
Log::error('Error adding social media platform: ' . $e->getMessage());
throw $e;
}
}
/**
* Update a single social media platform
*/
public function updateSocialMediaPlatform(int $id, array $data): array
{
try {
DB::table('social_media_settings')
->where('id', $id)
->update([
'platform' => $data['platform'],
'name' => $data['name'],
'url' => $data['url'],
'icon' => $data['icon'] ?? null,
'is_active' => $data['is_active'] ?? true,
'sort_order' => $data['sort_order'] ?? 0,
'updated_at' => now(),
]);
return $this->getAllSocialMediaSettings();
} catch (\Exception $e) {
Log::error('Error updating social media platform: ' . $e->getMessage());
throw $e;
}
}
/**
* Delete a social media platform
*/
public function deleteSocialMediaPlatform(int $id): array
{
try {
DB::table('social_media_settings')->where('id', $id)->delete();
return $this->getAllSocialMediaSettings();
} catch (\Exception $e) {
Log::error('Error deleting social media platform: ' . $e->getMessage());
throw $e;
}
}
/**
* Toggle active status of a social media platform
*/
public function toggleSocialMediaPlatform(int $id): array
{
try {
$platform = DB::table('social_media_settings')->where('id', $id)->first();
if (!$platform) {
throw new \Exception('Social media platform not found');
}
DB::table('social_media_settings')
->where('id', $id)
->update([
'is_active' => !$platform->is_active,
'updated_at' => now(),
]);
return $this->getAllSocialMediaSettings();
} catch (\Exception $e) {
Log::error('Error toggling social media platform: ' . $e->getMessage());
throw $e;
}
}
}
+304
View File
@@ -0,0 +1,304 @@
<?php
namespace App\Services;
use Modules\Formation\Entities\Formation;
use Illuminate\Database\Eloquent\Builder;
class VisibilityService
{
/**
* Apply visibility scoping to a query builder based on user permissions
*
* @param bool $includeUnitsWithoutFormation When true (process models), SEL PERO 91 REJ
* users can see units without formations. Government users never see units without formations.
*/
public function applyVisibilityToQuery(Builder $query, $user, string $unitColumn = 'unit_id', bool $includeUnitsWithoutFormation = false): Builder
{
// Check if user has akses peringkat keseluruhan permission (super admin)
if ($user->can('akses peringkat keseluruhan', Unit::class)) {
return $query;
}
// Get user's unit
$userUnit = Unit::find($user->unit_id);
if (!$userUnit) {
return $query->whereRaw('1 = 0');
}
// Government permission: Can view all units under their government
// Government users do NOT see units without formations
// SEL PERO 91 REJ (process models): ONLY see units without formation - not their gov/formation units
if ($user->can('akses peringkat formasi')) {
if ($includeUnitsWithoutFormation && $user->hasRole('SEL PERO 91 REJ')) {
$unitIds = Unit::whereNull('formation_id')
->whereNull('repair_formation_id')
->pluck('id')
->toArray();
} else {
$unitIds = $this->getUnitsUnderGovernment($userUnit);
$unitIds = $this->excludeUnitsWithoutFormation($unitIds);
}
if (!empty($unitIds)) {
return $query->whereIn($unitColumn, $unitIds);
}
return $query->whereRaw('1 = 0');
}
// DIV permission: Can view all units under their formation (including repair formations)
if ($user->can('akses peringkat divisyen')) {
$unitIds = $this->getVisibleUnitIdsForFormationUser($user, $userUnit, $includeUnitsWithoutFormation);
if (!empty($unitIds)) {
return $query->whereIn($unitColumn, $unitIds);
}
return $query->whereRaw('1 = 0');
}
// Unit permission: Can only view data within their unit
return $query->where($unitColumn, $user->unit_id);
}
/**
* Apply visibility scoping for models with indirect unit relationships
*
* @param bool $includeUnitsWithoutFormation When true (process models), SEL PERO 91 REJ
* users can see units without formations. Government users never see units without formations.
*/
public function applyVisibilityToIndirectQuery(Builder $query, $user, array $unitRelationship, bool $includeUnitsWithoutFormation = false): Builder
{
// Check if user has akses peringkat keseluruhan permission (super admin)
if ($user->can('akses peringkat keseluruhan', Unit::class)) {
return $query;
}
// Get user's unit
$userUnit = Unit::find($user->unit_id);
if (!$userUnit) {
return $query->whereRaw('1 = 0');
}
// Government permission: Can view all units under their government
// SEL PERO 91 REJ (process models): ONLY see units without formation - not their gov/formation units
if ($user->can('akses peringkat formasi')) {
if ($includeUnitsWithoutFormation && $user->hasRole('SEL PERO 91 REJ')) {
$unitIds = Unit::whereNull('formation_id')
->whereNull('repair_formation_id')
->pluck('id')
->toArray();
} else {
$unitIds = $this->getUnitsUnderGovernment($userUnit);
$unitIds = $this->excludeUnitsWithoutFormation($unitIds);
}
if (!empty($unitIds)) {
return $query->whereHas($unitRelationship['relationship'], function ($subQuery) use ($unitIds, $unitRelationship) {
$subQuery->whereIn($unitRelationship['unitColumn'], $unitIds);
});
}
return $query->whereRaw('1 = 0');
}
// DIV permission: Can view all units under their formation (including repair formations)
if ($user->can('akses peringkat divisyen')) {
$unitIds = $this->getVisibleUnitIdsForFormationUser($user, $userUnit, $includeUnitsWithoutFormation);
if (!empty($unitIds)) {
return $query->whereHas($unitRelationship['relationship'], function ($subQuery) use ($unitIds, $unitRelationship) {
$subQuery->whereIn($unitRelationship['unitColumn'], $unitIds);
});
}
return $query->whereRaw('1 = 0');
}
// Unit permission: Can only view data within their unit
return $query->whereHas($unitRelationship['relationship'], function ($subQuery) use ($user, $unitRelationship) {
$subQuery->where($unitRelationship['unitColumn'], $user->unit_id);
});
}
/**
* Get all units under the same government as the user's unit
* Handles both direct government relationships and formation-government relationships
* Priority: Direct government_id takes precedence over formation government
*
* @param Unit $userUnit
* @return array
*/
public function getUnitsUnderGovernment(Unit $userUnit): array
{
$unitIds = collect();
// Case 1: User's unit has direct government relationship (HIGHEST PRIORITY)
if ($userUnit->government_id) {
$directUnits = Unit::where('government_id', $userUnit->government_id)
->pluck('id');
$unitIds = $unitIds->merge($directUnits);
// Also get units in formations under the same direct government
$formationUnits = Unit::whereHas('formation', function ($query) use ($userUnit) {
$query->where('government_id', $userUnit->government_id);
})->pluck('id');
$unitIds = $unitIds->merge($formationUnits);
return $unitIds->unique()->toArray();
}
// Case 2: User's unit belongs to a formation that has a government (FALLBACK)
if ($userUnit->formation_id) {
$formation = Formation::find($userUnit->formation_id);
if ($formation && $formation->government_id) {
// Get all units in formations under the same government
$formationUnits = Unit::whereHas('formation', function ($query) use ($formation) {
$query->where('government_id', $formation->government_id);
})->pluck('id');
$unitIds = $unitIds->merge($formationUnits);
// Also get direct government units under the same government
$directGovUnits = Unit::where('government_id', $formation->government_id)
->pluck('id');
$unitIds = $unitIds->merge($directGovUnits);
}
}
return $unitIds->unique()->toArray();
}
/**
* Get the repair formation ID for a unit
* Priority: repair_formation_id > formation_id > null
*
* @param Unit $unit
* @return int|null
*/
public function getRepairFormationId(Unit $unit): ?int
{
// Priority 1: Check repair_formation_id (custom repair formation)
if ($unit->repair_formation_id) {
return $unit->repair_formation_id;
}
// Priority 2: Fall back to regular formation_id
if ($unit->formation_id) {
return $unit->formation_id;
}
// Priority 3: No formation (government-level only)
return null;
}
/**
* Get visible unit IDs for a user
*
* @param bool $includeUnitsWithoutFormation When true (process models), SEL PERO 91 REJ
* users can see units without formations. Government users never see units without formations.
*/
public function getVisibleUnitIds($user, bool $includeUnitsWithoutFormation = false): array
{
// Check if user has akses peringkat keseluruhan permission (super admin)
if ($user->can('akses peringkat keseluruhan', Unit::class)) {
return Unit::pluck('id')->toArray();
}
// Get user's unit
$userUnit = Unit::find($user->unit_id);
if (!$userUnit) {
return [];
}
// Government permission: Can view all units under their government
// SEL PERO 91 REJ (process models): ONLY see units without formation - not their gov/formation units
if ($user->can('akses peringkat formasi')) {
if ($includeUnitsWithoutFormation && $user->hasRole('SEL PERO 91 REJ')) {
return Unit::whereNull('formation_id')
->whereNull('repair_formation_id')
->pluck('id')
->toArray();
}
$unitIds = $this->getUnitsUnderGovernment($userUnit);
return $this->excludeUnitsWithoutFormation($unitIds);
}
// DIV permission: Can view all units under their formation (including repair formations)
if ($user->can('akses peringkat divisyen')) {
return $this->getVisibleUnitIdsForFormationUser($user, $userUnit, $includeUnitsWithoutFormation);
}
// Unit permission: Can only view data within their unit
return [$user->unit_id];
}
/**
* Exclude units that have no formation (both formation_id and repair_formation_id are null).
* Government users do not see these units.
*/
protected function excludeUnitsWithoutFormation(array $unitIds): array
{
if (empty($unitIds)) {
return [];
}
return Unit::whereIn('id', $unitIds)
->where(function ($q) {
$q->whereNotNull('formation_id')
->orWhereNotNull('repair_formation_id');
})
->pluck('id')
->toArray();
}
/**
* Get visible unit IDs for a user with akses peringkat divisyen permission.
*
* Asset view (includeUnitsWithoutFormation=false): Only units where formation_id matches.
* Excludes units that have repair_formation_id but formation_id null (e.g. 10 SKN RAJD uses
* 3 DIV for repair only - 3 DIV users see their repairs but NOT their asset data).
*
* Process view (includeUnitsWithoutFormation=true): Units where repair_formation_id OR
* formation_id matches. SEL PERO 91 REJ also sees units without any formation.
*/
protected function getVisibleUnitIdsForFormationUser($user, Unit $userUnit, bool $includeUnitsWithoutFormation): array
{
$userRepairFormationId = $this->getRepairFormationId($userUnit);
if ($userRepairFormationId) {
if (!$includeUnitsWithoutFormation) {
// Asset view: Only units that belong to the formation (formation_id matches).
// Exclude units that use formation only for repair (formation_id null, repair_formation_id set).
return Unit::where('formation_id', $userRepairFormationId)
->pluck('id')
->toArray();
}
// SEL PERO 91 REJ: ONLY see units without formation - not their formation's units
// (formation units are handled by SEL PERO DIV)
if ($user->hasRole('SEL PERO 91 REJ')) {
return Unit::whereNull('formation_id')
->whereNull('repair_formation_id')
->pluck('id')
->toArray();
}
// Process view: Units in user's formation (repair_formation_id or formation_id)
return Unit::where(function ($q) use ($userRepairFormationId) {
$q->where('repair_formation_id', $userRepairFormationId)
->orWhere(function ($q2) use ($userRepairFormationId) {
$q2->whereNull('repair_formation_id')
->where('formation_id', $userRepairFormationId);
});
})->pluck('id')->toArray();
}
// User's unit has no formation - check if they have SEL PERO 91 REJ (process models only)
if ($includeUnitsWithoutFormation && $user->hasRole('SEL PERO 91 REJ')) {
return Unit::whereNull('formation_id')
->whereNull('repair_formation_id')
->pluck('id')
->toArray();
}
return [];
}
}