Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+372
View File
@@ -0,0 +1,372 @@
<?php
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
/**
* Retrieve the active profile based on the session data.
*
* This function checks if the session contains 'activeProfileType' and
* 'activeProfileId'. If both are present, it attempts to find and return
* the profile using the specified type and ID. If either is missing,
* it returns null.
*
* @return mixed|null The active profile object if found, otherwise null.
*/
if (!function_exists('getActiveProfile')) {
function getActiveProfile()
{
$profileType = Session::get('activeProfileType');
$profileId = Session::get('activeProfileId');
if ($profileType && $profileId) {
return $profileType::find($profileId);
}
return null;
}
}
if (!function_exists('getActiveProfileType')) {
function getActiveProfileType()
{
$profileType = Session::get('activeProfileType');
$profileTypeName = class_basename($profileType);
if ($profileType && $profileTypeName) {
return $profileTypeName;
}
return null;
}
}
/**
* Check if the currently authenticated web user can create payments as the active profile.
*
* Users with the coordinator role (organization-coordinator / bank-coordinator) have
* full profile access except payment execution. Only manager roles may pay.
* User profiles are always allowed to pay.
*
* @return bool
*/
if (!function_exists('canActiveProfileCreatePayments')) {
function canActiveProfileCreatePayments(): bool
{
$activeType = Session::get('activeProfileType');
$activeId = Session::get('activeProfileId');
if (!$activeType || !$activeId) {
return false;
}
// User profiles can always pay
if ($activeType === 'App\Models\User') {
return true;
}
$user = Auth::guard('web')->user();
if (!$user) {
return false;
}
$managerRoles = [
'App\Models\Organization' => "Organization\\{$activeId}\\organization-manager",
'App\Models\Bank' => "Bank\\{$activeId}\\bank-manager",
];
if (!isset($managerRoles[$activeType])) {
return false;
}
return $user->hasRole($managerRoles[$activeType]);
}
}
/**
* Get the human-readable role label (English DB key) for a profile + web user combination.
* Returns a string suitable for passing to __() at render time.
* Stored in session as 'activeProfileRoleLabel' at switch time.
*/
if (!function_exists('getProfileRoleLabel')) {
function getProfileRoleLabel($profile, ?\App\Models\User $webUser = null): string
{
if (!$profile) {
return '';
}
$type = get_class($profile);
if ($type === \App\Models\User::class) {
return 'User';
}
if ($type === \App\Models\Admin::class) {
// Look up the Admin role the web user holds for this admin profile
if ($webUser) {
$prefix = "Admin\\{$profile->id}\\";
$spatieRole = $webUser->roles->first(fn($r) => str_starts_with($r->name, $prefix));
if ($spatieRole) {
$roleName = str_replace($prefix, '', $spatieRole->name);
$adminRole = \App\Models\AdminRole::where('name', $roleName)->first();
if ($adminRole?->label) {
return $adminRole->label;
}
}
}
return 'Administrator';
}
if ($type === \App\Models\Organization::class) {
if ($webUser) {
$prefix = "Organization\\{$profile->id}\\";
$spatieRole = $webUser->roles->first(fn($r) => str_starts_with($r->name, $prefix));
if ($spatieRole) {
$roleName = str_replace($prefix, '', $spatieRole->name);
$orgRole = \App\Models\OrganizationRole::where('name', $roleName)->first();
if ($orgRole?->label) {
return $orgRole->label;
}
}
}
return 'Organization Manager';
}
if ($type === \App\Models\Bank::class) {
if ($webUser) {
$prefix = "Bank\\{$profile->id}\\";
$spatieRole = $webUser->roles->first(fn($r) => str_starts_with($r->name, $prefix));
if ($spatieRole) {
$roleName = str_replace($prefix, '', $spatieRole->name);
$orgRole = \App\Models\OrganizationRole::where('name', $roleName)->first();
if ($orgRole?->label) {
return $orgRole->label;
}
}
}
return 'Bank Manager';
}
return '';
}
}
/**
* Build the session key for the active profile's location hierarchy.
* Keyed by both profile type and ID so switching profiles never serves stale data.
*/
if (!function_exists('activeProfileLocationSessionKey')) {
function activeProfileLocationSessionKey(): ?string
{
$profileType = Session::get('activeProfileType');
$profileId = Session::get('activeProfileId');
if (!$profileType || !$profileId) {
return null;
}
return 'profile_location_hierarchy_' . class_basename($profileType) . '_' . $profileId;
}
}
/**
* Get the active profile's resolved location hierarchy, using the session as a cache.
* Returns [] if no location is set.
*/
if (!function_exists('getActiveProfileLocationHierarchy')) {
function getActiveProfileLocationHierarchy(): array
{
$key = activeProfileLocationSessionKey();
if (!$key) {
return [];
}
if (Session::has($key)) {
return Session::get($key);
}
try {
$activeProfile = getActiveProfile();
if (!$activeProfile) {
\Illuminate\Support\Facades\Log::debug('getActiveProfileLocationHierarchy: no active profile');
return [];
}
if (!$activeProfile->locations) {
\Illuminate\Support\Facades\Log::debug('getActiveProfileLocationHierarchy: profile has no locations relation');
return [];
}
$location = $activeProfile->locations->first();
if (!$location) {
\Illuminate\Support\Facades\Log::debug('getActiveProfileLocationHierarchy: no location record found', ['profile_id' => $activeProfile->id]);
return [];
}
// Resolve full hierarchy once to sync missing IDs, then extract only what's needed.
// We store a flat array of IDs and names — no model objects — to keep the session small.
$hierarchy = $location->getCompleteHierarchy();
$locale = app()->getLocale();
\Illuminate\Support\Facades\Log::debug('getActiveProfileLocationHierarchy: hierarchy resolved', [
'country_id' => $hierarchy['country']->id ?? null,
'division_id' => $hierarchy['division']->id ?? null,
'city_id' => $hierarchy['city']->id ?? null,
'district_id' => $hierarchy['district']->id ?? null,
'locale' => $locale,
]);
$resolveName = function ($model) use ($locale) {
if (!$model) return null;
if ($model->translations && $model->translations->count() > 0) {
$t = $model->translations->where('locale', $locale)->first();
if ($t && $t->name) return $t->name;
}
return $model->name ?? null;
};
$flat = [
'country_id' => $hierarchy['country']->id ?? null,
'division_id' => $hierarchy['division']->id ?? null,
'city_id' => $hierarchy['city']->id ?? null,
'district_id' => $hierarchy['district']->id ?? null,
'country_name' => $resolveName($hierarchy['country'] ?? null),
'division_name' => $resolveName($hierarchy['division'] ?? null),
'city_name' => $resolveName($hierarchy['city'] ?? null),
'district_name' => $resolveName($hierarchy['district'] ?? null),
];
\Illuminate\Support\Facades\Log::debug('getActiveProfileLocationHierarchy: storing in session', ['key' => $key, 'flat' => $flat]);
Session::put($key, $flat);
return $flat;
} catch (\Exception $e) {
\Illuminate\Support\Facades\Log::warning('getActiveProfileLocationHierarchy failed: ' . $e->getMessage());
return [];
}
}
}
/**
* Invalidate the session-cached location hierarchy for the active profile.
* Call this immediately after saving a profile's location.
*/
if (!function_exists('invalidateActiveProfileLocationCache')) {
function invalidateActiveProfileLocationCache(): void
{
$key = activeProfileLocationSessionKey();
if ($key) {
Session::forget($key);
}
}
}
/**
* Check if the active viewer can see a bank of a given level for a specific context.
*
* Reads `banks.visibility.{level}.{viewerType}.{visibilityKey}` from the platform config.
* Returns true (visible) when the config key is absent — opt-in hiding only.
*
* @param int $bankLevel The bank's level (0, 1, 2, …)
* @param string $visibilityKey One of: profile_visible, search_visible, messenger_visible, payment_visible
* @param string|null $viewerType Explicit viewer type override; defaults to active profile type or 'guest'
* @return bool
*/
if (!function_exists('canViewerSeeBankLevel')) {
function canViewerSeeBankLevel(?int $bankLevel, string $visibilityKey, ?string $viewerType = null): bool
{
if ($bankLevel === null) {
return true;
}
if ($viewerType === null) {
$viewerType = getActiveProfileType() ?? 'guest';
}
$configured = timebank_config("banks.visibility.{$bankLevel}.{$viewerType}.{$visibilityKey}");
// If the config key is absent, default to visible (safe fallback)
if ($configured === null) {
return true;
}
return (bool) $configured;
}
}
/**
* Check if the authenticated web user has multiple profiles (organizations, banks, admins).
* Returns true if user has 2+ profiles total (User + any elevated profiles).
*
* @return bool
*/
if (!function_exists('userHasMultipleProfiles')) {
function userHasMultipleProfiles(): bool
{
$user = Auth::guard('web')->user();
if (!$user) {
return false;
}
$profileCount = 0;
// User profile counts as 1
$profileCount++;
// Add organizations
$organizations = $user->organizations()
->where(fn($q) => $q->whereNull('deleted_at')->orWhere('deleted_at', '>', now()))
->count();
$profileCount += $organizations;
// Add banks
$banks = $user->banksManaged()
->where(fn($q) => $q->whereNull('deleted_at')->orWhere('deleted_at', '>', now()))
->count();
$profileCount += $banks;
// Add admins
$admins = $user->admins()
->where(fn($q) => $q->whereNull('deleted_at')->orWhere('deleted_at', '>', now()))
->count();
$profileCount += $admins;
return $profileCount >= 2;
}
}
/**
* Resolve the correct translation key for the "I/we just transferred..."
* chat message sent after a payment, based on the SENDER's profile type.
* Organizations and Banks are plural entities ("we"); Users are singular ("I").
*
* @param mixed $sender The Eloquent model that sent the payment (User/Organization/Bank).
* @return string
*/
if (!function_exists('payChatMessageKey')) {
function payChatMessageKey($sender): string
{
if ($sender instanceof \App\Models\Organization || $sender instanceof \App\Models\Bank) {
return 'messages.pay_chat_message_plural';
}
return 'messages.pay_chat_message';
}
}
/**
* Check if the system is in maintenance mode.
*
* @return bool
*/
if (!function_exists('isMaintenanceMode')) {
function isMaintenanceMode()
{
return \Illuminate\Support\Facades\Cache::remember('system_setting_maintenance_mode', 300, function () {
$setting = \Illuminate\Support\Facades\DB::table('system_settings')
->where('key', 'maintenance_mode')
->first();
return $setting ? $setting->value === 'true' : false;
});
}
}