initial: bootstrap from BukidBountyApp base
This commit is contained in:
296
app/Support/AccountingTheme.php
Normal file
296
app/Support/AccountingTheme.php
Normal file
@@ -0,0 +1,296 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\Accounting\Account;
|
||||
use App\Models\SystemSetting;
|
||||
use Hypervel\Support\Facades\Config;
|
||||
use Hypervel\Support\Str;
|
||||
|
||||
/**
|
||||
* Accounting theme registry.
|
||||
*
|
||||
* Themes are defined in config/accounting/themes.php and identified by the
|
||||
* `accounting_theme` system setting. Theme identity travels with the data:
|
||||
* accounts seeded from a theme are stamped with theme_key + theme_account_code
|
||||
* so we can branch on the active theme and detect drift later, even after
|
||||
* users edit the chart through admin CRUD.
|
||||
*/
|
||||
class AccountingTheme
|
||||
{
|
||||
public const DEFAULT_KEY = 'blank';
|
||||
|
||||
public const FALLBACK_KEY = 'blank';
|
||||
|
||||
/**
|
||||
* Active theme key for this deployment.
|
||||
*/
|
||||
public static function current(): string
|
||||
{
|
||||
$key = SystemSetting::getValue('accounting_theme', self::DEFAULT_KEY);
|
||||
|
||||
if (!\is_string($key) || $key === '' || !\array_key_exists($key, self::all())) {
|
||||
return self::FALLBACK_KEY;
|
||||
}
|
||||
|
||||
return $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the active theme matches the given key.
|
||||
*/
|
||||
public static function is(string $key): bool
|
||||
{
|
||||
return self::current() === $key;
|
||||
}
|
||||
|
||||
/**
|
||||
* All registered theme definitions, keyed by theme key.
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
$themes = Config::get('accounting.themes', []);
|
||||
return \is_array($themes) ? $themes : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The definition for a single theme (or the active theme when no key given).
|
||||
*
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public static function definition(?string $key = null): ?array
|
||||
{
|
||||
$key = $key ?? self::current();
|
||||
$themes = self::all();
|
||||
return $themes[$key] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The starter Chart of Accounts hierarchy for a theme.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function tree(?string $key = null): array
|
||||
{
|
||||
$definition = self::definition($key);
|
||||
$tree = $definition['tree'] ?? [];
|
||||
return \is_array($tree) ? $tree : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an account name to a stable code for the given theme.
|
||||
* Used so re-applying a theme can match existing seeded accounts even if
|
||||
* a user has renamed them.
|
||||
*/
|
||||
public static function accountCode(string $name): string
|
||||
{
|
||||
$slug = strtolower(trim($name));
|
||||
$slug = preg_replace('/[^a-z0-9]+/', '_', $slug) ?? '';
|
||||
return trim($slug, '_');
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten a theme tree into a list of definitions keyed by code.
|
||||
*
|
||||
* Each entry: ['code', 'name', 'type', 'default_flow', 'parent_code', 'description'].
|
||||
*
|
||||
* @return array<string, array<string, mixed>>
|
||||
*/
|
||||
public static function flatten(?string $key = null): array
|
||||
{
|
||||
$flat = [];
|
||||
self::flattenWalk(self::tree($key), null, $flat);
|
||||
return $flat;
|
||||
}
|
||||
|
||||
private static function flattenWalk(array $nodes, ?string $parentCode, array &$flat): void
|
||||
{
|
||||
foreach ($nodes as $node) {
|
||||
$name = (string) ($node['name'] ?? '');
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
$code = self::accountCode($name);
|
||||
$type = strtoupper((string) ($node['type'] ?? ''));
|
||||
$flat[$code] = [
|
||||
'code' => $code,
|
||||
'name' => $name,
|
||||
'type' => $type,
|
||||
'default_flow' => $node['default_flow'] ?? (($type === 'REVENUE' || $type === 'LIABILITY') ? 'INCOME' : 'EXPENSE'),
|
||||
'parent_code' => $parentCode,
|
||||
'description' => $node['description'] ?? null,
|
||||
];
|
||||
if (!empty($node['children']) && \is_array($node['children'])) {
|
||||
self::flattenWalk($node['children'], $code, $flat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare the active theme against the DB. Pure read — never mutates.
|
||||
*
|
||||
* Returns:
|
||||
* - missing: codes in the theme but absent from DB (need apply)
|
||||
* - present: codes in both, name matches
|
||||
* - renamed: codes in both but name differs in DB (user-edited)
|
||||
* - user_added: account ids/names with theme_key NULL (custom additions)
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function drift(?string $key = null): array
|
||||
{
|
||||
$key = $key ?? self::current();
|
||||
$themeAccounts = self::flatten($key);
|
||||
|
||||
$rows = Account::where('theme_key', $key)
|
||||
->get(['id', 'name', 'theme_account_code', 'is_active', 'parent_id'])
|
||||
->keyBy('theme_account_code');
|
||||
|
||||
$missing = [];
|
||||
$present = [];
|
||||
$renamed = [];
|
||||
foreach ($themeAccounts as $code => $def) {
|
||||
$row = $rows[$code] ?? null;
|
||||
if (!$row) {
|
||||
$missing[] = ['code' => $code, 'name' => $def['name']];
|
||||
continue;
|
||||
}
|
||||
if ($row->name !== $def['name']) {
|
||||
$renamed[] = ['code' => $code, 'expected' => $def['name'], 'current' => $row->name, 'id' => $row->id];
|
||||
} else {
|
||||
$present[] = ['code' => $code, 'name' => $def['name'], 'id' => $row->id];
|
||||
}
|
||||
}
|
||||
|
||||
$userAdded = Account::whereNull('theme_key')
|
||||
->orderBy('id')
|
||||
->get(['id', 'name', 'parent_id'])
|
||||
->map(fn($r) => ['id' => $r->id, 'name' => $r->name, 'parent_id' => $r->parent_id])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'theme_key' => $key,
|
||||
'missing' => $missing,
|
||||
'present' => $present,
|
||||
'renamed' => $renamed,
|
||||
'user_added' => $userAdded,
|
||||
'totals' => [
|
||||
'expected' => \count($themeAccounts),
|
||||
'missing' => \count($missing),
|
||||
'present' => \count($present),
|
||||
'renamed' => \count($renamed),
|
||||
'user_added' => \count($userAdded),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Idempotently apply the active (or specified) theme to the DB.
|
||||
*
|
||||
* Never deletes, never renames user edits. For each theme node:
|
||||
* - if a row matches by (theme_key, theme_account_code), stamp/refresh metadata
|
||||
* - else if a row matches by (parent_id, name), back-stamp it
|
||||
* - else create a new row
|
||||
*
|
||||
* @return array{created:int, stamped:int, skipped:int, theme_key:string}
|
||||
*/
|
||||
public static function apply(?string $key = null): array
|
||||
{
|
||||
$key = $key ?? self::current();
|
||||
$tree = self::tree($key);
|
||||
|
||||
$stats = ['created' => 0, 'stamped' => 0, 'skipped' => 0, 'theme_key' => $key];
|
||||
foreach ($tree as $node) {
|
||||
self::applyNode($node, null, $key, $stats);
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private static function applyNode(array $node, ?int $parentId, string $themeKey, array &$stats): void
|
||||
{
|
||||
$name = (string) ($node['name'] ?? '');
|
||||
if ($name === '') {
|
||||
return;
|
||||
}
|
||||
$code = self::accountCode($name);
|
||||
$type = strtoupper((string) ($node['type'] ?? ''));
|
||||
$defaultFlow = $node['default_flow']
|
||||
?? (($type === 'REVENUE' || $type === 'LIABILITY') ? 'INCOME' : 'EXPENSE');
|
||||
|
||||
$account = Account::where('theme_key', $themeKey)
|
||||
->where('theme_account_code', $code)
|
||||
->where(function ($q) use ($parentId) {
|
||||
if ($parentId === null) {
|
||||
$q->whereNull('parent_id');
|
||||
} else {
|
||||
$q->where('parent_id', $parentId);
|
||||
}
|
||||
})
|
||||
->first();
|
||||
|
||||
if (!$account) {
|
||||
$account = Account::where('parent_id', $parentId)
|
||||
->where('name', $name)
|
||||
->first();
|
||||
}
|
||||
|
||||
if ($account) {
|
||||
$dirty = false;
|
||||
if ($account->theme_key !== $themeKey) { $account->theme_key = $themeKey; $dirty = true; }
|
||||
if ($account->theme_account_code !== $code) { $account->theme_account_code = $code; $dirty = true; }
|
||||
if (empty($account->default_flow)) { $account->default_flow = $defaultFlow; $dirty = true; }
|
||||
if (empty($account->description) && !empty($node['description'])) {
|
||||
$account->description = $node['description'];
|
||||
$dirty = true;
|
||||
}
|
||||
if ($dirty) {
|
||||
$account->save();
|
||||
$stats['stamped']++;
|
||||
} else {
|
||||
$stats['skipped']++;
|
||||
}
|
||||
} else {
|
||||
$account = Account::create([
|
||||
'hashkey' => Str::random(40),
|
||||
'parent_id' => $parentId,
|
||||
'type' => $type,
|
||||
'default_flow' => $defaultFlow,
|
||||
'name' => $name,
|
||||
'description' => $node['description'] ?? null,
|
||||
'theme_key' => $themeKey,
|
||||
'theme_account_code' => $code,
|
||||
'is_active' => true,
|
||||
]);
|
||||
$stats['created']++;
|
||||
}
|
||||
|
||||
if (!empty($node['children']) && \is_array($node['children'])) {
|
||||
foreach ($node['children'] as $child) {
|
||||
self::applyNode($child, $account->id, $themeKey, $stats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Lightweight summary of all themes for admin UI dropdowns.
|
||||
*
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function options(): array
|
||||
{
|
||||
$options = [];
|
||||
foreach (self::all() as $key => $def) {
|
||||
$options[] = [
|
||||
'key' => $key,
|
||||
'label' => $def['label'] ?? $key,
|
||||
'description' => $def['description'] ?? null,
|
||||
'version' => $def['version'] ?? 1,
|
||||
];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
}
|
||||
24
app/Support/AppVersion.php
Normal file
24
app/Support/AppVersion.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class AppVersion
|
||||
{
|
||||
/**
|
||||
* Get the current app asset version derived from the Vite manifest's last modified time.
|
||||
*/
|
||||
public static function get(): string
|
||||
{
|
||||
// Try manifest path
|
||||
$manifestPath = BASE_PATH . '/public/build/.vite/manifest.json';
|
||||
|
||||
if (file_exists($manifestPath)) {
|
||||
return (string) filemtime($manifestPath);
|
||||
}
|
||||
|
||||
// Fallback for non-build environments
|
||||
return '1';
|
||||
}
|
||||
}
|
||||
46
app/Support/CdnAssetHelper.php
Normal file
46
app/Support/CdnAssetHelper.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
final class CdnAssetHelper
|
||||
{
|
||||
private static ?array $manifest = null;
|
||||
|
||||
private static function load(): array
|
||||
{
|
||||
if (self::$manifest === null) {
|
||||
$path = BASE_PATH . '/resources/cdn-manifest.json';
|
||||
$raw = @file_get_contents($path);
|
||||
if ($raw === false) {
|
||||
self::$manifest = ['assets' => []];
|
||||
} else {
|
||||
$decoded = json_decode($raw, true);
|
||||
self::$manifest = is_array($decoded) ? $decoded : ['assets' => []];
|
||||
}
|
||||
}
|
||||
return self::$manifest;
|
||||
}
|
||||
|
||||
public static function url(string $logicalName): string
|
||||
{
|
||||
$base = (string) config('cdn.base');
|
||||
$manifest = self::load();
|
||||
$path = $manifest['assets'][$logicalName] ?? null;
|
||||
if ($path === null) {
|
||||
return $base . '/missing/' . ltrim($logicalName, '/');
|
||||
}
|
||||
return $base . '/' . ltrim($path, '/');
|
||||
}
|
||||
|
||||
public static function base(): string
|
||||
{
|
||||
return (string) config('cdn.base');
|
||||
}
|
||||
|
||||
public static function manifestForJs(): array
|
||||
{
|
||||
return self::load()['assets'] ?? [];
|
||||
}
|
||||
}
|
||||
66
app/Support/HasApiTokens.php
Normal file
66
app/Support/HasApiTokens.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\PersonalAccessToken;
|
||||
use Hyperf\Database\Model\Relations\MorphMany;
|
||||
|
||||
trait HasApiTokens
|
||||
{
|
||||
public function tokens(): MorphMany
|
||||
{
|
||||
return $this->morphMany(PersonalAccessToken::class, 'tokenable');
|
||||
}
|
||||
|
||||
public function currentAccessToken(): ?PersonalAccessToken
|
||||
{
|
||||
return $this->accessToken ?? null;
|
||||
}
|
||||
|
||||
public function withAccessToken(?PersonalAccessToken $token): static
|
||||
{
|
||||
$this->accessToken = $token;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new token.
|
||||
*
|
||||
* @param array<int, string> $abilities
|
||||
* @param array<int, string> $allowedIps
|
||||
* @return array{token: PersonalAccessToken, plainTextToken: string}
|
||||
*/
|
||||
public function createToken(
|
||||
string $name,
|
||||
array $abilities,
|
||||
?array $allowedIps = null,
|
||||
?\DateTimeInterface $expiresAt = null,
|
||||
?string $description = null,
|
||||
?int $createdBy = null,
|
||||
): array {
|
||||
foreach ($abilities as $ability) {
|
||||
if (! TokenAbilities::exists($ability)) {
|
||||
throw new \InvalidArgumentException("Unknown ability: {$ability}");
|
||||
}
|
||||
}
|
||||
|
||||
$plain = bin2hex(random_bytes(32));
|
||||
|
||||
$token = $this->tokens()->create([
|
||||
'name' => $name,
|
||||
'description' => $description,
|
||||
'token' => hash('sha256', $plain),
|
||||
'abilities' => array_values(array_unique($abilities)),
|
||||
'allowed_ips' => $allowedIps ? array_values(array_filter(array_map('trim', $allowedIps))) : null,
|
||||
'expires_at' => $expiresAt,
|
||||
'created_by' => $createdBy,
|
||||
]);
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'plainTextToken' => $token->id . '|' . $plain,
|
||||
];
|
||||
}
|
||||
}
|
||||
132
app/Support/HashkeyResolver.php
Normal file
132
app/Support/HashkeyResolver.php
Normal file
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Hypervel\Codec\Json;
|
||||
use App\Models\User;
|
||||
use App\Models\Market\Product;
|
||||
use App\Models\Market\Store;
|
||||
|
||||
/**
|
||||
* HashkeyResolver provides a universal resolver for any model by hashkey.
|
||||
* This allows resolving resources (User, Product, Store, etc.) without knowing
|
||||
* the specific model class upfront.
|
||||
*/
|
||||
class HashkeyResolver
|
||||
{
|
||||
/**
|
||||
* Resolve a resource by its hashkey.
|
||||
* This is a generic method that works with any model class.
|
||||
*
|
||||
* @param string $hashkey The hashkey to resolve
|
||||
* @param string $modelClass The model class name (e.g., 'App\Models\User')
|
||||
* @return mixed|null The resolved model instance or null if not found
|
||||
*/
|
||||
public function resolveByHashkey($hashkey, $modelClass)
|
||||
{
|
||||
if (!$hashkey || empty($modelClass)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Use the model's static where method to find by hashkey
|
||||
try {
|
||||
return $modelClass::where('hashkey', $hashkey)->first();
|
||||
} catch (\Exception $e) {
|
||||
error_log("[HashkeyResolver] Error resolving {$modelClass} by hashkey: " . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a user by their hashkey.
|
||||
*
|
||||
* @param string $hashkey The user's hashkey
|
||||
* @return mixed|null The resolved User instance or null if not found
|
||||
*/
|
||||
public function resolveUserByHashkey($hashkey)
|
||||
{
|
||||
return $this->resolveByHashkey($hashkey, User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a product by its hashkey.
|
||||
*
|
||||
* @param string $hashkey The product's hashkey
|
||||
* @return mixed|null The resolved Product instance or null if not found
|
||||
*/
|
||||
public function resolveProductByHashkey($hashkey)
|
||||
{
|
||||
return $this->resolveByHashkey($hashkey, Product::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a store by its hashkey.
|
||||
*
|
||||
* @param string $hashkey The store's hashkey
|
||||
* @return mixed|null The resolved Store instance or null if not found
|
||||
*/
|
||||
public function resolveStoreByHashkey($hashkey)
|
||||
{
|
||||
return $this->resolveByHashkey($hashkey, Store::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a model exists by its hashkey.
|
||||
*
|
||||
* @param string $hashkey The resource's hashkey
|
||||
* @param string $modelClass The model class name
|
||||
* @return bool True if the resource exists, false otherwise
|
||||
*/
|
||||
public function existsByHashkey($hashkey, $modelClass)
|
||||
{
|
||||
if (!$hashkey || empty($modelClass)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return $modelClass::where('hashkey', $hashkey)->exists();
|
||||
} catch (\Exception $e) {
|
||||
error_log("[HashkeyResolver] Error checking existence for {$modelClass}: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve multiple resources by their hashkeys.
|
||||
*
|
||||
* @param array $hashkeys Array of hashkeys to resolve
|
||||
* @param string $modelClass The model class name
|
||||
* @return array Collection of resolved models
|
||||
*/
|
||||
public function resolveMultipleByHashkey($hashkeys, $modelClass)
|
||||
{
|
||||
if (!is_array($hashkeys) || empty($hashkeys)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
return $modelClass::where('hashkey', '!=', null)
|
||||
->whereIn('hashkey', $hashkeys)
|
||||
->get();
|
||||
} catch (\Exception $e) {
|
||||
error_log("[HashkeyResolver] Error resolving multiple by hashkey: " . $e->getMessage());
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hashkey from request input (from various sources).
|
||||
* This helper method looks for 'hashkey' or 'target' in the request.
|
||||
*
|
||||
* @param \Hypervel\Http\Request $request The request object
|
||||
* @return string|null The hashkey value or null
|
||||
*/
|
||||
public function getHashkeyFromRequest($request)
|
||||
{
|
||||
$hashkey = $request->input('hashkey') ?: $request->input('target');
|
||||
|
||||
return !empty($hashkey) ? $hashkey : null;
|
||||
}
|
||||
}
|
||||
123
app/Support/IslandGroupHelper.php
Normal file
123
app/Support/IslandGroupHelper.php
Normal file
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class IslandGroupHelper
|
||||
{
|
||||
public const LUZON = 'luzon';
|
||||
public const VISAYAS = 'visayas';
|
||||
public const MINDANAO = 'mindanao';
|
||||
|
||||
/**
|
||||
* Map of region location_key (lowercased) → island group key.
|
||||
* Covers common name variants (e.g., "ncr" / "national capital region").
|
||||
*/
|
||||
private const REGION_TO_ISLAND = [
|
||||
// Luzon
|
||||
'ncr' => self::LUZON,
|
||||
'national capital region' => self::LUZON,
|
||||
'metro manila' => self::LUZON,
|
||||
'car' => self::LUZON,
|
||||
'cordillera administrative region' => self::LUZON,
|
||||
'region i' => self::LUZON,
|
||||
'region 1' => self::LUZON,
|
||||
'ilocos region' => self::LUZON,
|
||||
'region ii' => self::LUZON,
|
||||
'region 2' => self::LUZON,
|
||||
'cagayan valley' => self::LUZON,
|
||||
'region iii' => self::LUZON,
|
||||
'region 3' => self::LUZON,
|
||||
'central luzon' => self::LUZON,
|
||||
'region iv-a' => self::LUZON,
|
||||
'region 4-a' => self::LUZON,
|
||||
'region iva' => self::LUZON,
|
||||
'calabarzon' => self::LUZON,
|
||||
'region iv-b' => self::LUZON,
|
||||
'region 4-b' => self::LUZON,
|
||||
'region ivb' => self::LUZON,
|
||||
'mimaropa' => self::LUZON,
|
||||
'mimaropa region' => self::LUZON,
|
||||
'region v' => self::LUZON,
|
||||
'region 5' => self::LUZON,
|
||||
'bicol region' => self::LUZON,
|
||||
|
||||
// Visayas
|
||||
'region vi' => self::VISAYAS,
|
||||
'region 6' => self::VISAYAS,
|
||||
'western visayas' => self::VISAYAS,
|
||||
'region vii' => self::VISAYAS,
|
||||
'region 7' => self::VISAYAS,
|
||||
'central visayas' => self::VISAYAS,
|
||||
'region viii' => self::VISAYAS,
|
||||
'region 8' => self::VISAYAS,
|
||||
'eastern visayas' => self::VISAYAS,
|
||||
|
||||
// Mindanao
|
||||
'region ix' => self::MINDANAO,
|
||||
'region 9' => self::MINDANAO,
|
||||
'zamboanga peninsula' => self::MINDANAO,
|
||||
'region x' => self::MINDANAO,
|
||||
'region 10' => self::MINDANAO,
|
||||
'northern mindanao' => self::MINDANAO,
|
||||
'region xi' => self::MINDANAO,
|
||||
'region 11' => self::MINDANAO,
|
||||
'davao region' => self::MINDANAO,
|
||||
'region xii' => self::MINDANAO,
|
||||
'region 12' => self::MINDANAO,
|
||||
'soccsksargen' => self::MINDANAO,
|
||||
'region xiii' => self::MINDANAO,
|
||||
'region 13' => self::MINDANAO,
|
||||
'caraga' => self::MINDANAO,
|
||||
'barmm' => self::MINDANAO,
|
||||
'bangsamoro' => self::MINDANAO,
|
||||
'bangsamoro autonomous region in muslim mindanao' => self::MINDANAO,
|
||||
'armm' => self::MINDANAO,
|
||||
];
|
||||
|
||||
private const ISLAND_LABELS = [
|
||||
self::LUZON => 'Luzon',
|
||||
self::VISAYAS => 'Visayas',
|
||||
self::MINDANAO => 'Mindanao',
|
||||
];
|
||||
|
||||
private const ISLAND_CENTERS = [
|
||||
self::LUZON => ['lat' => 16.5, 'lng' => 121.0],
|
||||
self::VISAYAS => ['lat' => 11.0, 'lng' => 123.5],
|
||||
self::MINDANAO => ['lat' => 7.5, 'lng' => 124.5],
|
||||
];
|
||||
|
||||
public static function islands(): array
|
||||
{
|
||||
return [self::LUZON, self::VISAYAS, self::MINDANAO];
|
||||
}
|
||||
|
||||
public static function label(string $island): string
|
||||
{
|
||||
return self::ISLAND_LABELS[$island] ?? ucfirst($island);
|
||||
}
|
||||
|
||||
public static function center(string $island): array
|
||||
{
|
||||
return self::ISLAND_CENTERS[$island] ?? ['lat' => 12.0, 'lng' => 122.5];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an island group from a region's location_key or name.
|
||||
* Returns null if it can't be matched (caller should bucket as "unassigned").
|
||||
*/
|
||||
public static function fromRegion(?string $regionKeyOrName): ?string
|
||||
{
|
||||
if (!$regionKeyOrName) return null;
|
||||
$key = strtolower(trim($regionKeyOrName));
|
||||
if (isset(self::REGION_TO_ISLAND[$key])) {
|
||||
return self::REGION_TO_ISLAND[$key];
|
||||
}
|
||||
// Loose substring match for entries like "Region IV-A (CALABARZON)"
|
||||
foreach (self::REGION_TO_ISLAND as $needle => $island) {
|
||||
if (str_contains($key, $needle)) return $island;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
139
app/Support/ModuleHelper.php
Normal file
139
app/Support/ModuleHelper.php
Normal file
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\SystemSetting;
|
||||
use Hypervel\Support\Facades\Config;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Helper for checking module enable/disable status.
|
||||
*
|
||||
* Resolution order (highest priority first):
|
||||
* 1. SystemSetting override (key: "module_states_override", JSON map)
|
||||
* 2. config/modules.php (env-driven defaults)
|
||||
*
|
||||
* The DB-backed override allows the Ultimate Console to toggle modules at
|
||||
* runtime without redeploying / changing env vars.
|
||||
*/
|
||||
class ModuleHelper
|
||||
{
|
||||
/**
|
||||
* SystemSetting key that holds the per-module override map.
|
||||
*/
|
||||
public const OVERRIDE_KEY = 'module_states_override';
|
||||
|
||||
public static function isSystemEnabled(): bool
|
||||
{
|
||||
return (bool) Config::get('modules.system_enabled', true);
|
||||
}
|
||||
|
||||
public static function isEnabled(string $moduleKey): bool
|
||||
{
|
||||
if (!static::isSystemEnabled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$overrides = static::getOverrides();
|
||||
if (array_key_exists($moduleKey, $overrides)) {
|
||||
return (bool) $overrides[$moduleKey];
|
||||
}
|
||||
|
||||
return (bool) Config::get("modules.{$moduleKey}.enabled", true);
|
||||
}
|
||||
|
||||
public static function isDisabled(string $moduleKey): bool
|
||||
{
|
||||
return !static::isEnabled($moduleKey);
|
||||
}
|
||||
|
||||
public static function getLabel(string $moduleKey): string
|
||||
{
|
||||
return Config::get("modules.{$moduleKey}.label", ucfirst($moduleKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all module statuses including the effective enabled state, the
|
||||
* config/env default, and the override value (if any).
|
||||
*
|
||||
* Returns: ['key' => [
|
||||
* 'enabled' => bool, // effective state
|
||||
* 'config_default' => bool, // env / config value
|
||||
* 'override' => bool|null, // SystemSetting override (null if none)
|
||||
* 'label' => string,
|
||||
* 'description' => string,
|
||||
* ], ...]
|
||||
*/
|
||||
public static function getAllModules(): array
|
||||
{
|
||||
$modules = Config::get('modules', []);
|
||||
$overrides = static::getOverrides();
|
||||
$result = [];
|
||||
|
||||
foreach ($modules as $key => $config) {
|
||||
if (!is_array($config)) {
|
||||
// Skips scalar entries like 'system_enabled'.
|
||||
continue;
|
||||
}
|
||||
|
||||
$configDefault = (bool) ($config['enabled'] ?? true);
|
||||
$hasOverride = array_key_exists($key, $overrides);
|
||||
$override = $hasOverride ? (bool) $overrides[$key] : null;
|
||||
|
||||
$result[$key] = [
|
||||
'enabled' => $hasOverride ? $override : $configDefault,
|
||||
'config_default' => $configDefault,
|
||||
'override' => $override,
|
||||
'label' => $config['label'] ?? ucfirst($key),
|
||||
'description' => $config['description'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get only the effective enabled/disabled states.
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
public static function getModuleStates(): array
|
||||
{
|
||||
$states = [];
|
||||
foreach (static::getAllModules() as $key => $info) {
|
||||
$states[$key] = (bool) $info['enabled'];
|
||||
}
|
||||
return $states;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the override map from SystemSetting (cached).
|
||||
*
|
||||
* @return array<string, bool>
|
||||
*/
|
||||
protected static function getOverrides(): array
|
||||
{
|
||||
try {
|
||||
$value = SystemSetting::getValue(static::OVERRIDE_KEY);
|
||||
} catch (Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (is_string($value) && $value !== '') {
|
||||
$decoded = json_decode($value, true);
|
||||
$value = is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
|
||||
if (!is_array($value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$out = [];
|
||||
foreach ($value as $k => $v) {
|
||||
$out[(string) $k] = filter_var($v, FILTER_VALIDATE_BOOLEAN);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
145
app/Support/PaletteExtractor.php
Normal file
145
app/Support/PaletteExtractor.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class PaletteExtractor
|
||||
{
|
||||
/**
|
||||
* Extract a usable brand palette from an image file.
|
||||
* Returns ['primary' => '#rrggbb', 'accent' => '#rrggbb', 'tint' => '#rrggbb']
|
||||
* or null if extraction fails (e.g. GD missing, unreadable file).
|
||||
*/
|
||||
public static function extract(string $path): ?array
|
||||
{
|
||||
if (!extension_loaded('gd') || !is_readable($path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$info = @getimagesize($path);
|
||||
if (!$info) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$img = match ($info[2]) {
|
||||
IMAGETYPE_JPEG => @imagecreatefromjpeg($path),
|
||||
IMAGETYPE_PNG => @imagecreatefrompng($path),
|
||||
IMAGETYPE_GIF => @imagecreatefromgif($path),
|
||||
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($path) : null,
|
||||
default => null,
|
||||
};
|
||||
if (!$img) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$w = imagesx($img);
|
||||
$h = imagesy($img);
|
||||
$stepX = max(1, (int) floor($w / 60));
|
||||
$stepY = max(1, (int) floor($h / 60));
|
||||
|
||||
$buckets = [];
|
||||
for ($y = 0; $y < $h; $y += $stepY) {
|
||||
for ($x = 0; $x < $w; $x += $stepX) {
|
||||
$rgba = imagecolorat($img, $x, $y);
|
||||
$a = ($rgba >> 24) & 0x7F;
|
||||
if ($a > 90) {
|
||||
continue; // mostly transparent
|
||||
}
|
||||
$r = ($rgba >> 16) & 0xFF;
|
||||
$g = ($rgba >> 8) & 0xFF;
|
||||
$b = $rgba & 0xFF;
|
||||
|
||||
$sum = $r + $g + $b;
|
||||
if ($sum < 60 || $sum > 720) {
|
||||
continue; // too dark or too light
|
||||
}
|
||||
|
||||
$max = max($r, $g, $b);
|
||||
$min = min($r, $g, $b);
|
||||
if (($max - $min) < 25) {
|
||||
continue; // too gray
|
||||
}
|
||||
|
||||
// quantize to 32-step buckets
|
||||
$key = (($r >> 5) << 10) | (($g >> 5) << 5) | ($b >> 5);
|
||||
if (!isset($buckets[$key])) {
|
||||
$buckets[$key] = ['r' => 0, 'g' => 0, 'b' => 0, 'n' => 0];
|
||||
}
|
||||
$buckets[$key]['r'] += $r;
|
||||
$buckets[$key]['g'] += $g;
|
||||
$buckets[$key]['b'] += $b;
|
||||
$buckets[$key]['n']++;
|
||||
}
|
||||
}
|
||||
imagedestroy($img);
|
||||
|
||||
if (empty($buckets)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
uasort($buckets, fn ($a, $b) => $b['n'] <=> $a['n']);
|
||||
|
||||
$candidates = [];
|
||||
foreach ($buckets as $bucket) {
|
||||
$candidates[] = [
|
||||
'r' => (int) round($bucket['r'] / $bucket['n']),
|
||||
'g' => (int) round($bucket['g'] / $bucket['n']),
|
||||
'b' => (int) round($bucket['b'] / $bucket['n']),
|
||||
'n' => $bucket['n'],
|
||||
];
|
||||
if (count($candidates) >= 8) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$primary = $candidates[0];
|
||||
$accent = null;
|
||||
foreach (array_slice($candidates, 1) as $c) {
|
||||
$dr = $c['r'] - $primary['r'];
|
||||
$dg = $c['g'] - $primary['g'];
|
||||
$db = $c['b'] - $primary['b'];
|
||||
if (sqrt($dr * $dr + $dg * $dg + $db * $db) > 60) {
|
||||
$accent = $c;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$accent) {
|
||||
$accent = self::shift($primary, 30);
|
||||
}
|
||||
|
||||
$tint = self::mixWithWhite($primary, 0.92);
|
||||
|
||||
return [
|
||||
'primary' => self::toHex($primary),
|
||||
'accent' => self::toHex($accent),
|
||||
'tint' => self::toHex($tint),
|
||||
];
|
||||
}
|
||||
|
||||
private static function toHex(array $c): string
|
||||
{
|
||||
return sprintf('#%02x%02x%02x',
|
||||
max(0, min(255, $c['r'])),
|
||||
max(0, min(255, $c['g'])),
|
||||
max(0, min(255, $c['b']))
|
||||
);
|
||||
}
|
||||
|
||||
private static function shift(array $c, int $delta): array
|
||||
{
|
||||
return [
|
||||
'r' => ($c['r'] + $delta) % 256,
|
||||
'g' => ($c['g'] + $delta * 2) % 256,
|
||||
'b' => ($c['b'] + $delta * 3) % 256,
|
||||
];
|
||||
}
|
||||
|
||||
private static function mixWithWhite(array $c, float $whiteRatio): array
|
||||
{
|
||||
$w = max(0.0, min(1.0, $whiteRatio));
|
||||
return [
|
||||
'r' => (int) round($c['r'] * (1 - $w) + 255 * $w),
|
||||
'g' => (int) round($c['g'] * (1 - $w) + 255 * $w),
|
||||
'b' => (int) round($c['b'] * (1 - $w) + 255 * $w),
|
||||
];
|
||||
}
|
||||
}
|
||||
137
app/Support/RouteArgumentParser.php
Normal file
137
app/Support/RouteArgumentParser.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* RouteArgumentParser handles parsing URL arguments in format: "page-name--h:HASHKEY" or "page-name--e:ENCODED_PAYLOAD"
|
||||
*/
|
||||
class RouteArgumentParser
|
||||
{
|
||||
/**
|
||||
* Parse a route argument string into its components
|
||||
*
|
||||
* @param string $argument The route argument string (e.g., "edituser--h:USER_HASH123")
|
||||
* @return array {slug: string, type?: 'hash'|'payload', value?: string} Array with parsed components
|
||||
*/
|
||||
public function parseArgument($argument)
|
||||
{
|
||||
if (!is_string($argument) || empty($argument)) {
|
||||
return [
|
||||
'slug' => $argument,
|
||||
'type' => null,
|
||||
'value' => null
|
||||
];
|
||||
}
|
||||
|
||||
// Check for hash format: --h:HASHKEY
|
||||
if (preg_match('/^(.*?)--h:(.*)$/', $argument, $matches)) {
|
||||
return [
|
||||
'slug' => $matches[1],
|
||||
'type' => 'hash',
|
||||
'value' => $this->decodeHashValue($matches[2])
|
||||
];
|
||||
}
|
||||
|
||||
// Check for payload format: --e:ENCODED_PAYLOAD
|
||||
if (preg_match('/^(.*?)--e:(.*)$/', $argument, $matches)) {
|
||||
return [
|
||||
'slug' => $matches[1],
|
||||
'type' => 'payload',
|
||||
'value' => $this->decodePayloadValue($matches[2])
|
||||
];
|
||||
}
|
||||
|
||||
// Backward compatibility: Check for hash format without colon: --hHASHKEY
|
||||
if (preg_match('/^(.*?)--h([^:].*)$/', $argument, $matches)) {
|
||||
return [
|
||||
'slug' => $matches[1],
|
||||
'type' => 'hash',
|
||||
'value' => $this->decodeHashValue($matches[2])
|
||||
];
|
||||
}
|
||||
|
||||
// No hash/payload found - just return the slug
|
||||
return [
|
||||
'slug' => $argument,
|
||||
'type' => null,
|
||||
'value' => null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a hash value from URL format (base64 encoded)
|
||||
*/
|
||||
private function decodeHashValue($encodedValue)
|
||||
{
|
||||
if (!is_string($encodedValue) || empty($encodedValue)) {
|
||||
return $encodedValue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove base64 encoding and decode
|
||||
$decoded = base64_decode($encodedValue, true);
|
||||
|
||||
// If it's valid UTF-8 string representation of the hashkey
|
||||
if (is_string($decoded) && mb_check_encoding($decoded, 'utf-8')) {
|
||||
// Frontend encodes as btoa(encodeURIComponent(hashkey)),
|
||||
// so we need to urldecode after base64_decode
|
||||
return urldecode($decoded);
|
||||
}
|
||||
|
||||
return $encodedValue; // Return original if decoding fails
|
||||
} catch (\Exception $e) {
|
||||
error_log('[RouteArgumentParser] Error decoding hash: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a payload value from URL format (base64 encoded JSON)
|
||||
*/
|
||||
private function decodePayloadValue($encodedValue)
|
||||
{
|
||||
if (!is_string($encodedValue) || empty($encodedValue)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove base64 encoding and decode
|
||||
$decoded = base64_decode($encodedValue, true);
|
||||
|
||||
// Decode URI components if it was encoded on frontend
|
||||
if ($decoded) {
|
||||
$decoded = urldecode($decoded);
|
||||
}
|
||||
|
||||
// Parse JSON
|
||||
$jsonData = json_decode($decoded, true);
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return $jsonData;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (\Exception $e) {
|
||||
error_log('[RouteArgumentParser] Error decoding payload: ' . $e->getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if argument contains hash format
|
||||
*/
|
||||
public function isHashFormat($argument)
|
||||
{
|
||||
return (bool) preg_match('/^(.*?)--h:?(.*)$/', $argument, $matches);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if argument contains payload format
|
||||
*/
|
||||
public function isPayloadFormat($argument)
|
||||
{
|
||||
return (bool) preg_match('/^(.*?)--e:(.*)$/', $argument, $matches);
|
||||
}
|
||||
}
|
||||
119
app/Support/SystemSettingsHelper.php
Normal file
119
app/Support/SystemSettingsHelper.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\SystemSetting;
|
||||
use App\Http\Controllers\FilesMainController;
|
||||
use App\Support\AccountingTheme;
|
||||
|
||||
class SystemSettingsHelper
|
||||
{
|
||||
/**
|
||||
* Get application name.
|
||||
*/
|
||||
public static function appName(): string
|
||||
{
|
||||
return SystemSetting::getValue('app_name', 'BukidBounty');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application description.
|
||||
*/
|
||||
public static function appDescription(): string
|
||||
{
|
||||
return SystemSetting::getValue('app_description', 'Agricultural Management Platform & Marketplace');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get application tagline.
|
||||
*/
|
||||
public static function appTagline(): string
|
||||
{
|
||||
return SystemSetting::getValue('app_tagline', 'Bounty of the Fields at Your Fingertips');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get logo URL.
|
||||
*/
|
||||
public static function logoUrl(): string
|
||||
{
|
||||
$hashkey = SystemSetting::getValue('app_logo');
|
||||
if ($hashkey) {
|
||||
return FilesMainController::generateURLforFileListHash($hashkey);
|
||||
}
|
||||
return cdn_asset('vendor/assets/icons/192x192.png');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get primary theme color.
|
||||
*/
|
||||
public static function primaryColor(): string
|
||||
{
|
||||
return SystemSetting::getValue('primary_color', '#0d6efd');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get footer text.
|
||||
*/
|
||||
public static function footerText(): string
|
||||
{
|
||||
return SystemSetting::getValue('footer_text', '© 2026 BukidBounty Ecosystem. All rights reserved.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if maintenance mode is active.
|
||||
*/
|
||||
public static function isMaintenanceMode(): bool
|
||||
{
|
||||
return SystemSetting::getValue('maintenance_mode', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the application mode (corporate, cooperative, ngo, others).
|
||||
*/
|
||||
public static function appMode(): string
|
||||
{
|
||||
return SystemSetting::getValue('app_mode', 'corporate');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the hashkey of the main cooperative/organization (if any).
|
||||
*/
|
||||
public static function mainOrganizationHashkey(): ?string
|
||||
{
|
||||
$value = SystemSetting::getValue('main_organization');
|
||||
return $value ?: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the main cooperative/organization model (if any).
|
||||
*/
|
||||
public static function mainOrganization(): ?\App\Models\Market\Organization
|
||||
{
|
||||
$hashkey = static::mainOrganizationHashkey();
|
||||
if (!$hashkey) {
|
||||
return null;
|
||||
}
|
||||
return \App\Models\Market\Organization::where('hashkey', $hashkey)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the active accounting theme key.
|
||||
*/
|
||||
public static function accountingTheme(): string
|
||||
{
|
||||
return SystemSetting::getValue('accounting_theme', AccountingTheme::DEFAULT_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available chapter/org position titles.
|
||||
*/
|
||||
public static function chapterPositions(): array
|
||||
{
|
||||
$value = SystemSetting::getValue('chapter_positions');
|
||||
if (!$value) {
|
||||
return ['National Director', 'Regional Director', 'Provincial Coordinator', 'City/Municipal Officer', 'Barangay Captain', 'Secretary', 'Treasurer', 'Auditor', 'Member'];
|
||||
}
|
||||
return is_string($value) ? (json_decode($value, true) ?? []) : (array) $value;
|
||||
}
|
||||
}
|
||||
100
app/Support/TokenAbilities.php
Normal file
100
app/Support/TokenAbilities.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
class TokenAbilities
|
||||
{
|
||||
public const WILDCARD = '*';
|
||||
|
||||
/**
|
||||
* Catalog of all valid abilities grouped for UI presentation.
|
||||
* @return array<string, array<int, array{key: string, label: string}>>
|
||||
*/
|
||||
public static function catalog(): array
|
||||
{
|
||||
return [
|
||||
'Identity' => self::group([
|
||||
'users:read', 'users:write', 'users:delete', 'users:impersonate',
|
||||
'chapters:read', 'chapters:write', 'chapters:delete',
|
||||
'chapter-members:read', 'chapter-members:write', 'chapter-members:delete',
|
||||
'announcements:read', 'announcements:write', 'announcements:delete',
|
||||
]),
|
||||
'Content' => self::group([
|
||||
'pages:read', 'pages:write', 'pages:delete',
|
||||
'landing-pages:read', 'landing-pages:write', 'landing-pages:delete',
|
||||
'page-memory:read', 'page-memory:write',
|
||||
]),
|
||||
'Files' => self::group([
|
||||
'files:read', 'files:upload', 'files:delete', 'files:publish',
|
||||
]),
|
||||
'System' => self::group([
|
||||
'system-settings:read', 'system-settings:write',
|
||||
'db-backups:read', 'db-backups:write', 'db-backups:restore',
|
||||
'table-logs:read',
|
||||
'global-transactions:read', 'global-transactions:write',
|
||||
]),
|
||||
'Accounting' => self::group([
|
||||
'accounts:read', 'accounts:write', 'accounts:delete',
|
||||
'account-transactions:read', 'account-transactions:write', 'account-transactions:void',
|
||||
'member-ledgers:read', 'member-ledgers:write',
|
||||
]),
|
||||
'Cooperatives' => self::group([
|
||||
'cooperatives:read', 'cooperatives:write', 'cooperatives:delete',
|
||||
'cooperative-members:read', 'cooperative-members:write', 'cooperative-members:delete',
|
||||
'cooperative-documents:read', 'cooperative-documents:write', 'cooperative-documents:delete',
|
||||
'cooperative-resolutions:read', 'cooperative-resolutions:write', 'cooperative-resolutions:delete',
|
||||
'cooperative-votes:read', 'cooperative-votes:write',
|
||||
'main-organization:read', 'main-organization:write',
|
||||
]),
|
||||
'Market' => self::group([
|
||||
'products:read', 'products:write', 'products:delete',
|
||||
'product-transactions:read', 'product-transactions:write', 'product-transactions:void',
|
||||
'stores:read', 'stores:write', 'stores:delete',
|
||||
'store-managers:read', 'store-managers:write',
|
||||
'customers:read', 'customers:write', 'customers:delete',
|
||||
'farmers:read', 'farmers:write', 'farmers:delete',
|
||||
'couriers:read', 'couriers:write',
|
||||
'shipments:read', 'shipments:write',
|
||||
'carts:read', 'carts:write',
|
||||
]),
|
||||
'POS' => self::group([
|
||||
'pos-sessions:read', 'pos-sessions:write', 'pos-sessions:close',
|
||||
'pos-transactions:read', 'pos-transactions:write', 'pos-transactions:void',
|
||||
'pos-access-keys:read', 'pos-access-keys:write', 'pos-access-keys:delete',
|
||||
]),
|
||||
'Property' => self::group([
|
||||
'properties:read', 'properties:write', 'properties:delete',
|
||||
'referrals:read', 'referrals:write',
|
||||
'referral-keys:read', 'referral-keys:write', 'referral-keys:delete',
|
||||
]),
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int, string> */
|
||||
public static function all(): array
|
||||
{
|
||||
$out = [];
|
||||
foreach (self::catalog() as $group) {
|
||||
foreach ($group as $entry) {
|
||||
$out[] = $entry['key'];
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
public static function exists(string $ability): bool
|
||||
{
|
||||
return $ability === self::WILDCARD || in_array($ability, self::all(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, string> $keys
|
||||
* @return array<int, array{key: string, label: string}>
|
||||
*/
|
||||
private static function group(array $keys): array
|
||||
{
|
||||
return array_map(static fn ($k) => ['key' => $k, 'label' => $k], $keys);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user