commit 94ecbe588784b258b40f6fefc6c4c2ce271386ee Author: ISMAIL MASSERAN Date: Mon Jun 8 11:37:14 2026 +0800 first init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6e3cfa5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Ignore everything +/* + +# But keep these folders +!/be/ +!/fe/ +!/SUTERA-server-credential.txt/ +!/TODO.md +!/README.md +!/redis-config +!/.gitea +!/my-kopkb.code-workspace +!/docs + +# Also keep .gitignore itself +!.gitignore diff --git a/README.md b/README.md new file mode 100644 index 0000000..5cecd01 --- /dev/null +++ b/README.md @@ -0,0 +1,142 @@ +# SUTERA 3.0 – Guide + +Welcome to **SUTERA 3.0**, the streamlined training deployment setup using **Docker**, **Laravel**, and **Vite**. + +--- + +## 🔐 Login Credentials + +> Use the following to log into the system: + +- **Email**: `isml.msrn11@gmail.com` +- **Password**: `TOPAZTHEGOAT` + +--- + +## Formula Used: +1. KEUPAYAAN (%) = (PEGANGAN/PERJAWATAN) * 100 +2. SIAPSIAGA or KESIAGAAN (%) = (BDG/perjawatan) * 100 +3. SERVISIBILITI (%) = (BDG/pegangan) * 100 + +## Laravel command to clear +```bash +sail artisan config:clear +sail artisan cache:clear +sail artisan permission:cache-reset +``` + +--- + +# 🎯 Addtional Notes to consider +- Make sure `/var/www/public/build` exists inside your PHP-FPM container (handled by Dockerfile). +- Assets are built via Vite in the **Node.js stage** and injected into the Laravel `public/` directory. +- Use `entrypoint.sh` for Laravel setup like `php artisan migrate` or `storage:link` if needed. +- 'DEVELOPER' roles can bypassed all permissions by default and cannot be deleted even through database +- Use delete in controller not directly in routes because no authorization and validation +- Use Gate for non-model-specific permissions. general-purpose or simple logic, +- Use policy when need structured authorization for CRUD operations. anything tied to a model/resource +- Servisibility always higher than siapsiaga +- Solution — put specific routes first before resource routes +- to use soft delete, make id and deleted_at as unique +- to make new module, run: +```bash +sail artisan module:make Example +``` +- There are two User model files + +## PKJ Default Varian (DO NOT DELETE) or update. If require update, be sure to update the export name for each PKJ report: +- BANTUAN MOBILITI +- BEKALAN AIR +- EOD/IED/PEMUSNAHAN +- JURUUKUR +- KENDERAAN JENIS A +- KENDERAAN JENIS B +- OPTRONIK DAN PERALATAN LATIHAN +- PELURU DAN BAHAN LETUPAN +- PEMUSNAHAN +- PERALATAN KETUKANGAN +- PERALATAN KOMUNIKASI PERTAHANAN +- PERALATAN PD&P +- PERALATAN PERAIRAN +- PERALATAN PNBK +- SENJATA PERTAHANAN + + +```sql +--- KJC TO UPDATE PEGANGAN IN PERJAWATAN TABLE; CHECK WILL BE UPDATED FIRST +SELECT + ae.id, + ae.entitlement, + ae.holding AS current_holding, + ( + SELECT COUNT(*) + FROM kjc_asset_holdings ah + WHERE ah.kjc_category_id = ae.kjc_category_id + AND ah.kjc_category_id = ae.kjc_category_id + AND ah.unit_id = ae.unit_id + AND ah.deleted_at IS NULL + ) AS actual_holding_count +FROM kjc_asset_entitlements ae +WHERE ae.deleted_at IS NULL; + +--- EXECUTE IT +UPDATE kjc_asset_entitlements ae +SET ae.holding = ( + SELECT COUNT(*) + FROM kjc_asset_holdings ah + WHERE ah.kjc_category_id = ae.kjc_category_id + AND ah.kjc_category_id = ae.kjc_category_id + AND ah.unit_id = ae.unit_id + AND ah.deleted_at IS NULL +) +WHERE ae.deleted_at IS NULL; +``` + +## Run specific laravel migration: +- If it is fresh, then run: +```bash +sail artisan migrate --path=database/migrations/2025_07_30_044653_create_historical_pkj_perjawatan_table.php +``` + +- If not, rollback first: +```bash +sail artisan migrate:rollback --step=1 + +sail artisan migrate --path=database/migrations/2025_07_30_044708_create_historical_pkj_pegangan_status_table.php +``` + +## To remove data in postgres and restart numbering +```bash +TRUNCATE TABLE kjc_spare_parts RESTART IDENTITY; +``` + +## Reset query numbering +```sql +SELECT pg_get_serial_sequence('"permissions"', 'id') AS seq_name; +SELECT last_value FROM public.permissions_id_seq; +SELECT setval('public.permissions_id_seq', COALESCE((SELECT MAX(id) FROM permissions), 0) + 1, false); +``` + +## Run capture data command +```bash +# kjc +sail artisan kjc:historical:trigger + +# pkj +sail artisan pkj:historical:trigger +``` + +## Auto-generate weekly KJC report (Jabatan Arah RAJD) - Scheduled to run every Monday at 3:00 AM. +```bash +# Manual trigger +sail artisan kjc:auto-generate-weekly-report "Jabatan Arah RAJD" + +# Force regenerate even if reports exist +sail artisan kjc:auto-generate-weekly-report "Jabatan Arah RAJD" --force +``` + +# Git +## DO NOT RUN THIS BECAUSE IT WILL REMOVE ALL THE UNTAGGED FILES IN LOCAL +```bash +git reset --hard development # moves the current branch (e.g., main) to point exactly where development +``` \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..9069e57 --- /dev/null +++ b/TODO.md @@ -0,0 +1,15 @@ +## Fix + +## Bug + +## Improvement +[ ] mesyuarat anggota tertinggi +[ ] penyata anggota +[ ] daftar anggota +[ ] baki pinjaman - transaction history: tarik dari ubs +[ ] pembiayaan anggota +[ ] sumbangan +[ ] wasi/penama +[ ] pendaftaran anggota/meneruskan anggota/pencen +[ ] daftar lembaga (backdated) +[ ] boleh print semua borang \ No newline at end of file diff --git a/be/.dockerignore b/be/.dockerignore new file mode 100644 index 0000000..bd7e2f9 --- /dev/null +++ b/be/.dockerignore @@ -0,0 +1,25 @@ +/public/build +/public/hot +/public/storage +/docker_images +/docker-images +/.vscode +.env +NOTE.md +/node_modules +/vendor +.git +.gitignore +/bootstrap/cache +/supporting_documents +/docs +TODO.md +.DS_Store +.idea +deploy-production.sh +deploy.sh +deploy-staging.sh +deploy-training.sh +build.sh +TODO.md +README.md \ No newline at end of file diff --git a/be/.editorconfig b/be/.editorconfig new file mode 100644 index 0000000..8f0de65 --- /dev/null +++ b/be/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[docker-compose.yml] +indent_size = 4 diff --git a/be/.env.development b/be/.env.development new file mode 100644 index 0000000..b5c5094 --- /dev/null +++ b/be/.env.development @@ -0,0 +1,98 @@ +APP_NAME="SUTERA 3.0 Dev" +APP_ENV=local +APP_KEY=base64:0dM5HYEFRu9P6ni51QTUZm5mQ23znc/s/aebLTRUGxU= +APP_DEBUG=true +APP_TIMEZONE=Asia/Kuala_Lumpur +APP_URL=http://localhost + +APP_LOCALE=en +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=en_US + +APP_MAINTENANCE_DRIVER=file +APP_MAINTENANCE_STORE=database + +PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=pgsql +DB_HOST=pgsql +DB_PORT=5432 +DB_DATABASE=sutera +DB_USERNAME=suterauser +DB_PASSWORD=password + +SESSION_DRIVER=redis +SESSION_LIFETIME=60 +SESSION_ENCRYPT=true +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=redis +CACHE_PREFIX=sutera_cache + +MEMCACHED_HOST=memcached + +REDIS_CLIENT=phpredis +REDIS_HOST=redis +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=smtp +MAIL_SCHEME=null +MAIL_HOST=mailpit +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" +# VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +# VITE_PUSHER_HOST="${PUSHER_HOST}" +# VITE_PUSHER_PORT="${PUSHER_PORT}" +# VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" +# VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +EXTERNAL_API_TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibWJzcC1yZXNvdXJjZUlkIl0sInRpbWVvdXRQZXJpb2QiOjkwMCwidXNlcl9uYW1lIjoic3BhLXVzZXIiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXhwIjoxNzI4NTgzNjEyLCJqdGkiOiJjMmU2OTkxOS0yZDU3LTQ5YmYtYTVmZS1lZjVmMzIyNDZkZDIiLCJjbGllbnRfaWQiOiJzcGEtY2xpZW50In0.LE2byaRzsYkchzI4ox9Gz2u6jNmTqZQa-_ttR2PY8e8" +EXTERNAL_API_BASE_URL=http://20.11.32.134/rest/td/gasset/asset + +SSO_SECRET="Y6g#Rb@Km=Nz]o^X+fEC~ 'ActivityLog', +]; diff --git a/be/Modules/ActivityLog/Console/.gitkeep b/be/Modules/ActivityLog/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Database/Factories/.gitkeep b/be/Modules/ActivityLog/Database/Factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Database/Migrations/.gitkeep b/be/Modules/ActivityLog/Database/Migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Database/Seeders/.gitkeep b/be/Modules/ActivityLog/Database/Seeders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Database/Seeders/ActivityLogDatabaseSeeder.php b/be/Modules/ActivityLog/Database/Seeders/ActivityLogDatabaseSeeder.php new file mode 100644 index 0000000..8442f29 --- /dev/null +++ b/be/Modules/ActivityLog/Database/Seeders/ActivityLogDatabaseSeeder.php @@ -0,0 +1,16 @@ +call([]); + } +} diff --git a/be/Modules/ActivityLog/Emails/.gitkeep b/be/Modules/ActivityLog/Emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Entities/.gitkeep b/be/Modules/ActivityLog/Entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Helpers/.gitkeep b/be/Modules/ActivityLog/Helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Http/Controllers/.gitkeep b/be/Modules/ActivityLog/Http/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Http/Controllers/ActivityLogController.php b/be/Modules/ActivityLog/Http/Controllers/ActivityLogController.php new file mode 100644 index 0000000..0a1e8d7 --- /dev/null +++ b/be/Modules/ActivityLog/Http/Controllers/ActivityLogController.php @@ -0,0 +1,97 @@ +get('per_page', 10); + $perPage = min($perPage, 1000); // Limit max per page to 100 + $search = trim((string) $request->get('search', '')); + + $query = Activity::latest()->with(['causer']); + + if (! empty($search)) { + $query->where(function ($q) use ($search) { + $q->where('description', 'ILIKE', "%{$search}%") + ->orWhere('subject_type', 'ILIKE', "%{$search}%") + ->orWhere('event', 'ILIKE', "%{$search}%") + ->orWhere('causer.name', 'ILIKE', "%{$search}%") + ->orWhere('causer.email', 'ILIKE', "%{$search}%"); + }); + } + + $activities = $query->paginate($perPage); + + return response()->json([ + 'success' => true, + 'data' => $activities->items(), + 'pagination' => [ + 'current_page' => $activities->currentPage(), + 'per_page' => $activities->perPage(), + 'total' => $activities->total(), + 'last_page' => $activities->lastPage(), + 'from' => $activities->firstItem(), + 'to' => $activities->lastItem(), + 'has_more_pages' => $activities->hasMorePages(), + 'links' => [ + 'first' => $activities->appends(request()->query())->url(1), + 'last' => $activities->appends(request()->query())->url($activities->lastPage()), + 'prev' => $activities->appends(request()->query())->previousPageUrl(), + 'next' => $activities->appends(request()->query())->nextPageUrl(), + ] + ], + 'message' => 'Activity logs retrieved successfully', + ]); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + return view('activitylog::create'); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) {} + + /** + * Show the specified resource. + */ + public function show($id) + { + return view('activitylog::show'); + } + + /** + * Show the form for editing the specified resource. + */ + public function edit($id) + { + return view('activitylog::edit'); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, $id) {} + + /** + * Remove the specified resource from storage. + */ + public function destroy($id) {} +} diff --git a/be/Modules/ActivityLog/Http/Requests/.gitkeep b/be/Modules/ActivityLog/Http/Requests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Jobs/.gitkeep b/be/Modules/ActivityLog/Jobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Notifications/.gitkeep b/be/Modules/ActivityLog/Notifications/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Policies/.gitkeep b/be/Modules/ActivityLog/Policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Policies/ActivityLogPolicy.php b/be/Modules/ActivityLog/Policies/ActivityLogPolicy.php new file mode 100644 index 0000000..9219913 --- /dev/null +++ b/be/Modules/ActivityLog/Policies/ActivityLogPolicy.php @@ -0,0 +1,27 @@ +hasPermissionTo('lihat log aktiviti'); + } + + /** + * Determine whether the user can view the model. + */ + public function view($user, ?Activity $activity = null): bool + { + return $user->hasPermissionTo('lihat log aktiviti'); + } +} diff --git a/be/Modules/ActivityLog/Providers/.gitkeep b/be/Modules/ActivityLog/Providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Providers/ActivityLogServiceProvider.php b/be/Modules/ActivityLog/Providers/ActivityLogServiceProvider.php new file mode 100644 index 0000000..d1fad58 --- /dev/null +++ b/be/Modules/ActivityLog/Providers/ActivityLogServiceProvider.php @@ -0,0 +1,154 @@ +registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations')); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, 'Lang')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, 'Resources/Views'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } +} diff --git a/be/Modules/ActivityLog/Providers/EventServiceProvider.php b/be/Modules/ActivityLog/Providers/EventServiceProvider.php new file mode 100644 index 0000000..ec60530 --- /dev/null +++ b/be/Modules/ActivityLog/Providers/EventServiceProvider.php @@ -0,0 +1,27 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/Modules/ActivityLog/Providers/RouteServiceProvider.php b/be/Modules/ActivityLog/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..8bda2e9 --- /dev/null +++ b/be/Modules/ActivityLog/Providers/RouteServiceProvider.php @@ -0,0 +1,49 @@ +mapApiRoutes(); + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + */ + protected function mapWebRoutes(): void + { + Route::middleware('web')->group(module_path($this->name, '/Routes/web.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + */ + protected function mapApiRoutes(): void + { + Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php')); + } +} diff --git a/be/Modules/ActivityLog/Repositories/.gitkeep b/be/Modules/ActivityLog/Repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Repositories/Contracts/.gitkeep b/be/Modules/ActivityLog/Repositories/Contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Routes/.gitkeep b/be/Modules/ActivityLog/Routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Routes/api.php b/be/Modules/ActivityLog/Routes/api.php new file mode 100644 index 0000000..77ae656 --- /dev/null +++ b/be/Modules/ActivityLog/Routes/api.php @@ -0,0 +1,8 @@ +prefix('v1')->group(function () { + Route::apiResource('activitylogs', ActivityLogController::class)->names('activitylog'); +}); diff --git a/be/Modules/ActivityLog/Services/.gitkeep b/be/Modules/ActivityLog/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Tests/Feature/.gitkeep b/be/Modules/ActivityLog/Tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Tests/Unit/.gitkeep b/be/Modules/ActivityLog/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/Transformers/.gitkeep b/be/Modules/ActivityLog/Transformers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/ActivityLog/composer.json b/be/Modules/ActivityLog/composer.json new file mode 100644 index 0000000..a48c0ee --- /dev/null +++ b/be/Modules/ActivityLog/composer.json @@ -0,0 +1,30 @@ +{ + "name": "nwidart/activitylog", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\ActivityLog\\": "App", + "Modules\\ActivityLog\\Database\\Factories\\": "database/factories/", + "Modules\\ActivityLog\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\ActivityLog\\Tests\\": "tests/" + } + } +} diff --git a/be/Modules/ActivityLog/module.json b/be/Modules/ActivityLog/module.json new file mode 100644 index 0000000..8896020 --- /dev/null +++ b/be/Modules/ActivityLog/module.json @@ -0,0 +1,11 @@ +{ + "name": "ActivityLog", + "alias": "activitylog", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\ActivityLog\\Providers\\ActivityLogServiceProvider" + ], + "files": [] +} diff --git a/be/Modules/ActivityLog/package.json b/be/Modules/ActivityLog/package.json new file mode 100644 index 0000000..d6fbfc8 --- /dev/null +++ b/be/Modules/ActivityLog/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.1.2", + "laravel-vite-plugin": "^0.7.5", + "sass": "^1.69.5", + "postcss": "^8.3.7", + "vite": "^4.0.0" + } +} diff --git a/be/Modules/Auth/Actions/Fortify/CreateNewUser.php b/be/Modules/Auth/Actions/Fortify/CreateNewUser.php new file mode 100644 index 0000000..932ce8e --- /dev/null +++ b/be/Modules/Auth/Actions/Fortify/CreateNewUser.php @@ -0,0 +1,91 @@ + $input + */ + public function create(array $input): User + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique(User::class), + ], + 'ic_number' => ['required', 'string', 'max:255'], + 'password' => ['required', 'string', 'min:8'], + ])->validate(); + + $user = User::create([ + 'name' => $input['name'], + 'uuid' => Str::uuid(), + 'email' => $input['email'], + 'password' => Hash::make($input['password']), + 'ic_number' => $input['ic_number'], + 'status' => 'pending', + ]); + + // Assign role using Spatie permissions + $role = Role::where('name', 'Anggota')->first(); + if ($role) { + $user->assignRole($role); + } + + app(EmailVerificationOtpService::class)->send($user); + + // Send notification to admins if user requires activation + if ($user->status === 'pending') { + $this->notifyAdminsForActivation($user); + } + + return $user; + } + + /** + * Notify admins about new user requiring activation + */ + private function notifyAdminsForActivation(User $newUser): void + { + try { + $adminRoles = ['PENTADBIR', 'PS 2 KJC', 'PS 2 ALAT']; + + // Get users with specific roles plus admins (PENTADBIR and DEVELOPER) + $adminUsers = $this->getUsersWithRolesAndAdmins($adminRoles); + + $sender = auth()->user() ?? $newUser; // Use current user as sender, or new user if no auth + + foreach ($adminUsers as $admin) { + try { + $admin->notify(new UserActivationNotification($newUser, $sender)); + } catch (Exception $e) { + Log::error('Failed to send user activation notification: '.$e->getMessage()); + } + } + } catch (Exception $e) { + Log::error('Failed to notify admins for user activation: '.$e->getMessage()); + } + } +} diff --git a/be/Modules/Auth/Actions/Fortify/LoginResponse.php b/be/Modules/Auth/Actions/Fortify/LoginResponse.php new file mode 100644 index 0000000..5226f5a --- /dev/null +++ b/be/Modules/Auth/Actions/Fortify/LoginResponse.php @@ -0,0 +1,42 @@ +user(); + + if (! $user->hasVerifiedEmail()) { + $this->otpService->send($user); + + Auth::guard(config('fortify.guard'))->logout(); + + return response()->json([ + 'success' => true, + 'message' => 'Sila semak e-mel anda untuk kod pengesahan 6 digit.', + 'data' => [ + 'email' => $user->email, + 'requires_email_verification' => true, + ], + ]); + } + + return $this->authSession->createAuthResponse( + $user, + 'Login successful' + ); + } +} diff --git a/be/Modules/Auth/Actions/Fortify/LogoutResponse.php b/be/Modules/Auth/Actions/Fortify/LogoutResponse.php new file mode 100644 index 0000000..037bec3 --- /dev/null +++ b/be/Modules/Auth/Actions/Fortify/LogoutResponse.php @@ -0,0 +1,39 @@ +cookie(AuthCookie::originalUserCookieName())) { + return response()->json([ + 'success' => false, + 'message' => 'Sila tamatkan penyamaran sebelum log keluar.', + ], 400); + } + + $user = $request->user(); + + if ($user) { + // Delete all tokens for this user to ensure clean logout + $user->tokens()->delete(); + } + + $response = response()->json([ + 'success' => true, + 'message' => 'Logout successful', + ], 200); + + return AuthCookie::clearAuthCookies($response); + } +} diff --git a/be/Modules/Auth/Actions/Fortify/PasswordValidationRules.php b/be/Modules/Auth/Actions/Fortify/PasswordValidationRules.php new file mode 100644 index 0000000..5285251 --- /dev/null +++ b/be/Modules/Auth/Actions/Fortify/PasswordValidationRules.php @@ -0,0 +1,18 @@ +|string> + */ + protected function passwordRules(): array + { + return ['required', 'string', Password::default(), 'confirmed']; + } +} diff --git a/be/Modules/Auth/Actions/Fortify/RegisterResponse.php b/be/Modules/Auth/Actions/Fortify/RegisterResponse.php new file mode 100644 index 0000000..c3d2487 --- /dev/null +++ b/be/Modules/Auth/Actions/Fortify/RegisterResponse.php @@ -0,0 +1,26 @@ +user()?->email; + + Auth::guard(config('fortify.guard'))->logout(); + + return response()->json([ + 'success' => true, + 'message' => 'Pendaftaran berjaya. Sila semak e-mel anda untuk kod pengesahan 6 digit.', + 'data' => [ + 'email' => $email, + 'requires_email_verification' => true, + ], + ], 201); + } +} diff --git a/be/Modules/Auth/Actions/Fortify/ResetUserPassword.php b/be/Modules/Auth/Actions/Fortify/ResetUserPassword.php new file mode 100644 index 0000000..7b5f22f --- /dev/null +++ b/be/Modules/Auth/Actions/Fortify/ResetUserPassword.php @@ -0,0 +1,29 @@ + $input + */ + public function reset(User $user, array $input): void + { + Validator::make($input, [ + 'password' => $this->passwordRules(), + ])->validate(); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/be/Modules/Auth/Actions/Fortify/UpdateUserPassword.php b/be/Modules/Auth/Actions/Fortify/UpdateUserPassword.php new file mode 100644 index 0000000..0ba6688 --- /dev/null +++ b/be/Modules/Auth/Actions/Fortify/UpdateUserPassword.php @@ -0,0 +1,32 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + // 'current_password' => ['required', 'string', 'current_password:web'], + 'password' => $this->passwordRules(), + ], [ + // 'current_password.current_password' => __('The provided password does not match your current password.'), + ])->validateWithBag('updatePassword'); + + $user->forceFill([ + 'password' => Hash::make($input['password']), + ])->save(); + } +} diff --git a/be/Modules/Auth/Actions/Fortify/UpdateUserProfileInformation.php b/be/Modules/Auth/Actions/Fortify/UpdateUserProfileInformation.php new file mode 100644 index 0000000..2d5caa0 --- /dev/null +++ b/be/Modules/Auth/Actions/Fortify/UpdateUserProfileInformation.php @@ -0,0 +1,58 @@ + $input + */ + public function update(User $user, array $input): void + { + Validator::make($input, [ + 'name' => ['required', 'string', 'max:255'], + + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users')->ignore($user->id), + ], + ])->validateWithBag('updateProfileInformation'); + + if ($input['email'] !== $user->email && + $user instanceof MustVerifyEmail) { + $this->updateVerifiedUser($user, $input); + } else { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + ])->save(); + } + } + + /** + * Update the given verified user's profile information. + * + * @param array $input + */ + protected function updateVerifiedUser(User $user, array $input): void + { + $user->forceFill([ + 'name' => $input['name'], + 'email' => $input['email'], + 'email_verified_at' => null, + ])->save(); + + $user->sendEmailVerificationNotification(); + } +} diff --git a/be/Modules/Auth/Config/.gitkeep b/be/Modules/Auth/Config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Config/config.php b/be/Modules/Auth/Config/config.php new file mode 100644 index 0000000..cba1be8 --- /dev/null +++ b/be/Modules/Auth/Config/config.php @@ -0,0 +1,5 @@ + 'Auth', +]; diff --git a/be/Modules/Auth/Console/.gitkeep b/be/Modules/Auth/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Database/Factories/.gitkeep b/be/Modules/Auth/Database/Factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Database/Migrations/.gitkeep b/be/Modules/Auth/Database/Migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Database/Seeders/.gitkeep b/be/Modules/Auth/Database/Seeders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Database/Seeders/AuthDatabaseSeeder.php b/be/Modules/Auth/Database/Seeders/AuthDatabaseSeeder.php new file mode 100644 index 0000000..7a22932 --- /dev/null +++ b/be/Modules/Auth/Database/Seeders/AuthDatabaseSeeder.php @@ -0,0 +1,16 @@ +call([]); + } +} diff --git a/be/Modules/Auth/Emails/.gitkeep b/be/Modules/Auth/Emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php b/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php new file mode 100644 index 0000000..3dcb338 --- /dev/null +++ b/be/Modules/Auth/Emails/EmailVerificationOtpEmail.php @@ -0,0 +1,35 @@ +subject('Pengesahan E-mel - Kod OTP') + ->markdown('auth::emails.verification-otp', [ + 'name' => $notifiable->name, + 'otp' => $this->otp, + 'minutes' => $minutes, + 'logoPath' => public_path('images/logo-kopkb.svg'), + ]); + } +} diff --git a/be/Modules/Auth/Entities/EmailVerificationOtp.php b/be/Modules/Auth/Entities/EmailVerificationOtp.php new file mode 100644 index 0000000..62d4780 --- /dev/null +++ b/be/Modules/Auth/Entities/EmailVerificationOtp.php @@ -0,0 +1,44 @@ + 'datetime', + 'attempts' => 'integer', + ]; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function isExpired(): bool + { + return $this->expires_at->isPast(); + } + + public function hasExceededMaxAttempts(): bool + { + return $this->attempts >= (int) config('auth.email_verification.max_attempts', 5); + } +} diff --git a/be/Modules/Auth/Entities/User.php b/be/Modules/Auth/Entities/User.php new file mode 100644 index 0000000..28ff4c1 --- /dev/null +++ b/be/Modules/Auth/Entities/User.php @@ -0,0 +1,252 @@ + */ + use HasApiTokens, HasFactory, HasPermissions, HasRoles, HasUuids, Impersonate, LogsActivity, Notifiable, SoftDeletes, HasVisibility; + + protected $table = 'users'; + + /** + * The attributes that are mass assignable. + * + * @var list + */ + protected $fillable = [ + 'name', + 'email', + 'password', + 'ic_number', + 'position', + 'phone_number', + 'image_url', + 'status', + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]; + + /** + * The attributes that should be hidden for serialization. + * + * @var list + */ + protected $hidden = [ + 'password', + 'remember_token', + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]; + + /** + * Get the attributes that should be cast. + * + * @return array + */ + protected function casts(): array + { + return [ + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'status' => 'string', + ]; + } + + + /** + * Role used for permission checks in the current session (Sanctum token). + */ + public function activeRole(): ?Role + { + return ActiveRoleService::getActiveRole($this); + } + + public function hasPermissionAction($action): bool + { + $role = $this->activeRole(); + + return $role && $role->permission_actions && + in_array($action, $role->permission_actions, true); + } + + public function getActivityLogOptions(): LogOptions + { + return LogOptions::defaults() + ->logAll() + ->logOnlyDirty() + ->dontSubmitEmptyLogs() + ->setDescriptionForEvent(fn (string $eventName) => "User {$this->name} was {$eventName}"); + } + + /** + * Override hasPermissionTo to bypass all permission checks for DEVELOPER role + */ + public function hasPermissionTo($permission, $guardName = null): bool + { + $activeRole = $this->activeRole(); + + if ($activeRole?->name === 'DEVELOPER') { + return true; + } + + static $isCheckingPermission = false; + + if ($isCheckingPermission) { + return false; + } + + $isCheckingPermission = true; + + try { + $permissionClass = app(PermissionRegistrar::class)->getPermissionClass(); + + if (is_string($permission)) { + $permission = $permissionClass::findByName($permission, $guardName ?? 'api'); + } + + if (is_int($permission)) { + $permission = $permissionClass::findById($permission, $guardName ?? 'api'); + } + + if (! $permission instanceof Permission) { + return false; + } + + if ($this->permissions->contains('id', $permission->id)) { + return true; + } + + if (! $activeRole) { + return false; + } + + $activeRole->loadMissing('permissions'); + + return $activeRole->permissions->contains('id', $permission->id); + } catch (\Exception $e) { + return false; + } finally { + $isCheckingPermission = false; + } + } + + /** + * Scope to exclude current user unless they have developer role + */ + public function scopeExcludeCurrentUserUnlessDeveloper($query, $currentUser = null) + { + $currentUser = $currentUser ?? auth()->user(); + + if ($currentUser && ! $currentUser->hasRole('DEVELOPER')) { + return $query->where('id', '!=', $currentUser->id); + } + + return $query; + } + + /** + * Scope to exclude users with DEVELOPER role unless current user is DEVELOPER + */ + public function scopeExcludeDevelopersUnlessDeveloper($query, $currentUser = null) + { + $currentUser = $currentUser ?? auth()->user(); + + if ($currentUser && ! $currentUser->hasRole('DEVELOPER')) { + return $query->whereDoesntHave('roles', function ($q) { + $q->where('name', 'DEVELOPER'); + }); + } + + return $query; + } + + /** + * Check if the user can impersonate another user + */ + public function canImpersonate(): bool + { + // Check permission instead of hardcoded roles + return $this->hasPermissionTo('menyamar pengguna'); + } + + /** + * Check if the user can be impersonated + */ + public function canBeImpersonated(): bool + { + // DEVELOPER cannot be impersonated by anyone + if ($this->hasRole('DEVELOPER')) { + return false; + } + + // Check if the current user has permission to impersonate + $currentUser = auth()->user(); + if (!$currentUser) { + return false; + } + + // Users with permission can impersonate other users (except DEVELOPER) + return $currentUser->hasPermissionTo('menyamar pengguna'); + } + + public function hasVerifiedEmail(): bool + { + return $this->email_verified_at !== null; + } + + /** + * Check if user can login based on their status + */ + public function canLogin(): bool + { + return $this->status === 'active'; + } + + /** + * Whether credentials are valid for issuing a session (includes verified pending users). + */ + public function canAuthenticate(): bool + { + if ($this->canLogin()) { + return true; + } + + return $this->hasVerifiedEmail() && $this->status === 'pending'; + } + + /** + * Get the login restriction message based on user status + */ + public function getLoginRestrictionMessage(): ?string + { + switch ($this->status) { + case 'pending': + return 'Akaun anda sedang menunggu pengaktifan dari pentadbir sistem. Sila hubungi pentadbir sistem.'; + case 'inactive': + return 'Akaun anda tidak aktif. Sila hubungi pentadbir sistem.'; + default: + return 'Akaun anda tidak dapat mengakses sistem. Sila hubungi pentadbir sistem.'; + } + } +} diff --git a/be/Modules/Auth/Helpers/.gitkeep b/be/Modules/Auth/Helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Http/Controllers/EmailVerificationController.php b/be/Modules/Auth/Http/Controllers/EmailVerificationController.php new file mode 100644 index 0000000..ead22a3 --- /dev/null +++ b/be/Modules/Auth/Http/Controllers/EmailVerificationController.php @@ -0,0 +1,62 @@ +validate([ + 'email' => ['required', 'string', 'email'], + 'otp' => ['required', 'string', 'digits:6'], + ]); + + $user = User::where('email', $validated['email'])->first(); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => 'Kod OTP tidak sah.', + ], 422); + } + + $this->otpService->verify($user, $validated['otp']); + + $user->refresh(); + + return $this->authSession->createAuthResponse( + $user, + 'E-mel anda telah berjaya disahkan. Akaun anda sedang menunggu pengaktifan daripada pentadbir sistem.' + ); + } + + public function resend(Request $request): JsonResponse + { + $validated = $request->validate([ + 'email' => ['required', 'string', 'email'], + ]); + + $user = User::where('email', $validated['email'])->first(); + + if ($user && ! $user->hasVerifiedEmail()) { + $this->otpService->send($user); + } + + return response()->json([ + 'success' => true, + 'message' => 'Jika e-mel wujud dan belum disahkan, kod OTP baharu telah dihantar.', + ]); + } +} diff --git a/be/Modules/Auth/Http/Controllers/SessionController.php b/be/Modules/Auth/Http/Controllers/SessionController.php new file mode 100644 index 0000000..34a0652 --- /dev/null +++ b/be/Modules/Auth/Http/Controllers/SessionController.php @@ -0,0 +1,66 @@ +user(); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => 'Unauthenticated', + ], 401); + } + + $user->load(['roles.permissions']); + + return response()->json([ + 'success' => true, + 'data' => new UserResource($user), + ...ActiveRoleService::sessionMeta($user), + ]); + } + + /** + * Switch active role for the current session token. + */ + public function switchRole(Request $request): JsonResponse + { + $validated = $request->validate([ + 'role_id' => ['required', 'uuid', 'exists:roles,id'], + ]); + + $user = $request->user(); + $user->load(['roles.permissions']); + + $role = ActiveRoleService::switchRole($user, $validated['role_id']); + + if (! $role) { + return response()->json([ + 'success' => false, + 'message' => 'Peranan tidak ditetapkan untuk pengguna ini.', + ], 403); + } + + return response()->json([ + 'success' => true, + 'message' => 'Peranan aktif dikemas kini.', + 'data' => new UserResource($user), + 'active_role' => ActiveRoleService::formatRole($role), + 'can_switch_role' => $user->roles->count() > 1, + 'redirect_path' => ActiveRoleService::redirectPathForRole($role), + ]); + } +} diff --git a/be/Modules/Auth/Jobs/.gitkeep b/be/Modules/Auth/Jobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Notifications/.gitkeep b/be/Modules/Auth/Notifications/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Policies/.gitkeep b/be/Modules/Auth/Policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Providers/AuthServiceProvider.php b/be/Modules/Auth/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..7cce36f --- /dev/null +++ b/be/Modules/Auth/Providers/AuthServiceProvider.php @@ -0,0 +1,171 @@ +registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, 'database/migrations')); + $this->registerFortify(); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, 'lang'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, 'lang')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, 'resources/views'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\'.$this->name.'\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } + + /** + * Register Fortify actions. + */ + protected function registerFortify(): void + { + Fortify::createUsersUsing(CreateNewUser::class); + Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class); + Fortify::updateUserPasswordsUsing(UpdateUserPassword::class); + Fortify::resetUserPasswordsUsing(ResetUserPassword::class); + } +} diff --git a/be/Modules/Auth/Providers/EventServiceProvider.php b/be/Modules/Auth/Providers/EventServiceProvider.php new file mode 100644 index 0000000..f84daf6 --- /dev/null +++ b/be/Modules/Auth/Providers/EventServiceProvider.php @@ -0,0 +1,27 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/Modules/Auth/Providers/RouteServiceProvider.php b/be/Modules/Auth/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..fbd0f44 --- /dev/null +++ b/be/Modules/Auth/Providers/RouteServiceProvider.php @@ -0,0 +1,35 @@ +mapApiRoutes(); + } + + protected function mapWebRoutes() + { + Route::middleware('web') + ->namespace($this->moduleNamespace) + ->group(module_path('Auth', '/Routes/web.php')); + } + + protected function mapApiRoutes() + { + Route::middleware('api') + ->namespace($this->moduleNamespace) + ->group(module_path('Auth', '/Routes/api.php')); + } +} diff --git a/be/Modules/Auth/Repositories/.gitkeep b/be/Modules/Auth/Repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Routes/.gitkeep b/be/Modules/Auth/Routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Routes/api.php b/be/Modules/Auth/Routes/api.php new file mode 100644 index 0000000..21d38a3 --- /dev/null +++ b/be/Modules/Auth/Routes/api.php @@ -0,0 +1,27 @@ +middleware('block.api.tools'); + +Route::post('/verify-email', [EmailVerificationController::class, 'verify']) + ->middleware('throttle:email-verification'); +Route::post('/verify-email/resend', [EmailVerificationController::class, 'resend']) + ->middleware('throttle:email-verification-resend'); + +Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () { + // get current user + Route::get('/me', [SessionController::class, 'currentUser']); + + // switch active role + Route::post('/active-role/switch', [SessionController::class, 'switchRole']); + + // logout + Route::post('/logout', [AuthenticatedSessionController::class, 'destroy']); +}); \ No newline at end of file diff --git a/be/Modules/Auth/Services/.gitkeep b/be/Modules/Auth/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Services/AuthSessionService.php b/be/Modules/Auth/Services/AuthSessionService.php new file mode 100644 index 0000000..bca4178 --- /dev/null +++ b/be/Modules/Auth/Services/AuthSessionService.php @@ -0,0 +1,49 @@ +tokens()->delete(); + } + + $token = $user->createToken( + name: 'authToken', + abilities: ['*'], + expiresAt: now()->addMinutes((int) config('auth_cookie.lifetime_minutes', 720)) + ); + + $user->load(['roles.permissions']); + + ActiveRoleService::assignDefaultToToken($user, $token->accessToken); + + $payload = [ + 'success' => true, + 'message' => $message, + 'data' => [ + 'user' => new UserResource($user), + 'token' => $token->plainTextToken, + 'token_type' => 'Bearer', + 'expires_at' => $token->accessToken->expires_at, + ], + ...ActiveRoleService::sessionMeta($user), + ]; + + if (AuthCookie::shouldExposeTokenInResponse()) { + $payload['data']['token'] = $token->plainTextToken; + } + + $response = response()->json($payload, $status); + + return AuthCookie::attachAuthToken($response, $token->plainTextToken); + } +} diff --git a/be/Modules/Auth/Services/EmailVerificationOtpService.php b/be/Modules/Auth/Services/EmailVerificationOtpService.php new file mode 100644 index 0000000..ed823d7 --- /dev/null +++ b/be/Modules/Auth/Services/EmailVerificationOtpService.php @@ -0,0 +1,98 @@ +hasVerifiedEmail()) { + return; + } + + $otp = $this->generateOtp(); + + EmailVerificationOtp::query() + ->where('user_id', $user->id) + ->delete(); + + EmailVerificationOtp::create([ + 'user_id' => $user->id, + 'code' => Hash::make($otp), + 'expires_at' => now()->addMinutes($this->expiryMinutes()), + 'attempts' => 0, + ]); + + $user->notify(new EmailVerificationOtpEmail($otp)); + } + + public function verify(User $user, string $otp): void + { + if ($user->hasVerifiedEmail()) { + throw ValidationException::withMessages([ + 'email' => ['E-mel anda telah disahkan.'], + ]); + } + + $record = EmailVerificationOtp::query() + ->where('user_id', $user->id) + ->latest() + ->first(); + + if (! $record) { + throw ValidationException::withMessages([ + 'otp' => ['Kod OTP tidak dijumpai. Sila minta kod baharu.'], + ]); + } + + if ($record->isExpired()) { + $record->delete(); + + throw ValidationException::withMessages([ + 'otp' => ['Kod OTP telah tamat tempoh. Sila minta kod baharu.'], + ]); + } + + if ($record->hasExceededMaxAttempts()) { + $record->delete(); + + throw ValidationException::withMessages([ + 'otp' => ['Terlalu banyak percubaan. Sila minta kod baharu.'], + ]); + } + + if (! Hash::check($otp, $record->code)) { + $record->increment('attempts'); + + if ($record->fresh()->hasExceededMaxAttempts()) { + $record->delete(); + } + + throw ValidationException::withMessages([ + 'otp' => ['Kod OTP tidak sah.'], + ]); + } + + $user->forceFill(['email_verified_at' => now()])->save(); + + EmailVerificationOtp::query() + ->where('user_id', $user->id) + ->delete(); + } + + protected function generateOtp(): string + { + return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT); + } + + protected function expiryMinutes(): int + { + return (int) config('auth.email_verification.expiry_minutes', 10); + } +} diff --git a/be/Modules/Auth/Tests/Feature/.gitkeep b/be/Modules/Auth/Tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Tests/Unit/.gitkeep b/be/Modules/Auth/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Transformers/.gitkeep b/be/Modules/Auth/Transformers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Auth/Transformers/AuthResource.php b/be/Modules/Auth/Transformers/AuthResource.php new file mode 100644 index 0000000..579bac3 --- /dev/null +++ b/be/Modules/Auth/Transformers/AuthResource.php @@ -0,0 +1,22 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'user' => new UserResource($this->resource), + 'token' => $this->token ?? null, + ]; + } +} diff --git a/be/Modules/Auth/Transformers/SSOUserResource.php b/be/Modules/Auth/Transformers/SSOUserResource.php new file mode 100644 index 0000000..d26441c --- /dev/null +++ b/be/Modules/Auth/Transformers/SSOUserResource.php @@ -0,0 +1,62 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'uuid' => $this->uuid, + 'name' => $this->name, + 'email' => $this->email, + 'army_number' => $this->army_number, + 'unit_id' => $this->unit_id, + 'rank_id' => $this->rank_id, + 'position_id' => $this->position_id, + 'phone_number' => $this->phone_number, + 'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null, + 'status' => $this->status, + 'token' => $this->when(isset($this->token), $this->token), + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'roles' => $this->whenLoaded('roles', function () { + return $this->roles->map(function ($role) { + return [ + 'id' => $role->id, + 'name' => $role->name, + 'guard_name' => $role->guard_name, + 'permissions' => $role->permissions ? $role->permissions->map(function ($permission) { + return [ + 'id' => $permission->id, + 'name' => $permission->name, + 'guard_name' => $permission->guard_name, + 'route_name' => $permission->route_name ?? null, + 'created_at' => $permission->created_at, + 'updated_at' => $permission->updated_at, + ]; + }) : [], + 'created_at' => $role->created_at, + 'updated_at' => $role->updated_at, + ]; + }); + }), + 'unit' => new UnitResource($this->whenLoaded('unit')), + 'rank' => new RankResource($this->whenLoaded('rank')), + 'position' => new PositionResource($this->whenLoaded('position')), + ]; + } +} diff --git a/be/Modules/Auth/Transformers/UserResource.php b/be/Modules/Auth/Transformers/UserResource.php new file mode 100644 index 0000000..a064874 --- /dev/null +++ b/be/Modules/Auth/Transformers/UserResource.php @@ -0,0 +1,61 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'ic_number' => $this->ic_number, + 'position' => $this->position, + 'phone_number' => $this->phone_number, + 'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null, + 'status' => $this->status, + 'two_factor_secret' => $this->two_factor_secret, + 'two_factor_recovery_codes' => $this->two_factor_recovery_codes, + 'two_factor_confirmed_at' => $this->two_factor_confirmed_at, + 'email_verified_at' => $this->email_verified_at, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'deleted_at' => $this->deleted_at, + 'roles' => $this->whenLoaded('roles', function () { + return $this->roles->map(function ($role) { + return [ + 'id' => $role->id, + 'name' => $role->name, + 'fullname' => $role->fullname ?? null, + 'context' => $role->context ?? 'member', + 'guard_name' => $role->guard_name, + 'permissions' => $this->when($role->relationLoaded('permissions'), function () use ($role) { + return $role->permissions->map(function ($permission) { + return [ + 'id' => $permission->id, + 'name' => $permission->name, + 'guard_name' => $permission->guard_name, + 'route_name' => $permission->route_name, + 'created_at' => $permission->created_at, + 'updated_at' => $permission->updated_at, + ]; + }); + }), + 'created_at' => $role->created_at, + 'updated_at' => $role->updated_at, + ]; + }); + }), + ]; + } +} diff --git a/be/Modules/Auth/composer.json b/be/Modules/Auth/composer.json new file mode 100644 index 0000000..5d41ef1 --- /dev/null +++ b/be/Modules/Auth/composer.json @@ -0,0 +1,30 @@ +{ + "name": "nwidart/auth", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\Auth\\": "App", + "Modules\\Auth\\Database\\Factories\\": "database/factories/", + "Modules\\Auth\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\Auth\\Tests\\": "tests/" + } + } +} diff --git a/be/Modules/Auth/module.json b/be/Modules/Auth/module.json new file mode 100644 index 0000000..7cbf9df --- /dev/null +++ b/be/Modules/Auth/module.json @@ -0,0 +1,11 @@ +{ + "name": "Auth", + "alias": "auth", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\Auth\\Providers\\AuthServiceProvider" + ], + "files": [] +} diff --git a/be/Modules/Auth/package.json b/be/Modules/Auth/package.json new file mode 100644 index 0000000..d6fbfc8 --- /dev/null +++ b/be/Modules/Auth/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.1.2", + "laravel-vite-plugin": "^0.7.5", + "sass": "^1.69.5", + "postcss": "^8.3.7", + "vite": "^4.0.0" + } +} diff --git a/be/Modules/Auth/resources/views/emails/verification-otp.blade.php b/be/Modules/Auth/resources/views/emails/verification-otp.blade.php new file mode 100644 index 0000000..05c3263 --- /dev/null +++ b/be/Modules/Auth/resources/views/emails/verification-otp.blade.php @@ -0,0 +1,26 @@ +@component('mail::message') +@if (! empty($logoPath) && file_exists($logoPath)) +
+ {{ config('app.name') }} +
+@endif + +# Pengesahan E-mel + +Assalamualaikum **{{ $name }}**, + +Terima kasih kerana mendaftar. Gunakan kod OTP di bawah untuk mengesahkan alamat e-mel anda. + +@component('mail::panel') +
+ {{ $otp }} +
+@endcomponent + +Kod ini akan tamat tempoh dalam **{{ $minutes }} minit**. + +Jika anda tidak membuat pendaftaran ini, abaikan e-mel ini. + +Terima kasih,
+{{ config('app.name') }} +@endcomponent diff --git a/be/Modules/Dashboard/Actions/.gitkeep b/be/Modules/Dashboard/Actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Config/.gitkeep b/be/Modules/Dashboard/Config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Config/config.php b/be/Modules/Dashboard/Config/config.php new file mode 100644 index 0000000..2c58cb0 --- /dev/null +++ b/be/Modules/Dashboard/Config/config.php @@ -0,0 +1,5 @@ + 'Dashboard', +]; diff --git a/be/Modules/Dashboard/Console/.gitkeep b/be/Modules/Dashboard/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Database/Factories/.gitkeep b/be/Modules/Dashboard/Database/Factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Database/Migrations/.gitkeep b/be/Modules/Dashboard/Database/Migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Database/Seeders/.gitkeep b/be/Modules/Dashboard/Database/Seeders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Database/Seeders/DashboardDatabaseSeeder.php b/be/Modules/Dashboard/Database/Seeders/DashboardDatabaseSeeder.php new file mode 100644 index 0000000..3cbe23c --- /dev/null +++ b/be/Modules/Dashboard/Database/Seeders/DashboardDatabaseSeeder.php @@ -0,0 +1,16 @@ +call([]); + } +} diff --git a/be/Modules/Dashboard/Emails/.gitkeep b/be/Modules/Dashboard/Emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Entities/.gitkeep b/be/Modules/Dashboard/Entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Helpers/.gitkeep b/be/Modules/Dashboard/Helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Http/Controllers/.gitkeep b/be/Modules/Dashboard/Http/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Http/Controllers/DashboardController.php b/be/Modules/Dashboard/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..341e9a2 --- /dev/null +++ b/be/Modules/Dashboard/Http/Controllers/DashboardController.php @@ -0,0 +1,109 @@ +kjcDashboardService = $kjcDashboardService; + $this->pkjDashboardService = $pkjDashboardService; + } + + /** + * Get dashboard overview data + */ + public function getOverview(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + + $overview = [ + 'total_units' => $this->kjcDashboardService->getTotalUnits($filters), + 'total_camps' => $this->kjcDashboardService->getTotalCamps($filters), + 'total_formations' => $this->kjcDashboardService->getTotalFormations($filters), + 'kjc' => [ + 'total_assets' => $this->kjcDashboardService->getTotalAssets($filters, $user), + 'operational_assets' => $this->kjcDashboardService->getOperationalAssets($filters, $user), + 'under_repair' => $this->kjcDashboardService->getUnderRepairAssets($filters, $user), + 'maintenance_due' => $this->kjcDashboardService->getMaintenanceDueAssets($filters, $user), + 'active_repairs' => $this->kjcDashboardService->getActiveRepairs($filters), + 'pending_reports' => $this->kjcDashboardService->getPendingReports($filters) + ], + 'pkj' => [ + 'total_assets' => $this->pkjDashboardService->getTotalAssets($filters, $user), + 'operational_assets' => $this->pkjDashboardService->getOperationalAssets($filters, $user), + 'under_repair' => $this->pkjDashboardService->getUnderRepairAssets($filters, $user), + 'maintenance_due' => $this->pkjDashboardService->getMaintenanceDueAssets($filters, $user), + 'active_repairs' => $this->pkjDashboardService->getActiveRepairs($filters), + 'pending_reports' => $this->pkjDashboardService->getPendingReports($filters) + ] + ]; + + return response()->json([ + 'success' => true, + 'data' => $overview + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch dashboard overview', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get stat cards data + */ + public function getStatCards(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + + $statCards = [ + 'kjc' => [ + 'total_assets' => $this->kjcDashboardService->getTotalAssets($filters, $user), + 'operational_assets' => $this->kjcDashboardService->getOperationalAssets($filters, $user), + 'under_repair' => $this->kjcDashboardService->getUnderRepairAssets($filters, $user), + 'maintenance_due' => $this->kjcDashboardService->getMaintenanceDueAssets($filters, $user), + ], + 'pkj' => [ + 'total_assets' => $this->pkjDashboardService->getTotalAssets($filters, $user), + 'operational_assets' => $this->pkjDashboardService->getOperationalAssets($filters, $user), + 'under_repair' => $this->pkjDashboardService->getUnderRepairAssets($filters, $user), + 'maintenance_due' => $this->pkjDashboardService->getMaintenanceDueAssets($filters, $user), + ], + 'overview' => [ + 'total_units' => $this->kjcDashboardService->getTotalUnits($filters), + 'active_repairs' => $this->kjcDashboardService->getActiveRepairs($filters) + $this->pkjDashboardService->getActiveRepairs($filters), + 'pending_reports' => $this->kjcDashboardService->getPendingReports($filters) + $this->pkjDashboardService->getPendingReports($filters), + ] + ]; + + return response()->json([ + 'success' => true, + 'data' => $statCards + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch stat cards', + 'error' => $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/be/Modules/Dashboard/Http/Controllers/KJC/KJCDashboardController.php b/be/Modules/Dashboard/Http/Controllers/KJC/KJCDashboardController.php new file mode 100644 index 0000000..7aeb2dd --- /dev/null +++ b/be/Modules/Dashboard/Http/Controllers/KJC/KJCDashboardController.php @@ -0,0 +1,302 @@ +kjcDashboardService = $kjcDashboardService; + $this->kjcDashboardRepository = $kjcDashboardRepository; + } + + /** + * Get KJC entitlement vs holdings pie chart + */ + public function getEntitlementVsHoldingsPieChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->kjcDashboardService->getEntitlementVsHoldingsPieChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch KJC entitlement vs holdings chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get KJC holdings status breakdown pie chart + */ + public function getHoldingsStatusBreakdownPieChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->kjcDashboardService->getHoldingsStatusBreakdownPieChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch KJC holdings status breakdown chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get KJC unit performance chart + */ + public function getUnitPerformanceChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->kjcDashboardService->getUnitPerformanceChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch KJC unit performance chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get KJC monthly trends chart + */ + public function getMonthlyTrendsChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->kjcDashboardService->getMonthlyTrendsChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch KJC monthly trends chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get KJC historical metrics bar chart + */ + public function getHistoricalMetricsBarChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->kjcDashboardService->getHistoricalMetricsBarChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch KJC historical metrics chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get drill-down data for entitlement vs holdings + */ + public function getEntitlementVsHoldingsDetails(Request $request): JsonResponse + { + try { + $filters = $request->only(['type', 'date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + + $type = $filters['type'] ?? 'entitlement'; + + if ($type === 'entitlement') { + // Return entitlement details + $entitlements = KJCAssetEntitlement::with(['kjcSubcategory']) + ->paginate(20); + + $data = [ + 'title' => 'KJC Asset Entitlements', + 'filters' => array_merge($filters, ['total_count' => $entitlements->total()]), + 'items' => $entitlements->items(), + 'pagination' => [ + 'current_page' => $entitlements->currentPage(), + 'per_page' => $entitlements->perPage(), + 'total' => $entitlements->total(), + 'last_page' => $entitlements->lastPage(), + 'from' => $entitlements->firstItem(), + 'to' => $entitlements->lastItem() + ] + ]; + } else { + // Return holdings details + $query = KJCAssetHolding::with(['unit', 'kjcCategory', 'kjcSubcategory', 'kjcModel']); + + if (isset($filters['date_from'])) { + $query->where('created_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('created_at', '<=', $filters['date_to']); + } + + if (isset($filters['unit_id'])) { + $query->where('unit_id', $filters['unit_id']); + } + + $holdings = $query->paginate(20); + + $data = [ + 'title' => 'KJC Asset Holdings', + 'filters' => array_merge($filters, ['total_count' => $holdings->total()]), + 'items' => $holdings->items(), + 'pagination' => [ + 'current_page' => $holdings->currentPage(), + 'per_page' => $holdings->perPage(), + 'total' => $holdings->total(), + 'last_page' => $holdings->lastPage(), + 'from' => $holdings->firstItem(), + 'to' => $holdings->lastItem() + ] + ]; + } + + return response()->json([ + 'success' => true, + 'data' => $data + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch entitlement vs holdings details', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get drill-down data for holdings status + */ + public function getHoldingsStatusDetails(Request $request): JsonResponse + { + try { + $filters = $request->only(['status', 'date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + + $query = KJCAssetHolding::with(['unit', 'kjcCategory', 'kjcSubcategory', 'kjcModel']); + + if (isset($filters['status'])) { + $query->where('status', $filters['status']); + } + + if (isset($filters['date_from'])) { + $query->where('created_at', '>=', $filters['date_from']); + } + + if (isset($filters['date_to'])) { + $query->where('created_at', '<=', $filters['date_to']); + } + + if (isset($filters['unit_id'])) { + $query->where('unit_id', $filters['unit_id']); + } + + $holdings = $query->paginate(20); + + // Map status codes to readable names + $statusNames = [ + 'BP' => 'Beroperasi (Operational)', + 'BDG' => 'Baik Diselenggara (Well Maintained)', + 'TBDG' => 'Tidak Baik Diselenggara (Poorly Maintained)' + ]; + + $statusName = $statusNames[$filters['status']] ?? $filters['status']; + + return response()->json([ + 'success' => true, + 'data' => [ + 'title' => "KJC Assets with Status: {$statusName}", + 'filters' => array_merge($filters, ['total_count' => $holdings->total()]), + 'items' => $holdings->items(), + 'pagination' => [ + 'current_page' => $holdings->currentPage(), + 'per_page' => $holdings->perPage(), + 'total' => $holdings->total(), + 'last_page' => $holdings->lastPage(), + 'from' => $holdings->firstItem(), + 'to' => $holdings->lastItem() + ] + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch holdings status details', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get KJC asset details by status (drill-down table, paginated). + * Status can be 'null' for assets not yet assigned a status (DB column NULL). + */ + public function getAssetDetailsByStatus(Request $request, string $status): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id', 'page', 'per_page']); + $user = $request->user(); + + $status = trim($status); + $status = $status === 'null' ? '' : $status; + + $details = $this->kjcDashboardRepository->getAssetDetailsByStatus($status, $filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $details + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch KJC asset details', + 'error' => $e->getMessage() + ], 500); + } + } +} diff --git a/be/Modules/Dashboard/Http/Controllers/PKJ/PKJDashboardController.php b/be/Modules/Dashboard/Http/Controllers/PKJ/PKJDashboardController.php new file mode 100644 index 0000000..67ca286 --- /dev/null +++ b/be/Modules/Dashboard/Http/Controllers/PKJ/PKJDashboardController.php @@ -0,0 +1,188 @@ +pkjDashboardService = $pkjDashboardService; + $this->pkjDashboardRepository = $pkjDashboardRepository; + } + + /** + * Get PKJ entitlement vs holdings pie chart + */ + public function getEntitlementVsHoldingsPieChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->pkjDashboardService->getEntitlementVsHoldingsPieChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch PKJ entitlement vs holdings chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get PKJ holdings status breakdown pie chart + */ + public function getHoldingsStatusBreakdownPieChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->pkjDashboardService->getHoldingsStatusBreakdownPieChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch PKJ holdings status breakdown chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get PKJ monthly trends chart + */ + public function getMonthlyTrendsChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->pkjDashboardService->getMonthlyTrendsChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch PKJ monthly trends chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get PKJ holdings by category bar chart + */ + public function getHoldingsByCategoryBarChart(Request $request): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->pkjDashboardService->getHoldingsByCategoryBarChart($filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch PKJ holdings by category chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get PKJ category metrics bar chart (drill-down) + */ + public function getCategoryMetricsBarChart(Request $request, int $categoryId): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->pkjDashboardService->getCategoryMetricsBarChart($categoryId, $filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch PKJ category metrics chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get PKJ category status breakdown pie chart (drill-down) + */ + public function getCategoryStatusBreakdownPieChart(Request $request, int $categoryId): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id']); + $user = $request->user(); // Get authenticated user + $chartData = $this->pkjDashboardService->getCategoryStatusBreakdownPieChart($categoryId, $filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $chartData + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch PKJ category status breakdown chart', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get PKJ asset details by category and status (drill-down) + */ + public function getAssetDetailsByCategoryAndStatus(Request $request, int $categoryId, string $status): JsonResponse + { + try { + $filters = $request->only(['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id', 'government_id', 'page', 'per_page']); + $user = $request->user(); // Get authenticated user + + // Convert 'null' string to empty string for null status handling + // Trim whitespace to ensure exact matching + $status = trim($status); + $status = $status === 'null' ? '' : $status; + + $details = $this->pkjDashboardRepository->getAssetDetailsByCategoryAndStatus($categoryId, $status, $filters, $user); + + return response()->json([ + 'success' => true, + 'data' => $details + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch PKJ asset details', + 'error' => $e->getMessage() + ], 500); + } + } +} diff --git a/be/Modules/Dashboard/Http/Requests/.gitkeep b/be/Modules/Dashboard/Http/Requests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Jobs/.gitkeep b/be/Modules/Dashboard/Jobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Notifications/.gitkeep b/be/Modules/Dashboard/Notifications/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Policies/.gitkeep b/be/Modules/Dashboard/Policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Providers/.gitkeep b/be/Modules/Dashboard/Providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Providers/DashboardServiceProvider.php b/be/Modules/Dashboard/Providers/DashboardServiceProvider.php new file mode 100644 index 0000000..c132690 --- /dev/null +++ b/be/Modules/Dashboard/Providers/DashboardServiceProvider.php @@ -0,0 +1,169 @@ +registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations')); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + + // Register dashboard repositories + $this->app->bind( + \Modules\Dashboard\Repositories\Contracts\KJCDashboardRepositoryInterface::class, + \Modules\Dashboard\Repositories\KJCDashboardRepository::class + ); + + $this->app->bind( + \Modules\Dashboard\Repositories\Contracts\PKJDashboardRepositoryInterface::class, + \Modules\Dashboard\Repositories\PKJDashboardRepository::class + ); + + // Register dashboard services + $this->app->singleton(\Modules\Dashboard\Services\KJC\KJCDashboardService::class); + $this->app->singleton(\Modules\Dashboard\Services\PKJ\PKJDashboardService::class); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, 'Lang')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, 'Resources/Views'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } +} diff --git a/be/Modules/Dashboard/Providers/EventServiceProvider.php b/be/Modules/Dashboard/Providers/EventServiceProvider.php new file mode 100644 index 0000000..29e6487 --- /dev/null +++ b/be/Modules/Dashboard/Providers/EventServiceProvider.php @@ -0,0 +1,27 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/Modules/Dashboard/Providers/RouteServiceProvider.php b/be/Modules/Dashboard/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..3a50d7c --- /dev/null +++ b/be/Modules/Dashboard/Providers/RouteServiceProvider.php @@ -0,0 +1,49 @@ +mapApiRoutes(); + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + */ + protected function mapWebRoutes(): void + { + Route::middleware('web')->group(module_path($this->name, '/Routes/web.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + */ + protected function mapApiRoutes(): void + { + Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php')); + } +} diff --git a/be/Modules/Dashboard/Repositories/.gitkeep b/be/Modules/Dashboard/Repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Repositories/Contracts/.gitkeep b/be/Modules/Dashboard/Repositories/Contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Repositories/Contracts/KJCDashboardRepositoryInterface.php b/be/Modules/Dashboard/Repositories/Contracts/KJCDashboardRepositoryInterface.php new file mode 100644 index 0000000..3506236 --- /dev/null +++ b/be/Modules/Dashboard/Repositories/Contracts/KJCDashboardRepositoryInterface.php @@ -0,0 +1,57 @@ +visibleTo($user); + } + + $this->applyFilters($query, $filters, excludeDateFilter: true); + + return $query->sum('entitlement'); + } + + /** + * Get total KJC asset holdings + */ + public function getTotalHoldings(array $filters = [], $user = null): int + { + $query = KJCAssetEntitlement::query(); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters, excludeDateFilter: true); + + return $query->sum('holding'); + } + + /** + * Get KJC asset holdings by status. + * Does not filter by date so totals match full KJCAssetHolding counts (unit/government/formation filters still apply). + */ + public function getHoldingsByStatus(array $filters = [], $user = null): array + { + $query = KJCAssetHolding::query(); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters, excludeDateFilter: true); + + return $query->selectRaw('status, COUNT(*) as count') + ->groupBy('status') + ->get() + ->toArray(); + } + + /** + * Get KJC asset holdings with pagination + */ + public function getHoldingsPaginated(array $filters = [], int $perPage = 20, $user = null): array + { + $query = KJCAssetHolding::with(['unit', 'kjcCategory', 'kjcSubcategory', 'kjcModel']); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters); + + $holdings = $query->paginate($perPage); + + return [ + 'items' => $holdings->items(), + 'pagination' => [ + 'current_page' => $holdings->currentPage(), + 'per_page' => $holdings->perPage(), + 'total' => $holdings->total(), + 'last_page' => $holdings->lastPage(), + 'from' => $holdings->firstItem(), + 'to' => $holdings->lastItem() + ] + ]; + } + + /** + * Get KJC asset entitlements with pagination + */ + public function getEntitlementsPaginated(array $filters = [], int $perPage = 20, $user = null): array + { + $query = KJCAssetEntitlement::with(['kjcSubcategory']); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters); + + $entitlements = $query->paginate($perPage); + + return [ + 'items' => $entitlements->items(), + 'pagination' => [ + 'current_page' => $entitlements->currentPage(), + 'per_page' => $entitlements->perPage(), + 'total' => $entitlements->total(), + 'last_page' => $entitlements->lastPage(), + 'from' => $entitlements->firstItem(), + 'to' => $entitlements->lastItem() + ] + ]; + } + + /** + * Get KJC asset holdings by specific status + */ + public function getHoldingsBySpecificStatus(string $status, array $filters = [], $user = null): array + { + $query = KJCAssetHolding::with(['unit', 'kjcCategory', 'kjcSubcategory', 'kjcModel']) + ->where('status', $status); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters); + + $holdings = $query->paginate(20); + + return [ + 'items' => $holdings->items(), + 'pagination' => [ + 'current_page' => $holdings->currentPage(), + 'per_page' => $holdings->perPage(), + 'total' => $holdings->total(), + 'last_page' => $holdings->lastPage(), + 'from' => $holdings->firstItem(), + 'to' => $holdings->lastItem() + ] + ]; + } + + /** + * Get KJC asset details by status (paginated). + * When status is '' or 'null', returns assets where status column is NULL (not yet assigned). + */ + public function getAssetDetailsByStatus(string $status, array $filters = [], $user = null): array + { + $status = trim($status ?? ''); + + $query = KJCAssetHolding::query() + ->with(['kjcCategory', 'kjcSubcategory', 'kjcModel', 'unit']) + ->select([ + 'id', + 'uuid', + 'kjc_category_id', + 'kjc_subcategory_id', + 'kjc_model_id', + 'registration_number', + 'kewpa_registration_number', + 'status', + 'unit_id', + 'purchase_date', + 'receipt_date', + 'purchase_price', + 'contract_reference', + 'economic_year', + 'economic_distance', + 'note', + 'engine_number', + 'chassis_number', + 'created_at', + 'updated_at' + ]); + + if ($status === '' || $status === 'null') { + $query->whereNull('status'); + } else { + $query->whereNotNull('status') + ->where('status', '=', $status); + } + + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters, excludeDateFilter: true); + + $perPage = isset($filters['per_page']) ? (int) $filters['per_page'] : 10; + $page = isset($filters['page']) ? (int) $filters['page'] : null; + $assets = $query->paginate($perPage, ['*'], 'page', $page); + + return [ + 'data' => $assets->items(), + 'pagination' => [ + 'current_page' => $assets->currentPage(), + 'per_page' => $assets->perPage(), + 'total' => $assets->total(), + 'last_page' => $assets->lastPage(), + 'from' => $assets->firstItem(), + 'to' => $assets->lastItem(), + 'has_more_pages' => $assets->hasMorePages() + ] + ]; + } + + /** + * Get KJC asset holdings grouped by unit + */ + public function getHoldingsByUnit(array $filters = [], $user = null): array + { + $query = KJCAssetHolding::query(); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters); + + return $query->join('units', 'kjc_asset_holdings.unit_id', '=', 'units.id') + ->selectRaw('units.name as unit_name, COUNT(*) as count') + ->groupBy('units.name') + ->get() + ->toArray(); + } + + /** + * Get KJC asset holdings monthly trends + */ + public function getMonthlyTrends(array $filters = [], $user = null): array + { + $dateRange = $this->getDateRange($filters); + + $query = KJCAssetHolding::query(); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters); + + return $query->selectRaw(' + DATE_FORMAT(created_at, "%Y-%m") as month, + status, + COUNT(*) as count + ') + ->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]) + ->groupBy('month', 'status') + ->orderBy('month') + ->get() + ->toArray(); + } + + /** + * Get KJC historical metrics (KEUPAYAAN, KESIAGAAN, SERVISIBILITI) by month + * 1. KEUPAYAAN (%) = (PEGANGAN/PERJAWATAN) * 100 + * 2. SIAPSIAGA or KESIAGAAN (%) = (BDG/perjawatan) * 100 + * 3. SERVISIBILITI (%) = (BDG/pegangan) * 100 + */ + public function getHistoricalMetricsByMonth(array $filters = [], $user = null): array + { + $dateRange = $this->getDateRange($filters); + $currentMonth = Carbon::now()->format('Y-m'); + + // Get entitlement and holding data by month from historical tables using Eloquent + $entitlementQuery = KJCHistoricalEntitlement::query() + ->selectRaw(' + TO_CHAR(kjc_historical_entitlement.date, \'YYYY-MM\') as month, + SUM(kjc_historical_entitlement.entitlement) as total_entitlement, + SUM(kjc_historical_entitlement.holding) as total_holding + ') + ->whereBetween('kjc_historical_entitlement.date', [$dateRange['from'], $dateRange['to']]); + + // Apply filters to historical entitlement query + $this->applyHistoricalFiltersToEloquent($entitlementQuery, $filters); + + // Apply visibility scoping if user is provided (uses HasVisibility trait) + if ($user) { + $entitlementQuery->visibleTo($user); + } + + $entitlementData = $entitlementQuery->groupBy('month') + ->orderBy('month') + ->get() + ->keyBy('month'); + + // Get BDG status count by month from historical tables using Eloquent + $bdgQuery = KJCHistoricalHoldingStatus::query() + ->selectRaw(' + TO_CHAR(kjc_historical_holding_status.date, \'YYYY-MM\') as month, + COUNT(*) as bdg_count + ') + ->where('kjc_historical_holding_status.status', 'BDG') + ->whereBetween('kjc_historical_holding_status.date', [$dateRange['from'], $dateRange['to']]); + + // Apply filters to historical holding status query + $this->applyHistoricalFiltersToEloquent($bdgQuery, $filters); + + // Apply visibility scoping if user is provided (uses HasVisibility trait) + if ($user) { + $bdgQuery->visibleTo($user); + } + + $bdgData = $bdgQuery->groupBy('month') + ->orderBy('month') + ->get() + ->keyBy('month'); + + // Get current month data from real-time tables if current month is in range + $currentMonthEntitlement = 0; + $currentMonthHolding = 0; + $currentMonthBdgCount = 0; + + if ($this->isCurrentMonthInRange($currentMonth, $dateRange)) { + // Get current month entitlement data (all entitlements, not filtered by updated_at) + // This ensures we get real-time entitlement values regardless of when they were last updated + $currentEntitlementQuery = KJCAssetEntitlement::query(); + if ($user) { + $currentEntitlementQuery->visibleTo($user); + } + $this->applyCurrentMonthFilters($currentEntitlementQuery, $filters); + + $currentMonthEntitlement = $currentEntitlementQuery->sum('entitlement'); + + // Calculate current month holdings from actual KJCAssetHolding records + // This ensures we get real-time holding counts even if entitlement.holding is stale + $currentHoldingQuery = KJCAssetHolding::query(); + if ($user) { + $currentHoldingQuery->visibleTo($user); + } + $this->applyCurrentMonthFilters($currentHoldingQuery, $filters); + + $currentMonthHolding = $currentHoldingQuery->count(); + + // Get current month BDG count (only holdings updated in current month) + // This shows BDG holdings that were updated/changed status in the current month + $currentBdgQuery = KJCAssetHolding::query() + ->where('status', 'BDG') + ->whereMonth('updated_at', Carbon::now()->month) + ->whereYear('updated_at', Carbon::now()->year); + if ($user) { + $currentBdgQuery->visibleTo($user); + } + $this->applyCurrentMonthFilters($currentBdgQuery, $filters); + + $currentMonthBdgCount = $currentBdgQuery->count(); + } + + // Generate all months in the range + $months = []; + $current = $dateRange['from']->copy()->startOfMonth(); + $end = $dateRange['to']->copy()->endOfMonth(); + + while ($current->lte($end)) { + $monthKey = $current->format('Y-m'); + $months[] = $monthKey; + $current->addMonth(); + } + + // Calculate metrics for each month + $result = []; + foreach ($months as $month) { + $entitlement = $entitlementData->get($month); + $bdg = $bdgData->get($month); + + // Use current month real-time data if this is the current month + if ($month === $currentMonth && $this->isCurrentMonthInRange($currentMonth, $dateRange)) { + $totalEntitlement = $currentMonthEntitlement; + $totalHolding = $currentMonthHolding; + $bdgCount = $currentMonthBdgCount; + } else { + $totalEntitlement = $entitlement ? $entitlement->total_entitlement : 0; + $totalHolding = $entitlement ? $entitlement->total_holding : 0; + $bdgCount = $bdg ? $bdg->bdg_count : 0; + } + + // Calculate metrics + $keupayaan = $totalEntitlement > 0 ? ($totalHolding / $totalEntitlement) * 100 : 0; + $kesiagaan = $totalEntitlement > 0 ? ($bdgCount / $totalEntitlement) * 100 : 0; + $servisibiliti = $totalHolding > 0 ? ($bdgCount / $totalHolding) * 100 : 0; + + $result[] = [ + 'month' => $month, + 'keupayaan' => round($keupayaan, 2), + 'kesiagaan' => round($kesiagaan, 2), + 'servisibiliti' => round($servisibiliti, 2), + 'total_entitlement' => $totalEntitlement, + 'total_holding' => $totalHolding, + 'bdg_count' => $bdgCount + ]; + } + + return $result; + } + + /** + * Apply common filters to query + */ + private function applyFilters($query, array $filters = [], bool $excludeDateFilter = false): void + { + $model = $query->getModel(); + if ($model && method_exists($model, 'getTable')) { + $schema = $model->getConnection()->getSchemaBuilder(); + $columns = $schema->getColumnListing($model->getTable()); + + // Only apply created_at filter if the model has created_at column (skip when excludeDateFilter) + if (!$excludeDateFilter && in_array('created_at', $columns)) { + $dateRange = $this->getDateRange($filters); + $query->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]); + } + + // Apply unit_id filter if model has unit_id column + if (isset($filters['unit_id']) && $filters['unit_id']) { + if (in_array('unit_id', $columns)) { + $query->where('unit_id', $filters['unit_id']); + } + } + + // Apply government_id filter if provided + if (isset($filters['government_id']) && $filters['government_id']) { + $unitIds = $this->getUnitsByGovernment($filters['government_id']); + if (in_array('unit_id', $columns)) { + if (!empty($unitIds)) { + $query->whereIn('unit_id', $unitIds); + } else { + // No units found for this government - return empty result + $query->whereNull('id'); + } + } + } + + // Apply camp_id filter + if (isset($filters['camp_id']) && $filters['camp_id']) { + $query->whereHas('unit', function ($q) use ($filters) { + $q->where('camp_id', $filters['camp_id']); + }); + } + + // Apply formation_id filter + if (isset($filters['formation_id']) && $filters['formation_id']) { + // Get units under this formation + $unitIds = Unit::where('formation_id', $filters['formation_id']) + ->pluck('id') + ->toArray(); + + if (!empty($unitIds)) { + if (in_array('unit_id', $columns)) { + $query->whereIn('unit_id', $unitIds); + } else { + // Fallback to whereHas if no unit_id column + $query->whereHas('unit', function ($q) use ($filters) { + $q->where('formation_id', $filters['formation_id']); + }); + } + } else { + // No units found for this formation - return empty result + $query->whereNull('id'); + } + } + } + } + + /** + * Get units by government ID + * Uses VisibilityService for consistent logic + */ + private function getUnitsByGovernment(int $governmentId): array + { + $governmentUnit = Unit::where('government_id', $governmentId)->first(); + if (!$governmentUnit) { + // If no direct unit found, try to get from formation + $formation = Formation::where('government_id', $governmentId)->first(); + if ($formation) { + $governmentUnit = Unit::where('formation_id', $formation->id)->first(); + } + } + + if (!$governmentUnit) { + return []; + } + + return $this->visibilityService->getUnitsUnderGovernment($governmentUnit); + } + + /** + * Get date range for filtering + */ + private function getDateRange(array $filters = []): array + { + $dateFrom = $filters['date_from'] ?? Carbon::now()->startOfYear(); + $dateTo = $filters['date_to'] ?? Carbon::now()->endOfYear(); + + return [ + 'from' => Carbon::parse($dateFrom)->startOfDay(), + 'to' => Carbon::parse($dateTo)->endOfDay() + ]; + } + + /** + * Apply filters to historical Eloquent queries + * Handles government_id, formation_id, unit_id, and camp_id filters + */ + private function applyHistoricalFiltersToEloquent($query, array $filters = []): void + { + $model = $query->getModel(); + $isHoldingStatus = $model instanceof KJCHistoricalHoldingStatus; + + // Apply unit_id filter + if (isset($filters['unit_id']) && $filters['unit_id']) { + if ($isHoldingStatus) { + // For holding status, filter through the relationship + $query->whereHas('kjcAssetHolding', function ($q) use ($filters) { + $q->where('unit_id', $filters['unit_id']); + }); + } else { + $query->where('unit_id', $filters['unit_id']); + } + } + + // Apply government_id filter + if (isset($filters['government_id']) && $filters['government_id']) { + $unitIds = $this->getUnitsByGovernment($filters['government_id']); + if (!empty($unitIds)) { + if ($isHoldingStatus) { + $query->whereHas('kjcAssetHolding', function ($q) use ($unitIds) { + $q->whereIn('unit_id', $unitIds); + }); + } else { + $query->whereIn('unit_id', $unitIds); + } + } else { + // No units found for this government - return empty result + $query->whereRaw('1 = 0'); + } + } + + // Apply formation_id filter + if (isset($filters['formation_id']) && $filters['formation_id']) { + // Get units under this formation + $unitIds = Unit::where('formation_id', $filters['formation_id']) + ->pluck('id') + ->toArray(); + + if (!empty($unitIds)) { + if ($isHoldingStatus) { + $query->whereHas('kjcAssetHolding', function ($q) use ($unitIds) { + $q->whereIn('unit_id', $unitIds); + }); + } else { + $query->whereIn('unit_id', $unitIds); + } + } else { + // No units found for this formation - return empty result + $query->whereRaw('1 = 0'); + } + } + + // Apply camp_id filter + if (isset($filters['camp_id']) && $filters['camp_id']) { + if ($isHoldingStatus) { + $query->whereHas('kjcAssetHolding.unit', function ($q) use ($filters) { + $q->where('camp_id', $filters['camp_id']); + }); + } else { + $query->whereHas('unit', function ($q) use ($filters) { + $q->where('camp_id', $filters['camp_id']); + }); + } + } + } + + /** + * Check if current month is within the date range + */ + private function isCurrentMonthInRange(string $currentMonth, array $dateRange): bool + { + $currentMonthDate = Carbon::createFromFormat('Y-m', $currentMonth)->startOfMonth(); + return $currentMonthDate->between($dateRange['from'], $dateRange['to']); + } + + /** + * Apply filters for current month data queries (without date range filtering) + */ + private function applyCurrentMonthFilters($query, array $filters = []): void + { + if (isset($filters['unit_id']) && $filters['unit_id']) { + $query->where('unit_id', $filters['unit_id']); + } + + // Apply government_id filter + if (isset($filters['government_id']) && $filters['government_id']) { + $unitIds = $this->getUnitsByGovernment($filters['government_id']); + if (!empty($unitIds)) { + $query->whereIn('unit_id', $unitIds); + } else { + // No units found for this government - return empty result + $query->whereNull('id'); + } + } + + if (isset($filters['camp_id']) && $filters['camp_id']) { + $query->whereHas('unit', function ($q) use ($filters) { + $q->where('camp_id', $filters['camp_id']); + }); + } + + if (isset($filters['formation_id']) && $filters['formation_id']) { + // Get units under this formation + $unitIds = Unit::where('formation_id', $filters['formation_id']) + ->pluck('id') + ->toArray(); + + if (!empty($unitIds)) { + $query->whereIn('unit_id', $unitIds); + } else { + // No units found for this formation - return empty result + $query->whereNull('id'); + } + } + } + +} diff --git a/be/Modules/Dashboard/Repositories/PKJDashboardRepository.php b/be/Modules/Dashboard/Repositories/PKJDashboardRepository.php new file mode 100644 index 0000000..99fa14a --- /dev/null +++ b/be/Modules/Dashboard/Repositories/PKJDashboardRepository.php @@ -0,0 +1,495 @@ +visibleTo($user); + } + + $this->applyFilters($query, $filters, excludeDateFilter: true); + + return $query->sum('entitlement'); + } + + public function getTotalHoldings(array $filters = [], $user = null): int + { + $query = PKJAssetEntitlement::query(); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters, excludeDateFilter: true); + + return $query->sum('holding') ?? 0; + } + + /** + * Get PKJ asset holdings grouped by status + */ + public function getHoldingsByStatus(array $filters = [], $user = null): array + { + $query = PKJAssetHolding::query(); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters); + + return $query->selectRaw('status, COUNT(*) as count') + ->groupBy('status') + ->orderBy('status') + ->get() + ->toArray(); + } + + /** + * Get PKJ asset holdings by specific status + */ + public function getHoldingsBySpecificStatus(string $status, array $filters = [], $user = null): array + { + $query = PKJAssetHolding::query(); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $this->applyFilters($query, $filters); + + $holdings = $query->where('status', $status)->paginate(10); + + return [ + 'data' => $holdings->items(), + 'pagination' => [ + 'current_page' => $holdings->currentPage(), + 'per_page' => $holdings->perPage(), + 'total' => $holdings->total(), + 'last_page' => $holdings->lastPage(), + 'from' => $holdings->firstItem(), + 'to' => $holdings->lastItem() + ] + ]; + } + + /** + * Get PKJ monthly trends + */ + public function getMonthlyTrends(array $filters = [], $user = null): array + { + $dateRange = $this->getDateRange($filters); + + $query = PKJReport::query(); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + $trends = $query->selectRaw(' + TO_CHAR(created_at, \'YYYY-MM\') as month, + COUNT(*) as total_reports + ') + ->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]) + ->groupBy('month') + ->orderBy('month') + ->get() + ->toArray(); + + return $trends; + } + + /** + * Get PKJ holdings by category + */ + public function getHoldingsByCategory(array $filters = [], $user = null): array + { + // Build a subquery for filtered entitlements + // This ensures all categories are included, but only matching entitlements are summed + $entitlementSubquery = PKJAssetEntitlement::query() + ->select('pkj_category_id', DB::raw('SUM(holding) as total_holding')) + ->groupBy('pkj_category_id'); + + // Apply visibility scoping if user is provided + if ($user) { + $entitlementSubquery->visibleTo($user); + } + + // Apply filters to entitlement subquery + if (isset($filters['unit_id'])) { + $entitlementSubquery->where('unit_id', $filters['unit_id']); + } + + // Apply government_id filter + if (isset($filters['government_id']) && $filters['government_id']) { + $unitIds = $this->getUnitsByGovernment($filters['government_id']); + if (!empty($unitIds)) { + $entitlementSubquery->whereIn('unit_id', $unitIds); + } else { + // If no units match, set to empty result but don't filter categories + $entitlementSubquery->whereRaw('1 = 0'); + } + } + + // Apply formation_id filter + if (isset($filters['formation_id']) && $filters['formation_id']) { + $unitIds = Unit::where('formation_id', $filters['formation_id']) + ->pluck('id') + ->toArray(); + + if (!empty($unitIds)) { + $entitlementSubquery->whereIn('unit_id', $unitIds); + } else { + // If no units match, set to empty result but don't filter categories + $entitlementSubquery->whereRaw('1 = 0'); + } + } + + // Main query: Get ALL categories and LEFT JOIN with filtered entitlements + // This ensures all categories are returned even if they have no matching entitlements + $query = PKJCategory::query() + ->leftJoinSub($entitlementSubquery, 'filtered_entitlements', function ($join) { + $join->on('pkj_categories.id', '=', 'filtered_entitlements.pkj_category_id'); + }) + ->selectRaw(' + pkj_categories.id as category_id, + pkj_categories.name as category_name, + COALESCE(filtered_entitlements.total_holding, 0) as total_holding + ') + ->orderBy('pkj_categories.name'); + + return $query->get()->toArray(); + } + + /** + * Get PKJ category metrics (KEUPAYAAN, KESIAGAAN, SERVISIBILITI) + */ + public function getCategoryMetrics(int $categoryId, array $filters = [], $user = null): array + { + // Get category name + $category = PKJCategory::find($categoryId); + $categoryName = $category ? $category->name : 'Unknown Category'; + + // Get entitlement and holding data for the category + $entitlementQuery = PKJAssetEntitlement::query() + ->where('pkj_category_id', $categoryId); + + // Apply visibility scoping if user is provided + if ($user) { + $entitlementQuery->visibleTo($user); + } + + // Apply filters + if (isset($filters['unit_id'])) { + $entitlementQuery->where('unit_id', $filters['unit_id']); + } + + $entitlementData = $entitlementQuery->selectRaw(' + SUM(entitlement) as total_entitlement, + SUM(holding) as total_holding + ')->first(); + + // Get operational/usable assets count (BDG, BT, BP statuses) for the category + $operationalQuery = PKJAssetHolding::query() + ->where('pkj_category_id', $categoryId) + ->whereIn('status', ['BDG', 'BT', 'BP']); + + // Apply visibility scoping if user is provided + if ($user) { + $operationalQuery->visibleTo($user); + } + + // Apply filters + if (isset($filters['unit_id'])) { + $operationalQuery->where('unit_id', $filters['unit_id']); + } + + $operationalCount = $operationalQuery->count(); + + // Get BDG, BT, BG count specifically for servisibiliti calculation + $bdgQuery = PKJAssetHolding::query() + ->where('pkj_category_id', $categoryId) + ->whereIn('status', ['BDG', 'BT', 'BG']); + + // Apply visibility scoping if user is provided + if ($user) { + $bdgQuery->visibleTo($user); + } + + // Apply filters + if (isset($filters['unit_id'])) { + $bdgQuery->where('unit_id', $filters['unit_id']); + } + + $bdgCount = $bdgQuery->count(); + + $totalEntitlement = $entitlementData ? $entitlementData->total_entitlement : 0; + $totalHolding = $entitlementData ? $entitlementData->total_holding : 0; + + // Calculate metrics + $keupayaan = $totalEntitlement > 0 ? ($totalHolding / $totalEntitlement) * 100 : 0; + $kesiagaan = $totalEntitlement > 0 ? ($operationalCount / $totalEntitlement) * 100 : 0; + $servisibiliti = $totalHolding > 0 ? ($bdgCount / $totalHolding) * 100 : 0; + + return [ + 'category_id' => $categoryId, + 'category_name' => $categoryName, + 'keupayaan' => round($keupayaan, 2), + 'kesiagaan' => round($kesiagaan, 2), + 'servisibiliti' => round($servisibiliti, 2), + 'total_entitlement' => $totalEntitlement, + 'total_holding' => $totalHolding, + 'operational_count' => $operationalCount, + 'bdg_count' => $bdgCount + ]; + } + + /** + * Get PKJ category status breakdown + */ + public function getCategoryStatusBreakdown(int $categoryId, array $filters = [], $user = null): array + { + // Get category name + $category = PKJCategory::find($categoryId); + $categoryName = $category ? $category->name : 'Unknown Category'; + + $query = PKJAssetHolding::query() + ->where('pkj_category_id', $categoryId) + ->selectRaw('status, COUNT(*) as count') + ->groupBy('status') + ->orderBy('status'); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + // Apply filters + if (isset($filters['unit_id'])) { + $query->where('unit_id', $filters['unit_id']); + } + + $statusCounts = $query->get()->toArray(); + + return [ + 'category_id' => $categoryId, + 'category_name' => $categoryName, + 'status_breakdown' => $statusCounts + ]; + } + + /** + * Get PKJ asset details by category and status. + * + * When the asset has no status yet, the status column in the database is NULL. + * Callers pass status as '' or the string 'null' to request these "no status" assets. + */ + public function getAssetDetailsByCategoryAndStatus(int $categoryId, string $status, array $filters = [], $user = null): array + { + // Handle no-status: API passes '' or 'null'; DB column is NULL for assets not yet assigned a status + $status = trim($status ?? ''); + + $query = PKJAssetHolding::query() + ->with(['unit', 'pkjCategory', 'pkjSubcategory', 'pkjModel']) + ->where('pkj_category_id', $categoryId); + + if ($status === '' || $status === 'null') { + // Assets with status column NULL (not yet assigned a status) + $query->whereNull('status'); + } else { + // For specific status, must match exactly and NOT be null + $query->whereNotNull('status') + ->where('status', '=', $status); + } + + $query->select([ + 'id', + 'uuid', + 'registration_number', + 'kewpa_registration_number', + 'status', + 'unit_id', + 'pkj_category_id', + 'pkj_subcategory_id', + 'pkj_model_id', + 'purchase_date', + 'receipt_date', + 'purchase_price', + 'contract_reference', + 'country_of_manufacture', + 'economic_year', + 'economic_distance', + 'note', + 'engine_number', + 'chassis_number', + 'created_at', + 'updated_at' + ]); + + // Apply visibility scoping if user is provided + if ($user) { + $query->visibleTo($user); + } + + // Apply filters + if (isset($filters['unit_id'])) { + $query->where('unit_id', $filters['unit_id']); + } + + $perPage = isset($filters['per_page']) ? (int) $filters['per_page'] : 10; + $page = isset($filters['page']) ? (int) $filters['page'] : null; + $assets = $query->paginate($perPage, ['*'], 'page', $page); + + return [ + 'data' => $assets->items(), + 'pagination' => [ + 'current_page' => $assets->currentPage(), + 'per_page' => $assets->perPage(), + 'total' => $assets->total(), + 'last_page' => $assets->lastPage(), + 'from' => $assets->firstItem(), + 'to' => $assets->lastItem(), + 'has_more_pages' => $assets->hasMorePages() + ] + ]; + } + + /** + * Apply common filters to query + */ + private function applyFilters($query, array $filters = [], bool $excludeDateFilter = false): void + { + // Only apply created_at filter if the model has created_at column + $model = $query->getModel(); + if ($model && method_exists($model, 'getTable')) { + $schema = $model->getConnection()->getSchemaBuilder(); + $columns = $schema->getColumnListing($model->getTable()); + + if (!$excludeDateFilter && in_array('created_at', $columns)) { + $dateRange = $this->getDateRange($filters); + $query->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]); + } + } + + // Apply unit_id filter if model has unit_id column + if (isset($filters['unit_id']) && $filters['unit_id']) { + if ($model && method_exists($model, 'getTable')) { + $columns = $schema->getColumnListing($model->getTable()); + if (in_array('unit_id', $columns)) { + $query->where('unit_id', $filters['unit_id']); + } + } + } + + // Apply government_id filter if provided + if (isset($filters['government_id']) && $filters['government_id']) { + // Get units under the government + $unitIds = $this->getUnitsByGovernment($filters['government_id']); + + if ($model && method_exists($model, 'getTable')) { + $columns = $schema->getColumnListing($model->getTable()); + if (in_array('unit_id', $columns)) { + if (!empty($unitIds)) { + $query->whereIn('unit_id', $unitIds); + } else { + // No units found for this government - return empty result + $query->whereNull('id'); + } + } + } + } + + // Apply formation_id filter if provided + if (isset($filters['formation_id']) && $filters['formation_id']) { + if ($model && method_exists($model, 'getTable')) { + $columns = $schema->getColumnListing($model->getTable()); + if (in_array('unit_id', $columns)) { + // Get units under this formation + $unitIds = Unit::where('formation_id', $filters['formation_id']) + ->pluck('id') + ->toArray(); + + if (!empty($unitIds)) { + $query->whereIn('unit_id', $unitIds); + } else { + // No units found for this formation - return empty result + $query->whereNull('id'); + } + } + } + } + + // Apply camp_id filter if provided + if (isset($filters['camp_id']) && $filters['camp_id']) { + if ($model && method_exists($model, 'getTable')) { + $columns = $schema->getColumnListing($model->getTable()); + if (in_array('camp_id', $columns)) { + $query->where('camp_id', $filters['camp_id']); + } + } + } + } + + /** + * Get units by government ID + * Uses VisibilityService for consistent logic + */ + private function getUnitsByGovernment(int $governmentId): array + { + $governmentUnit = Unit::where('government_id', $governmentId)->first(); + if (!$governmentUnit) { + // If no direct unit found, try to get from formation + $formation = \Modules\Formation\Entities\Formation::where('government_id', $governmentId)->first(); + if ($formation) { + $governmentUnit = Unit::where('formation_id', $formation->id)->first(); + } + } + + if (!$governmentUnit) { + return []; + } + + return $this->visibilityService->getUnitsUnderGovernment($governmentUnit); + } + + /** + * Get date range for filtering + */ + private function getDateRange(array $filters = []): array + { + $dateFrom = $filters['date_from'] ?? Carbon::now()->startOfYear(); + $dateTo = $filters['date_to'] ?? Carbon::now()->endOfYear(); + + return [ + 'from' => Carbon::parse($dateFrom)->startOfDay(), + 'to' => Carbon::parse($dateTo)->endOfDay() + ]; + } +} diff --git a/be/Modules/Dashboard/Routes/.gitkeep b/be/Modules/Dashboard/Routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Routes/api.php b/be/Modules/Dashboard/Routes/api.php new file mode 100644 index 0000000..5fedfb7 --- /dev/null +++ b/be/Modules/Dashboard/Routes/api.php @@ -0,0 +1,44 @@ +prefix('v1')->group(function () { + // Main dashboard endpoints + Route::get('/dashboard/stat-cards', [DashboardController::class, 'getStatCards']); + Route::get('/dashboard/overview', [DashboardController::class, 'getOverview']); + + // KJC specific endpoints + Route::prefix('dashboard/kjc')->group(function () { + // Pie charts + Route::get('/entitlement-vs-holdings-pie', [KJCDashboardController::class, 'getEntitlementVsHoldingsPieChart']); + Route::get('/holdings-status-breakdown-pie', [KJCDashboardController::class, 'getHoldingsStatusBreakdownPieChart']); + + // Bar charts + Route::get('/historical-metrics-bar', [KJCDashboardController::class, 'getHistoricalMetricsBarChart']); + + // Other charts + Route::get('/unit-performance-chart', [KJCDashboardController::class, 'getUnitPerformanceChart']); + Route::get('/monthly-trends-chart', [KJCDashboardController::class, 'getMonthlyTrendsChart']); + + // Drill-down endpoints + Route::get('/drill-down/entitlement-vs-holdings', [KJCDashboardController::class, 'getEntitlementVsHoldingsDetails']); + Route::get('/drill-down/holdings-status', [KJCDashboardController::class, 'getHoldingsStatusDetails']); + Route::get('/status/{status}/details', [KJCDashboardController::class, 'getAssetDetailsByStatus']); + }); + + // PKJ specific endpoints + Route::prefix('dashboard/pkj')->group(function () { + Route::get('/entitlement-vs-holdings-pie', [PKJDashboardController::class, 'getEntitlementVsHoldingsPieChart']); + Route::get('/holdings-status-breakdown-pie', [PKJDashboardController::class, 'getHoldingsStatusBreakdownPieChart']); + Route::get('/monthly-trends', [PKJDashboardController::class, 'getMonthlyTrendsChart']); + Route::get('/holdings-by-category-bar', [PKJDashboardController::class, 'getHoldingsByCategoryBarChart']); + + // Drill-down endpoints + Route::get('/category/{categoryId}/metrics-bar', [PKJDashboardController::class, 'getCategoryMetricsBarChart']); + Route::get('/category/{categoryId}/status-breakdown-pie', [PKJDashboardController::class, 'getCategoryStatusBreakdownPieChart']); + Route::get('/category/{categoryId}/status/{status}/details', [PKJDashboardController::class, 'getAssetDetailsByCategoryAndStatus']); + }); +}); \ No newline at end of file diff --git a/be/Modules/Dashboard/Services/.gitkeep b/be/Modules/Dashboard/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Services/DashboardService.php b/be/Modules/Dashboard/Services/DashboardService.php new file mode 100644 index 0000000..73ab4f8 --- /dev/null +++ b/be/Modules/Dashboard/Services/DashboardService.php @@ -0,0 +1,486 @@ +startOfYear(); + $dateTo = $filters['date_to'] ?? Carbon::now()->endOfYear(); + + return [ + 'from' => Carbon::parse($dateFrom)->startOfDay(), + 'to' => Carbon::parse($dateTo)->endOfDay() + ]; + } + + /** + * Apply common filters to query + */ + protected function applyCommonFilters($query, array $filters = []): void + { + $dateRange = $this->getDateRange($filters); + + $query->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]); + + // Government filter: Filter by government (get all units under that government) + if (isset($filters['government_id']) && $filters['government_id']) { + $unitIds = $this->getUnitsByGovernment($filters['government_id']); + + if (!empty($unitIds)) { + $query->whereIn('unit_id', $unitIds); + } else { + // No units found for this government + $query->whereNull('id'); + } + } + + if (isset($filters['unit_id']) && $filters['unit_id']) { + $query->where('unit_id', $filters['unit_id']); + } + + if (isset($filters['camp_id']) && $filters['camp_id']) { + $query->whereHas('unit', function ($q) use ($filters) { + $q->where('camp_id', $filters['camp_id']); + }); + } + + if (isset($filters['formation_id']) && $filters['formation_id']) { + $query->whereHas('unit', function ($q) use ($filters) { + $q->where('formation_id', $filters['formation_id']); + }); + } + } + + /** + * Apply visibility scoping to query based on user permissions + */ + protected function applyVisibilityScoping($query, $user): void + { + if (!$user) { + return; + } + + // Check if the model has the HasVisibility trait + if (method_exists($query->getModel(), 'scopeVisibleTo')) { + $query->visibleTo($user); + } + } + + /** + * Apply visibility scoping to raw database queries + * Uses VisibilityService for consistent logic + */ + protected function applyRawVisibilityScoping($query, $user, string $tableName): void + { + if (!$user) { + return; + } + + $userUnit = Unit::find($user->unit_id); + if (!$userUnit) { + $query->whereRaw('1 = 0'); + return; + } + + $unitIds = $this->visibilityService->getVisibleUnitIds($user); + + if (empty($unitIds)) { + $query->whereRaw('1 = 0'); + } else { + $query->whereIn('unit_id', $unitIds); + } + } + + /** + * Get all units by government ID + * Uses VisibilityService for consistent logic + */ + protected function getUnitsByGovernment(int $governmentId): array + { + $governmentUnit = Unit::where('government_id', $governmentId)->first(); + if (!$governmentUnit) { + // If no direct unit found, try to get from formation + $formation = Formation::where('government_id', $governmentId)->first(); + if ($formation) { + $governmentUnit = Unit::where('formation_id', $formation->id)->first(); + } + } + + if (!$governmentUnit) { + return []; + } + + return $this->visibilityService->getUnitsUnderGovernment($governmentUnit); + } + + /** + * Get total units count + */ + public function getTotalUnits(array $filters = []): int + { + $query = Unit::query(); + + if (isset($filters['camp_id']) && $filters['camp_id']) { + $query->where('camp_id', $filters['camp_id']); + } + + if (isset($filters['formation_id']) && $filters['formation_id']) { + $query->where('formation_id', $filters['formation_id']); + } + + return $query->count(); + } + + /** + * Get total camps count + */ + public function getTotalCamps(array $filters = []): int + { + return Camp::count(); + } + + /** + * Get total formations count + */ + public function getTotalFormations(array $filters = []): int + { + return Formation::count(); + } + + /** + * Get active repairs count (to be implemented by child classes) + */ + abstract public function getActiveRepairs(array $filters = []): int; + + /** + * Get pending reports count (to be implemented by child classes) + */ + abstract public function getPendingReports(array $filters = []): int; + + /** + * Get total assets count (to be implemented by child classes) + */ + abstract public function getTotalAssets(array $filters = []): int; + + /** + * Get operational assets count (to be implemented by child classes) + */ + abstract public function getOperationalAssets(array $filters = []): int; + + /** + * Get under repair assets count (to be implemented by child classes) + */ + abstract public function getUnderRepairAssets(array $filters = []): int; + + /** + * Get maintenance due assets count (to be implemented by child classes) + */ + abstract public function getMaintenanceDueAssets(array $filters = []): int; + + /** + * Format chart data with common structure + */ + protected function formatChartData( + string $chartType, + string $title, + array $series, + array $categories = [], + array $drillDown = [] + ): array { + return [ + 'chart_type' => $chartType, + 'title' => $title, + 'series' => $series, + 'categories' => $categories, + 'drill_down' => $drillDown + ]; + } + + /** + * Format pie chart data + */ + protected function formatPieChartData( + string $title, + array $data, + string $drillDownEndpoint = '', + array $drillDownParameters = [] + ): array { + $total = array_sum(array_column($data, 'value')); + $series = []; + + foreach ($data as $item) { + $percentage = $total > 0 ? round(($item['value'] / $total) * 100, 1) : 0; + + $seriesItem = [ + 'name' => $item['name'], + 'value' => $item['value'], + 'percentage' => $percentage + ]; + + // Preserve status_code if it exists (for drill-down functionality) + if (isset($item['status_code'])) { + $seriesItem['status_code'] = $item['status_code']; + } + + $series[] = $seriesItem; + } + + return $this->formatChartData( + 'pie', + $title, + $series, + [], + [ + 'enabled' => !empty($drillDownEndpoint), + 'endpoint' => $drillDownEndpoint, + 'parameters' => $drillDownParameters + ] + ); + } + + /** + * Format bar chart data + */ + protected function formatBarChartData( + string $title, + array $series, + array $categories, + string $drillDownEndpoint = '', + array $drillDownParameters = [] + ): array { + return $this->formatChartData( + 'bar', + $title, + $series, + $categories, + [ + 'enabled' => !empty($drillDownEndpoint), + 'endpoint' => $drillDownEndpoint, + 'parameters' => $drillDownParameters + ] + ); + } + + /** + * Format line chart data + */ + protected function formatLineChartData( + string $title, + array $series, + array $categories, + string $drillDownEndpoint = '', + array $drillDownParameters = [] + ): array { + return $this->formatChartData( + 'line', + $title, + $series, + $categories, + [ + 'enabled' => !empty($drillDownEndpoint), + 'endpoint' => $drillDownEndpoint, + 'parameters' => $drillDownParameters + ] + ); + } + + /** + * Get monthly categories for date range + */ + protected function getMonthlyCategories(array $filters = []): array + { + $dateRange = $this->getDateRange($filters); + $categories = []; + + $current = $dateRange['from']->copy()->startOfMonth(); + $end = $dateRange['to']->copy()->endOfMonth(); + + while ($current->lte($end)) { + $categories[] = $current->format('M Y'); + $current->addMonth(); + } + + return $categories; + } + + /** + * Get weekly categories for date range + */ + protected function getWeeklyCategories(array $filters = []): array + { + $dateRange = $this->getDateRange($filters); + $categories = []; + + $current = $dateRange['from']->copy()->startOfWeek(); + $end = $dateRange['to']->copy()->endOfWeek(); + + while ($current->lte($end)) { + $categories[] = 'Week ' . $current->weekOfYear . ' ' . $current->year; + $current->addWeek(); + } + + return $categories; + } + + /** + * Get daily categories for date range + */ + protected function getDailyCategories(array $filters = []): array + { + $dateRange = $this->getDateRange($filters); + $categories = []; + + $current = $dateRange['from']->copy(); + $end = $dateRange['to']->copy(); + + while ($current->lte($end)) { + $categories[] = $current->format('M d'); + $current->addDay(); + } + + return $categories; + } + + /** + * Calculate percentage change + */ + protected function calculatePercentageChange(int $current, int $previous): float + { + if ($previous === 0) { + return $current > 0 ? 100.0 : 0.0; + } + + return round((($current - $previous) / $previous) * 100, 1); + } + + /** + * Get status color mapping + */ + protected function getStatusColors(): array + { + return [ + 'operational' => '#4caf50', + 'under_repair' => '#ff9800', + 'maintenance_due' => '#f44336', + 'out_of_service' => '#9e9e9e', + 'completed' => '#4caf50', + 'pending' => '#ff9800', + 'in_progress' => '#2196f3', + 'cancelled' => '#f44336' + ]; + } + + /** + * Get priority color mapping + */ + protected function getPriorityColors(): array + { + return [ + 'high' => '#f44336', + 'medium' => '#ff9800', + 'low' => '#4caf50' + ]; + } + + /** + * Format currency + */ + protected function formatCurrency(float $amount): string + { + return 'RM ' . number_format($amount, 2); + } + + /** + * Format duration in hours + */ + protected function formatDuration(float $hours): string + { + if ($hours < 24) { + return round($hours, 1) . ' hours'; + } + + $days = floor($hours / 24); + $remainingHours = $hours % 24; + + if ($remainingHours > 0) { + return $days . ' days ' . round($remainingHours, 1) . ' hours'; + } + + return $days . ' days'; + } + + /** + * Get common drill-down parameters + */ + protected function getCommonDrillDownParameters(): array + { + return ['date_from', 'date_to', 'unit_id', 'camp_id', 'formation_id']; + } + + /** + * Build drill-down endpoint + */ + protected function buildDrillDownEndpoint(string $baseEndpoint, array $parameters = []): string + { + $endpoint = $baseEndpoint; + + if (!empty($parameters)) { + $endpoint .= '?' . http_build_query($parameters); + } + + return $endpoint; + } + + /** + * Get summary statistics for drill-down + */ + protected function getSummaryStatistics($query, array $filters = []): array + { + $total = $query->count(); + + // Get breakdown by unit + $byUnit = $query->clone() + ->join('units', function ($join) { + $join->on('units.id', '=', $this->getUnitIdColumn()); + }) + ->selectRaw('units.name as unit_name, COUNT(*) as count') + ->groupBy('units.name') + ->get() + ->toArray(); + + return [ + 'total' => $total, + 'by_unit' => $byUnit + ]; + } + + /** + * Get unit ID column name (to be implemented by child classes) + */ + abstract protected function getUnitIdColumn(): string; + + /** + * Get asset model class (to be implemented by child classes) + */ + abstract protected function getAssetModelClass(): string; + + /** + * Get repair model class (to be implemented by child classes) + */ + abstract protected function getRepairModelClass(): string; +} \ No newline at end of file diff --git a/be/Modules/Dashboard/Services/KJC/KJCDashboardService.php b/be/Modules/Dashboard/Services/KJC/KJCDashboardService.php new file mode 100644 index 0000000..2e276e9 --- /dev/null +++ b/be/Modules/Dashboard/Services/KJC/KJCDashboardService.php @@ -0,0 +1,315 @@ +kjcDashboardRepository = $kjcDashboardRepository; + } + /** + * Get total KJC assets count + */ + public function getTotalAssets(array $filters = [], $user = null): int + { + return $this->kjcDashboardRepository->getTotalHoldings($filters, $user); + } + + /** + * Get operational KJC assets count (BP status) + */ + public function getOperationalAssets(array $filters = [], $user = null): int + { + $holdings = $this->kjcDashboardRepository->getHoldingsBySpecificStatus('BP', $filters, $user); + return $holdings['pagination']['total']; + } + + /** + * Get under repair KJC assets count (BDG status) + */ + public function getUnderRepairAssets(array $filters = [], $user = null): int + { + $holdings = $this->kjcDashboardRepository->getHoldingsBySpecificStatus('BDG', $filters, $user); + return $holdings['pagination']['total']; + } + + /** + * Get maintenance due KJC assets count (TBDG status) + */ + public function getMaintenanceDueAssets(array $filters = [], $user = null): int + { + $holdings = $this->kjcDashboardRepository->getHoldingsBySpecificStatus('TBDG', $filters, $user); + return $holdings['pagination']['total']; + } + + /** + * Get active KJC repairs count + */ + public function getActiveRepairs(array $filters = []): int + { + $query = KJCRepair::whereIn('status', ['pending', 'in_progress']); + $this->applyCommonFilters($query, $filters); + + return $query->count(); + } + + /** + * Get pending KJC reports count + */ + public function getPendingReports(array $filters = []): int + { + $query = KJCReport::where('status', 'pending'); + $this->applyCommonFilters($query, $filters); + + return $query->count(); + } + + /** + * Get KJC entitlement vs holdings pie chart + */ + public function getEntitlementVsHoldingsPieChart(array $filters = [], $user = null): array + { + // Get total entitlement + $totalEntitlement = $this->kjcDashboardRepository->getTotalEntitlement($filters, $user); + + // Get total holdings + $totalHoldings = $this->kjcDashboardRepository->getTotalHoldings($filters, $user); + + // Prepare data for pie chart (only showing Perjawatan and Pegangan) + $data = [ + [ + 'name' => 'Perjawatan', + 'value' => $totalEntitlement, + 'color' => '#4caf50' + ], + [ + 'name' => 'Pegangan', + 'value' => $totalHoldings, + 'color' => '#2196f3' + ] + ]; + + return $this->formatPieChartData( + 'Perjawatan vs Pegangan KJC', + $data, + '/v1/dashboard/kjc/drill-down/entitlement-vs-holdings', + ['type'] + ); + } + + /** + * Get KJC holdings status breakdown pie chart + */ + public function getHoldingsStatusBreakdownPieChart(array $filters = [], $user = null): array + { + $statusCounts = $this->kjcDashboardRepository->getHoldingsByStatus($filters, $user); + + $statusNames = [ + 'BP' => 'BP', + 'BDG' => 'BDG', + 'TBDG' => 'TBDG', + 'BT' => 'BT', + 'TBT' => 'TBT', + 'BG' => 'BG', + 'TBG' => 'TBG', + ]; + + $data = []; + foreach ($statusCounts as $item) { + $statusCode = $item['status']; + if ($statusCode === null || $statusCode === '') { + $statusName = 'Belum Diberi Status'; + } else { + $statusName = $statusNames[$statusCode] ?? $statusCode; + } + $data[] = [ + 'name' => $statusName, + 'value' => $item['count'], + 'percentage' => 0, + 'status_code' => $statusCode, + ]; + } + + // Calculate percentages + $total = array_sum(array_column($data, 'value')); + foreach ($data as &$row) { + $row['percentage'] = $total > 0 ? round(($row['value'] / $total) * 100, 2) : 0; + } + + return $this->formatPieChartData( + 'KJC Status Pegangan', + $data, + '/v1/dashboard/kjc/drill-down/holdings-status', + ['status'] + ); + } + + /** + * Get KJC unit performance chart + */ + public function getUnitPerformanceChart(array $filters = []): array + { + $units = Unit::withCount([ + 'kjcAssetHoldings as bp_count' => function ($query) use ($filters) { + $query->where('status', 'BP'); + $this->applyCommonFilters($query, $filters); + }, + 'kjcAssetHoldings as bdg_count' => function ($query) use ($filters) { + $query->where('status', 'BDG'); + $this->applyCommonFilters($query, $filters); + }, + 'kjcAssetHoldings as tbdg_count' => function ($query) use ($filters) { + $query->where('status', 'TBDG'); + $this->applyCommonFilters($query, $filters); + } + ])->get(); + + $unitNames = []; + $bpCounts = []; + $bdgCounts = []; + $tbdgCounts = []; + + foreach ($units as $unit) { + $unitNames[] = $unit->name; + $bpCounts[] = $unit->bp_count; + $bdgCounts[] = $unit->bdg_count; + $tbdgCounts[] = $unit->tbdg_count; + } + + return $this->formatBarChartData( + 'KJC Unit 2025', + [ + ['name' => 'Baik dalam Perhatian (BP)', 'data' => $bpCounts], + ['name' => 'Boleh Digunakan (BDG)', 'data' => $bdgCounts], + ['name' => 'Tidak Boleh Digunakan (TBDG)', 'data' => $tbdgCounts] + ], + $unitNames, + '/v1/dashboard/kjc/drill-down/unit-performance', + ['unit_id'] + ); + } + + /** + * Get KJC monthly trends chart + */ + public function getMonthlyTrendsChart(array $filters = [], $user = null): array + { + $dateRange = $this->getDateRange($filters); + $categories = $this->getMonthlyCategories($filters); + + $monthlyData = $this->kjcDashboardRepository->getMonthlyTrends($filters, $user); + + // Initialize data arrays + $bpData = array_fill(0, count($categories), 0); + $bdgData = array_fill(0, count($categories), 0); + $tbdgData = array_fill(0, count($categories), 0); + + // Fill data arrays + foreach ($monthlyData as $item) { + $index = array_search($item['month'], $categories); + if ($index !== false) { + switch ($item['status']) { + case 'BP': + $bpData[$index] = $item['count']; + break; + case 'BDG': + $bdgData[$index] = $item['count']; + break; + case 'TBDG': + $tbdgData[$index] = $item['count']; + break; + } + } + } + + return $this->formatLineChartData( + 'KJC Trend Bulanan 2025', + [ + ['name' => 'Baik dalam Perhatian (BP)', 'data' => $bpData], + ['name' => 'Boleh Digunakan (BDG)', 'data' => $bdgData], + ['name' => 'Tidak Boleh Digunakan (TBDG)', 'data' => $tbdgData] + ], + $categories, + '/v1/dashboard/kjc/drill-down/monthly-trends', + ['month', 'status'] + ); + } + + /** + * Get KJC historical metrics bar chart (KEUPAYAAN, KESIAGAAN, SERVISIBILITI) + */ + public function getHistoricalMetricsBarChart(array $filters = [], $user = null): array + { + $monthlyData = $this->kjcDashboardRepository->getHistoricalMetricsByMonth($filters, $user); + + // Extract data for chart + $categories = []; + $keupayaanData = []; + $kesiagaanData = []; + $servisibilitiData = []; + + foreach ($monthlyData as $data) { + $monthDate = Carbon::createFromFormat('Y-m', $data['month']); + $categories[] = $monthDate->format('M'); // Remove year from categories + $keupayaanData[] = $data['keupayaan']; + $kesiagaanData[] = $data['kesiagaan']; + $servisibilitiData[] = $data['servisibiliti']; + } + + // Use year from user selection (date_from/date_to) or fall back to current year + $year = Carbon::now()->year; + if (!empty($filters['date_from'])) { + $year = Carbon::parse($filters['date_from'])->year; + } elseif (!empty($filters['date_to'])) { + $year = Carbon::parse($filters['date_to'])->year; + } + + return $this->formatBarChartData( + "Metrik Tahunan KJC {$year}", + [ + ['name' => 'KEUPAYAAN (%)', 'data' => $keupayaanData], + ['name' => 'KESIAGAAN (%)', 'data' => $kesiagaanData], + ['name' => 'SERVISIBILITI (%)', 'data' => $servisibilitiData] + ], + $categories + ); + } + + /** + * Get unit ID column name for KJC + */ + protected function getUnitIdColumn(): string + { + return 'kjc_asset_holdings.unit_id'; + } + + /** + * Get asset model class for KJC + */ + protected function getAssetModelClass(): string + { + return KJCAssetHolding::class; + } + + /** + * Get repair model class for KJC + */ + protected function getRepairModelClass(): string + { + return KJCRepair::class; + } +} \ No newline at end of file diff --git a/be/Modules/Dashboard/Services/PKJ/PKJDashboardService.php b/be/Modules/Dashboard/Services/PKJ/PKJDashboardService.php new file mode 100644 index 0000000..af5d006 --- /dev/null +++ b/be/Modules/Dashboard/Services/PKJ/PKJDashboardService.php @@ -0,0 +1,319 @@ +pkjDashboardRepository = $pkjDashboardRepository; + } + /** + * Get total PKJ assets + */ + public function getTotalAssets(array $filters = [], $user = null): int + { + return $this->pkjDashboardRepository->getTotalHoldings($filters, $user); + } + + /** + * Get operational PKJ assets (BP status) + */ + public function getOperationalAssets(array $filters = [], $user = null): int + { + $holdings = $this->pkjDashboardRepository->getHoldingsBySpecificStatus('BP', $filters, $user); + return $holdings['pagination']['total']; + } + + /** + * Get under repair PKJ assets (TBDG status) + */ + public function getUnderRepairAssets(array $filters = [], $user = null): int + { + $holdings = $this->pkjDashboardRepository->getHoldingsBySpecificStatus('TBDG', $filters, $user); + return $holdings['pagination']['total']; + } + + /** + * Get maintenance due PKJ assets (BDG status) + */ + public function getMaintenanceDueAssets(array $filters = [], $user = null): int + { + $holdings = $this->pkjDashboardRepository->getHoldingsBySpecificStatus('BDG', $filters, $user); + return $holdings['pagination']['total']; + } + + /** + * Get active PKJ repairs + */ + public function getActiveRepairs(array $filters = []): int + { + $dateRange = $this->getDateRange($filters); + + return PKJRepair::query() + ->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]) + ->where('status', 'active') + ->count(); + } + + /** + * Get pending PKJ reports + */ + public function getPendingReports(array $filters = []): int + { + $dateRange = $this->getDateRange($filters); + + return PKJReport::query() + ->whereBetween('created_at', [$dateRange['from'], $dateRange['to']]) + ->where('status', 'pending') + ->count(); + } + + /** + * Get PKJ entitlement vs holdings pie chart + */ + public function getEntitlementVsHoldingsPieChart(array $filters = [], $user = null): array + { + $totalEntitlement = $this->pkjDashboardRepository->getTotalEntitlement($filters, $user); + $totalHoldings = $this->pkjDashboardRepository->getTotalHoldings($filters, $user); + + $data = [ + [ + 'name' => 'Perjawatan', + 'value' => $totalEntitlement, + 'percentage' => ($totalEntitlement + $totalHoldings) > 0 ? round(($totalEntitlement / ($totalEntitlement + $totalHoldings)) * 100, 2) : 0 + ], + [ + 'name' => 'Pegangan', + 'value' => $totalHoldings, + 'percentage' => ($totalEntitlement + $totalHoldings) > 0 ? round(($totalHoldings / ($totalEntitlement + $totalHoldings)) * 100, 2) : 0 + ] + ]; + + return $this->formatPieChartData( + 'Perjawatan vs Pegangan PKJ', + $data, + '', // No drill-down + [] + ); + } + + /** + * Get PKJ holdings status breakdown pie chart + */ + public function getHoldingsStatusBreakdownPieChart(array $filters = [], $user = null): array + { + $statusCounts = $this->pkjDashboardRepository->getHoldingsByStatus($filters, $user); + + // Map status codes to readable names + $statusNames = [ + 'BP' => 'Baik dalam Perhatian', + 'BDG' => 'Boleh Digunakan', + 'TBDG' => 'Tidak Boleh Digunakan', + 'BT' => 'Boleh Tembak', + 'BG' => 'Baik Guna' + ]; + + $data = []; + foreach ($statusCounts as $status) { + $statusName = $statusNames[$status['status']] ?? $status['status']; + $data[] = [ + 'name' => $statusName, + 'value' => $status['count'], + 'percentage' => 0 // Will be calculated after total + ]; + } + + // Calculate percentages + $total = array_sum(array_column($data, 'value')); + foreach ($data as &$item) { + $item['percentage'] = $total > 0 ? round(($item['value'] / $total) * 100, 2) : 0; + } + + return $this->formatPieChartData( + 'PKJ Status Pegangan', + $data, + '', // No drill-down + [] + ); + } + + /** + * Get PKJ monthly trends chart + */ + public function getMonthlyTrendsChart(array $filters = [], $user = null): array + { + $monthlyData = $this->pkjDashboardRepository->getMonthlyTrends($filters, $user); + + $categories = []; + $data = []; + + foreach ($monthlyData as $trend) { + $categories[] = Carbon::createFromFormat('Y-m', $trend['month'])->format('M Y'); + $data[] = $trend['total_reports']; + } + + return $this->formatBarChartData( + 'Trend Bulanan PKJ 2025', + [ + ['name' => 'Laporan', 'data' => $data] + ], + $categories + ); + } + + /** + * Get PKJ holdings by category bar chart + */ + public function getHoldingsByCategoryBarChart(array $filters = [], $user = null): array + { + $categoryData = $this->pkjDashboardRepository->getHoldingsByCategory($filters, $user); + + $categories = []; + $categoryIds = []; + $data = []; + + foreach ($categoryData as $category) { + $categories[] = $category['category_name']; + $categoryIds[] = (int) $category['category_id']; + $data[] = (int) $category['total_holding']; + } + + $chartData = $this->formatBarChartData( + 'Pegangan PKJ Mengikut Kategori', + [ + ['name' => 'Jumlah Pegangan', 'data' => $data] + ], + $categories + ); + + // Add category IDs to the chart data for drill-down functionality + $chartData['category_ids'] = $categoryIds; + + return $chartData; + } + + /** + * Get PKJ category metrics bar chart (drill-down) + */ + public function getCategoryMetricsBarChart(int $categoryId, array $filters = [], $user = null): array + { + $metrics = $this->pkjDashboardRepository->getCategoryMetrics($categoryId, $filters, $user); + + $categories = ['KEUPAYAAN (%)', 'KESIAGAAN (%)', 'SERVISIBILITI (%)']; + $data = [ + $metrics['keupayaan'], + $metrics['kesiagaan'], + $metrics['servisibiliti'] + ]; + + $chartData = $this->formatBarChartData( + 'Metrik Kategori PKJ', + [ + ['name' => 'Metrics', 'data' => $data] + ], + $categories + ); + + // Add category information to the response + $chartData['category_id'] = $metrics['category_id']; + $chartData['category_name'] = $metrics['category_name']; + + return $chartData; + } + + /** + * Get PKJ category status breakdown pie chart (drill-down) + */ + public function getCategoryStatusBreakdownPieChart(int $categoryId, array $filters = [], $user = null): array + { + $breakdown = $this->pkjDashboardRepository->getCategoryStatusBreakdown($categoryId, $filters, $user); + + // Map status codes to readable names + $statusNames = [ + 'BP' => 'Baik dalam Perhatian', + 'BDG' => 'Boleh Digunakan', + 'TBDG' => 'Tidak Boleh Digunakan', + 'BT' => 'Boleh Tembak', + 'TBT' => 'Tidak Boleh Tembak', + 'BG' => 'Baik Guna', + 'TBG' => 'Tidak Baik Guna', + ]; + + $data = []; + foreach ($breakdown['status_breakdown'] as $status) { + $statusCode = $status['status']; + + // In the breakdown, status_code is null when the asset has no status (DB column is NULL). + if ($statusCode === null || $statusCode === '') { + $statusName = 'Status Tidak Diketahui'; + } else { + $statusName = $statusNames[$statusCode] ?? $statusCode; + } + + $data[] = [ + 'name' => $statusName, + 'value' => $status['count'], + 'percentage' => 0, // Will be calculated after total + 'status_code' => $statusCode // null when DB status column is NULL (Belum Diberi Status) + ]; + } + + // Calculate percentages + $total = array_sum(array_column($data, 'value')); + foreach ($data as &$item) { + $item['percentage'] = $total > 0 ? round(($item['value'] / $total) * 100, 2) : 0; + } + + $chartData = $this->formatPieChartData( + 'PKJ Status Pegangan Mengikut Kategori', + $data, + '/v1/dashboard/pkj/category/' . $categoryId . '/status-details', + ['status'] + ); + + // Add category information to the response + $chartData['category_id'] = $breakdown['category_id']; + $chartData['category_name'] = $breakdown['category_name']; + + return $chartData; + } + + /** + * Get unit ID column name for PKJ + */ + protected function getUnitIdColumn(): string + { + return 'pkj_asset_holdings.unit_id'; + } + + /** + * Get asset model class for PKJ + */ + protected function getAssetModelClass(): string + { + return PKJAssetHolding::class; + } + + /** + * Get repair model class for PKJ + */ + protected function getRepairModelClass(): string + { + return PKJRepair::class; + } +} diff --git a/be/Modules/Dashboard/Tests/Feature/.gitkeep b/be/Modules/Dashboard/Tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Tests/Unit/.gitkeep b/be/Modules/Dashboard/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/Transformers/.gitkeep b/be/Modules/Dashboard/Transformers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Dashboard/composer.json b/be/Modules/Dashboard/composer.json new file mode 100644 index 0000000..b53db27 --- /dev/null +++ b/be/Modules/Dashboard/composer.json @@ -0,0 +1,28 @@ +{ + "name": "nwidart/dashboard", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": {} + } + }, + "autoload": { + "psr-4": { + "Modules\\Dashboard\\": "App", + "Modules\\Dashboard\\Database\\Factories\\": "database/factories/", + "Modules\\Dashboard\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\Dashboard\\Tests\\": "tests/" + } + } +} \ No newline at end of file diff --git a/be/Modules/Dashboard/module.json b/be/Modules/Dashboard/module.json new file mode 100644 index 0000000..aeeead2 --- /dev/null +++ b/be/Modules/Dashboard/module.json @@ -0,0 +1,11 @@ +{ + "name": "Dashboard", + "alias": "dashboard", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\Dashboard\\Providers\\DashboardServiceProvider" + ], + "files": [] +} \ No newline at end of file diff --git a/be/Modules/Dashboard/package.json b/be/Modules/Dashboard/package.json new file mode 100644 index 0000000..d6fbfc8 --- /dev/null +++ b/be/Modules/Dashboard/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.1.2", + "laravel-vite-plugin": "^0.7.5", + "sass": "^1.69.5", + "postcss": "^8.3.7", + "vite": "^4.0.0" + } +} diff --git a/be/Modules/Notification/Actions/.gitkeep b/be/Modules/Notification/Actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Config/.gitkeep b/be/Modules/Notification/Config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Config/config.php b/be/Modules/Notification/Config/config.php new file mode 100644 index 0000000..fa3483f --- /dev/null +++ b/be/Modules/Notification/Config/config.php @@ -0,0 +1,5 @@ + 'Notification', +]; diff --git a/be/Modules/Notification/Console/.gitkeep b/be/Modules/Notification/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Database/Factories/.gitkeep b/be/Modules/Notification/Database/Factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Database/Migrations/.gitkeep b/be/Modules/Notification/Database/Migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Database/Seeders/.gitkeep b/be/Modules/Notification/Database/Seeders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Database/Seeders/NotificationDatabaseSeeder.php b/be/Modules/Notification/Database/Seeders/NotificationDatabaseSeeder.php new file mode 100644 index 0000000..ab8dc40 --- /dev/null +++ b/be/Modules/Notification/Database/Seeders/NotificationDatabaseSeeder.php @@ -0,0 +1,16 @@ +call([]); + } +} diff --git a/be/Modules/Notification/Emails/.gitkeep b/be/Modules/Notification/Emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Entities/.gitkeep b/be/Modules/Notification/Entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Entities/Notification.php b/be/Modules/Notification/Entities/Notification.php new file mode 100644 index 0000000..e8c17de --- /dev/null +++ b/be/Modules/Notification/Entities/Notification.php @@ -0,0 +1,21 @@ + 'datetime', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } +} \ No newline at end of file diff --git a/be/Modules/Notification/Helpers/.gitkeep b/be/Modules/Notification/Helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Http/Controllers/.gitkeep b/be/Modules/Notification/Http/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Http/Controllers/NotificationController.php b/be/Modules/Notification/Http/Controllers/NotificationController.php new file mode 100644 index 0000000..1f1d209 --- /dev/null +++ b/be/Modules/Notification/Http/Controllers/NotificationController.php @@ -0,0 +1,469 @@ +get('per_page', 10); + $page = $request->get('page', 1); + $filter = $request->get('filter', 'all'); // all, unread, read + + $query = $user->notifications(); + + // Apply filter + switch ($filter) { + case 'unread': + $query->whereNull('read_at'); + break; + case 'read': + $query->whereNotNull('read_at'); + break; + default: + // Show all notifications + break; + } + + $notifications = $query->orderBy('created_at', 'desc') + ->paginate($perPage, ['*'], 'page', $page); + + // Transform notifications to match frontend format + $transformedNotifications = $notifications->map(function ($notification) { + $data = $notification->data; + + // Detect notification category (for internal categorization) + $notificationCategory = $this->detectNotificationCategory($data); + + // Get display type (action type for KJC Repair, or original type for others) + $displayType = $this->getDisplayType($data, $notificationCategory); + + return [ + 'id' => $notification->id, + 'type' => $displayType, + 'title' => $this->getNotificationTitle($data, $notificationCategory), + 'message' => $this->getNotificationMessage($data, $notificationCategory), + 'is_read' => $notification->read_at !== null, + 'created_at' => $notification->created_at->format('Y-m-d H:i:s'), + 'time_ago' => $notification->created_at->diffForHumans(), + 'icon' => $this->getNotificationIcon($notificationCategory), + 'color' => $this->getNotificationColor($notificationCategory), + 'navigation' => $this->getNotificationNavigation($data, $notificationCategory), + 'data' => $data + ]; + }); + + return response()->json([ + 'success' => true, + 'data' => $transformedNotifications, + 'pagination' => [ + 'current_page' => $notifications->currentPage(), + 'last_page' => $notifications->lastPage(), + 'per_page' => $notifications->perPage(), + 'total' => $notifications->total(), + 'has_more' => $notifications->hasMorePages() + ], + 'unread_count' => $user->unreadNotifications()->count() + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch notifications: ' . $e->getMessage() + ], 500); + } + } + + /** + * Mark notification as read + */ + public function markAsRead($id): JsonResponse + { + try { + $user = Auth::user(); + $notification = $user->notifications()->find($id); + + if (!$notification) { + return response()->json([ + 'success' => false, + 'message' => 'Notifikasi tidak ditemukan' + ], 404); + } + + $notification->markAsRead(); + + return response()->json([ + 'success' => true, + 'message' => 'Notifikasi ditandakan sebagai telah dibaca' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal menandakan notifikasi sebagai telah dibaca: ' . $e->getMessage() + ], 500); + } + } + + /** + * Mark all notifications as read + */ + public function markAllAsRead(): JsonResponse + { + try { + $user = Auth::user(); + $user->unreadNotifications->markAsRead(); + + return response()->json([ + 'success' => true, + 'message' => 'Semua notifikasi ditandakan sebagai telah dibaca' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal menandakan semua notifikasi sebagai telah dibaca: ' . $e->getMessage() + ], 500); + } + } + + /** + * Get notification count + */ + public function getCount(): JsonResponse + { + try { + $user = Auth::user(); + $unreadCount = $user->unreadNotifications()->count(); + + return response()->json([ + 'success' => true, + 'unread_count' => $unreadCount + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Gagal mendapatkan bilangan notifikasi: ' . $e->getMessage() + ], 500); + } + } + + /** + * Detect notification category from data (for internal categorization) + */ + private function detectNotificationCategory($data): string + { + // Check if it's a KJC Repair notification + if (isset($data['kjc_repair_id'])) { + return 'kjc_repair'; + } + + // Check if it's a KJC Temporary Loan notification + if (isset($data['kjc_temporary_loan_id'])) { + return 'kjc_temporary_loan'; + } + + // Check if it's a User Activation notification + if (isset($data['user_id']) && isset($data['type']) && $data['type'] === 'user_activation_required') { + return 'user_activation'; + } + + // Check if it's a Feedback notification + if (isset($data['feedback_id'])) { + return 'feedback'; + } + + // Check for other notification types + if (isset($data['type'])) { + return $data['type']; + } + + return 'general'; + } + + /** + * Get display type for frontend (action type for KJC Repair/Temporary Loan, original type for others) + */ + private function getDisplayType($data, $category): string + { + // For KJC Repair and Temporary Loan notifications, return the action type (next_approval, approved, etc.) + if (($category === 'kjc_repair' || $category === 'kjc_temporary_loan') && isset($data['type'])) { + return $data['type']; + } + + // For User Activation notifications, return the type + if ($category === 'user_activation' && isset($data['type'])) { + return $data['type']; + } + + // For Feedback notifications, return the type + if ($category === 'feedback' && isset($data['type'])) { + return $data['type']; + } + + // For other notifications, return the category + return $category; + } + + /** + * Get notification title based on category and data + */ + private function getNotificationTitle($data, $notificationCategory): string + { + if ($notificationCategory === 'kjc_repair') { + $repairType = $data['type'] ?? 'general'; + switch ($repairType) { + case 'next_approval': + return 'Pembaikan KJC - Permohonan Disemak'; + case 'approved': + return 'Pembaikan KJC - Disetujui'; + case 'rejected': + return 'Pembaikan KJC - Ditolak'; + case 'returned_for_revision': + return 'Pembaikan KJC - Dikembalikan untuk semakan semula'; + default: + return 'Notifikasi Pembaikan KJC'; + } + } + + if ($notificationCategory === 'kjc_temporary_loan') { + $loanType = $data['type'] ?? 'general'; + switch ($loanType) { + case 'next_approval': + return 'Pinjaman Sementara KJC - Permohonan Disemak'; + case 'approved': + return 'Pinjaman Sementara KJC - Disetujui'; + case 'rejected': + return 'Pinjaman Sementara KJC - Ditolak'; + case 'returned_for_revision': + return 'Pinjaman Sementara KJC - Dikembalikan untuk semakan semula'; + default: + return 'Notifikasi Pinjaman Sementara KJC'; + } + } + + if ($notificationCategory === 'user_activation') { + return 'Pengaktifan Pengguna Diperlukan'; + } + + if ($notificationCategory === 'feedback') { + return 'Maklum Balas Baru Diterima'; + } + + switch ($notificationCategory) { + case 'report_generation': + return 'Penyata Mingguan Dicipta'; + case 'system': + return 'Notifikasi Sistem'; + default: + return 'Notifikasi'; + } + } + + /** + * Get notification message based on category and data + */ + private function getNotificationMessage($data, $notificationCategory): string + { + // If message is already set, use it + if (!empty($data['message'])) { + return $data['message']; + } + + // Generate message for KJC Repair notifications + if ($notificationCategory === 'kjc_repair') { + $repairType = $data['type'] ?? 'general'; + $repairId = $data['kjc_repair_id'] ?? 'N/A'; + $assetName = $data['asset_name'] ?? 'Unknown'; + $unitName = $data['unit_name'] ?? 'Unknown'; + + switch ($repairType) { + case 'next_approval': + return "Permohonan pembaikan KJC perlu disemak oleh anda. ID Permohonan: {$repairId}, Asset: {$assetName}, Unit: {$unitName}"; + case 'approved': + return "Permohonan pembaikan KJC telah disetujui. ID Permohonan: {$repairId}, Asset: {$assetName}, Unit: {$unitName}"; + case 'rejected': + return "Permohonan pembaikan KJC telah ditolak. ID Permohonan: {$repairId}, Asset: {$assetName}, Unit: {$unitName}"; + case 'returned_for_revision': + return "Permohonan pembaikan KJC telah dikembalikan untuk semakan semula. ID Permohonan: {$repairId}, Asset: {$assetName}, Unit: {$unitName}"; + default: + return "Notifikasi berkaitan permohonan pembaikan KJC. ID Permohonan: {$repairId}"; + } + } + + // Generate message for KJC Temporary Loan notifications + if ($notificationCategory === 'kjc_temporary_loan') { + $loanType = $data['type'] ?? 'general'; + $assetName = $data['asset_name'] ?? 'Unknown'; + $originalUnitName = $data['original_unit_name'] ?? 'Unknown'; + $borrowerUnitName = $data['borrower_unit_name'] ?? 'Unknown'; + + switch ($loanType) { + case 'next_approval': + return "Permohonan pinjaman sementara KJC perlu disemak oleh anda, Asset: {$assetName}, Pasukan Asal: {$originalUnitName}, Pasukan Peminjam: {$borrowerUnitName}"; + case 'approved': + return "Permohonan pinjaman sementara KJC telah disetujui, Asset: {$assetName}, Pasukan Asal: {$originalUnitName}, Pasukan Peminjam: {$borrowerUnitName}"; + case 'rejected': + return "Permohonan pinjaman sementara KJC telah ditolak, Asset: {$assetName}, Pasukan Asal: {$originalUnitName}, Pasukan Peminjam: {$borrowerUnitName}"; + case 'returned_for_revision': + return "Permohonan pinjaman sementara KJC telah dikembalikan untuk semakan semula, Asset: {$assetName}, Pasukan Asal: {$originalUnitName}, Pasukan Peminjam: {$borrowerUnitName}"; + default: + return "Notifikasi berkaitan permohonan pinjaman sementara KJC, Asset: {$assetName}, Pasukan Asal: {$originalUnitName}, Pasukan Peminjam: {$borrowerUnitName}"; + } + } + + // Generate message for User Activation notifications + if ($notificationCategory === 'user_activation') { + // If message is already set, use it + if (!empty($data['message'])) { + return $data['message']; + } + + $userName = $data['user_name'] ?? 'Unknown'; + $userEmail = $data['user_email'] ?? 'Unknown'; + $userArmyNumber = $data['user_army_number'] ?? 'Unknown'; + $unitName = $data['user_unit_name'] ?? 'Unknown'; + + return "Pengguna baru memerlukan pengaktifan. Nama: {$userName}, Email: {$userEmail}, No. Tentera: {$userArmyNumber}, Unit: {$unitName}"; + } + + // Generate message for Feedback notifications + if ($notificationCategory === 'feedback') { + // If message is already set, use it + if (!empty($data['message'])) { + return $data['message']; + } + + $feedbackTitle = $data['feedback_title'] ?? 'Unknown'; + $feedbackType = $data['feedback_type'] ?? 'Unknown'; + $feedbackPriority = $data['feedback_priority'] ?? 'normal'; + $userName = $data['user_name'] ?? 'Anonymous'; + $feedbackId = $data['feedback_id'] ?? 'N/A'; + + return "Maklum balas baru telah diterima. ID: {$feedbackId}, Tajuk: {$feedbackTitle}, Jenis: {$feedbackType}, Keutamaan: {$feedbackPriority}, Pengguna: {$userName}"; + } + + return 'No message available'; + } + + /** + * Get notification icon based on type + */ + private function getNotificationIcon($type): string + { + switch ($type) { + case 'report_generation': + return 'mdi-file-document'; + case 'kjc_repair': + return 'mdi-tools'; + case 'kjc_temporary_loan': + return 'mdi-handshake'; + case 'user_activation': + case 'user_activation_required': + return 'mdi-account-plus'; + case 'feedback': + case 'feedback_submitted': + return 'mdi-message-alert'; + case 'system': + return 'mdi-cog'; + default: + return 'mdi-bell'; + } + } + + /** + * Get notification color based on type + */ + private function getNotificationColor($type): string + { + switch ($type) { + case 'report_generation': + return 'success'; + case 'kjc_repair': + return 'warning'; + case 'kjc_temporary_loan': + return 'info'; + case 'user_activation': + case 'user_activation_required': + return 'purple'; + case 'feedback': + case 'feedback_submitted': + return 'orange'; + case 'system': + return 'info'; + default: + return 'primary'; + } + } + + /** + * Get notification navigation data based on category + */ + private function getNotificationNavigation($data, $notificationCategory): array + { + if ($notificationCategory === 'kjc_repair') { + $repairId = $data['kjc_repair_id'] ?? null; + return [ + 'route' => '/kjcrepairs', + 'params' => [], + 'query' => $repairId ? ['id' => $repairId] : [] + ]; + } + + if ($notificationCategory === 'kjc_temporary_loan') { + $loanId = $data['kjc_temporary_loan_id'] ?? null; + return [ + 'route' => '/kjctemporaryloans', + 'params' => [], + 'query' => $loanId ? ['id' => $loanId] : [] + ]; + } + + if ($notificationCategory === 'user_activation') { + $userId = $data['user_id'] ?? null; + return [ + 'route' => '/users', + 'params' => [], + 'query' => $userId ? ['id' => $userId] : [] + ]; + } + + if ($notificationCategory === 'feedback') { + $feedbackId = $data['feedback_id'] ?? null; + return [ + 'route' => '/feedback', + 'params' => [], + 'query' => $feedbackId ? ['id' => $feedbackId] : [] + ]; + } + + switch ($notificationCategory) { + case 'report_generation': + return [ + 'route' => '/kjcreports', + 'params' => [], + 'query' => [] + ]; + case 'system': + return [ + 'route' => '/dashboard', + 'params' => [], + 'query' => [] + ]; + default: + return [ + 'route' => '/dashboard', + 'params' => [], + 'query' => [] + ]; + } + } +} diff --git a/be/Modules/Notification/Http/Requests/.gitkeep b/be/Modules/Notification/Http/Requests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Jobs/.gitkeep b/be/Modules/Notification/Jobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Notifications/.gitkeep b/be/Modules/Notification/Notifications/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Policies/.gitkeep b/be/Modules/Notification/Policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Providers/.gitkeep b/be/Modules/Notification/Providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Providers/EventServiceProvider.php b/be/Modules/Notification/Providers/EventServiceProvider.php new file mode 100644 index 0000000..c8a3d10 --- /dev/null +++ b/be/Modules/Notification/Providers/EventServiceProvider.php @@ -0,0 +1,27 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/Modules/Notification/Providers/NotificationServiceProvider.php b/be/Modules/Notification/Providers/NotificationServiceProvider.php new file mode 100644 index 0000000..9c14bd8 --- /dev/null +++ b/be/Modules/Notification/Providers/NotificationServiceProvider.php @@ -0,0 +1,154 @@ +registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations')); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, 'Lang')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, 'Resources/Views'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } +} diff --git a/be/Modules/Notification/Providers/RouteServiceProvider.php b/be/Modules/Notification/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..c826a75 --- /dev/null +++ b/be/Modules/Notification/Providers/RouteServiceProvider.php @@ -0,0 +1,50 @@ +mapApiRoutes(); + $this->mapWebRoutes(); + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + */ + protected function mapWebRoutes(): void + { + Route::middleware('web')->group(module_path($this->name, '/Routes/web.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + */ + protected function mapApiRoutes(): void + { + Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php')); + } +} diff --git a/be/Modules/Notification/Repositories/.gitkeep b/be/Modules/Notification/Repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Repositories/Contracts/.gitkeep b/be/Modules/Notification/Repositories/Contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Routes/.gitkeep b/be/Modules/Notification/Routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Routes/api.php b/be/Modules/Notification/Routes/api.php new file mode 100644 index 0000000..81be54b --- /dev/null +++ b/be/Modules/Notification/Routes/api.php @@ -0,0 +1,11 @@ +prefix('v1')->group(function () { + Route::get('notifications', [NotificationController::class, 'index'])->name('notifications.index'); + Route::patch('notifications/{id}/read', [NotificationController::class, 'markAsRead'])->name('notifications.markAsRead'); + Route::patch('notifications/mark-all-read', [NotificationController::class, 'markAllAsRead'])->name('notifications.markAllAsRead'); + Route::get('notifications/count', [NotificationController::class, 'getCount'])->name('notifications.count'); +}); diff --git a/be/Modules/Notification/Routes/web.php b/be/Modules/Notification/Routes/web.php new file mode 100644 index 0000000..4d1a0d2 --- /dev/null +++ b/be/Modules/Notification/Routes/web.php @@ -0,0 +1,8 @@ +group(function () { + Route::resource('notifications', NotificationController::class)->names('notification'); +}); diff --git a/be/Modules/Notification/Services/.gitkeep b/be/Modules/Notification/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Tests/Feature/.gitkeep b/be/Modules/Notification/Tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Tests/Unit/.gitkeep b/be/Modules/Notification/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/Transformers/.gitkeep b/be/Modules/Notification/Transformers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Notification/composer.json b/be/Modules/Notification/composer.json new file mode 100644 index 0000000..fc2c8df --- /dev/null +++ b/be/Modules/Notification/composer.json @@ -0,0 +1,30 @@ +{ + "name": "nwidart/notification", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\Notification\\": "App", + "Modules\\Notification\\Database\\Factories\\": "database/factories/", + "Modules\\Notification\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\Notification\\Tests\\": "tests/" + } + } +} diff --git a/be/Modules/Notification/module.json b/be/Modules/Notification/module.json new file mode 100644 index 0000000..181e6e8 --- /dev/null +++ b/be/Modules/Notification/module.json @@ -0,0 +1,11 @@ +{ + "name": "Notification", + "alias": "notification", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\Notification\\Providers\\NotificationServiceProvider" + ], + "files": [] +} diff --git a/be/Modules/Notification/package.json b/be/Modules/Notification/package.json new file mode 100644 index 0000000..d6fbfc8 --- /dev/null +++ b/be/Modules/Notification/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.1.2", + "laravel-vite-plugin": "^0.7.5", + "sass": "^1.69.5", + "postcss": "^8.3.7", + "vite": "^4.0.0" + } +} diff --git a/be/Modules/Role/Actions/.gitkeep b/be/Modules/Role/Actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Config/.gitkeep b/be/Modules/Role/Config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Config/config.php b/be/Modules/Role/Config/config.php new file mode 100644 index 0000000..516600a --- /dev/null +++ b/be/Modules/Role/Config/config.php @@ -0,0 +1,5 @@ + 'Role', +]; diff --git a/be/Modules/Role/Console/.gitkeep b/be/Modules/Role/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Database/Factories/.gitkeep b/be/Modules/Role/Database/Factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Database/Migrations/.gitkeep b/be/Modules/Role/Database/Migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Database/Seeders/.gitkeep b/be/Modules/Role/Database/Seeders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Database/Seeders/RoleDatabaseSeeder.php b/be/Modules/Role/Database/Seeders/RoleDatabaseSeeder.php new file mode 100644 index 0000000..efe6856 --- /dev/null +++ b/be/Modules/Role/Database/Seeders/RoleDatabaseSeeder.php @@ -0,0 +1,16 @@ +call([]); + } +} diff --git a/be/Modules/Role/Emails/.gitkeep b/be/Modules/Role/Emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Entities/.gitkeep b/be/Modules/Role/Entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Entities/Role.php b/be/Modules/Role/Entities/Role.php new file mode 100644 index 0000000..c5aa1aa --- /dev/null +++ b/be/Modules/Role/Entities/Role.php @@ -0,0 +1,17 @@ +authorize('update', $this->modelClass); + + try { + $item = $this->repository->findById($id); + + if (! $item) { + return $this->errorResponse($this->getNotFoundMessage(), 404); + } + + $validated = $this->validateRequest($request); + $data = $this->prepareUpdateData($validated, $item); + + // Use repository update method to handle permissions + $updatedItem = $this->repository->update($id, $data); + + ActivityLogger::log("Updated {$this->resourceName}: {$this->getItemName($updatedItem)}", $updatedItem); + + return response()->json([ + 'success' => true, + 'data' => new $this->resourceClass($updatedItem), + 'message' => $this->getSuccessMessage('update'), + ]); + + } catch (Exception $e) { + Log::error("Error updating {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('update').': '.$e->getMessage(), 500); + } + } + + /** + * Check dependencies before deletion + */ + protected function checkDependencies($role): ?JsonResponse + { + // Check if role has users assigned + if ($role->users()->count() > 0) { + return response()->json([ + 'success' => false, + 'message' => 'Cannot delete role. It has users assigned to it.', + ], 422); + } + + return null; + } + + /** + * Get all permissions for role assignment + */ + public function permissions(): JsonResponse + { + $permissions = Permission::all(); + + return response()->json([ + 'success' => true, + 'data' => $permissions->map(function ($permission) { + return [ + 'id' => $permission->id, + 'name' => $permission->name, + 'guard_name' => $permission->guard_name, + 'route_name' => $permission->route_name, + ]; + }), + ]); + } + + /** + * Assign permissions to a role + */ + public function assignPermissions(Request $request, string $id): JsonResponse + { + $request->validate([ + 'permissions' => 'required|array', + 'permissions.*' => ['required', 'uuid', Rule::exists('permissions', 'id')], + ]); + + $role = $this->repository->find($id); + $this->repository->syncPermissions($role, $request->permissions); + + return response()->json([ + 'success' => true, + 'message' => 'Permissions assigned successfully.', + 'data' => new RoleResource($role->load('permissions')), + ]); + } + + /** + * Get role with permissions + */ + public function show(string $id): JsonResponse + { + $role = $this->repository->find($id); + + return response()->json([ + 'success' => true, + 'data' => new RoleResource($role->load('permissions')), + ]); + } + + /** + * Get all roles with permissions + */ + public function index(Request $request): JsonResponse + { + $search = $request->get('search', ''); + $roles = $this->repository->withPermissions($search); + + return response()->json([ + 'success' => true, + 'data' => RoleResource::collection($roles), + ]); + } + + /** + * Public endpoint for registration form (no auth required) + */ + public function publicRole(): JsonResponse + { + $roles = $this->repository->all(); + + return response()->json([ + 'success' => true, + 'data' => RoleResource::collection($roles), + ]); + } +} diff --git a/be/Modules/Role/Http/Requests/.gitkeep b/be/Modules/Role/Http/Requests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Http/Requests/RoleRequest.php b/be/Modules/Role/Http/Requests/RoleRequest.php new file mode 100644 index 0000000..3fc7f76 --- /dev/null +++ b/be/Modules/Role/Http/Requests/RoleRequest.php @@ -0,0 +1,101 @@ + [ + 'required', + 'string', + 'max:255' + ], + 'guard_name' => [ + 'required', + 'string', + 'in:api,web' + ], + 'permissions' => [ + 'sometimes', + 'array' + ], + 'permissions.*' => [ + 'required', + 'uuid', + Rule::exists('permissions', 'id'), + ], + 'fullname' => [ + 'required', + 'string', + 'max:255', + ], + 'context' => [ + 'required', + 'string', + Rule::in(['member', 'admin']), + ], + ]; + } + + /** + * Configure the validator instance. + */ + public function withValidator($validator) + { + $validator->after(function ($validator) { + $roleId = $this->route('role'); + $name = $this->input('name'); + $guardName = $this->input('guard_name', 'api'); + + // Check for unique name within the same guard + $query = Role::where('name', $name) + ->where('guard_name', $guardName); + + if ($roleId) { + $query->where('id', '!=', $roleId); + } + + if ($query->exists()) { + $validator->errors()->add('name', 'This role name already exists for the selected guard.'); + } + }); + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'name.required' => 'Role name is required.', + 'name.unique' => 'This role name already exists for the selected guard.', + 'guard_name.required' => 'Guard name is required.', + 'guard_name.in' => 'Guard name must be either api or web.', + 'permissions.array' => 'Permissions must be an array.', + 'permissions.*.uuid' => 'Each permission id must be a valid UUID.', + 'permissions.*.exists' => 'One or more selected permissions do not exist.', + 'fullname.required' => 'Fullname is required.', + 'fullname.string' => 'Fullname must be a string.', + 'fullname.max' => 'Fullname must be less than 255 characters.', + 'context.required' => 'Konteks peranan diperlukan.', + 'context.in' => 'Konteks peranan mestilah ahli atau pentadbir.', + ]; + } +} diff --git a/be/Modules/Role/Jobs/.gitkeep b/be/Modules/Role/Jobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Notifications/.gitkeep b/be/Modules/Role/Notifications/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Policies/.gitkeep b/be/Modules/Role/Policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Policies/RolePolicy.php b/be/Modules/Role/Policies/RolePolicy.php new file mode 100644 index 0000000..07a49df --- /dev/null +++ b/be/Modules/Role/Policies/RolePolicy.php @@ -0,0 +1,59 @@ +hasPermissionTo('lihat jenis pengguna'); + } + + /** + * Determine whether the user can view the model. + */ + public function view($user, ?Role $role = null): bool + { + return $user->hasPermissionTo('lihat jenis pengguna'); + } + + /** + * Determine whether the user can create models. + */ + public function create($user): bool + { + return $user->hasPermissionTo('tambah jenis pengguna'); + } + + /** + * Determine whether the user can update the model. + */ + public function update($user, ?Role $role = null): bool + { + return $user->hasPermissionTo('kemaskini jenis pengguna'); + } + + /** + * Determine whether the user can delete any model. + */ + public function deleteAny($user): bool + { + return $user->hasPermissionTo('hapus jenis pengguna'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete($user, ?Role $role = null): bool + { + return $user->hasPermissionTo('hapus jenis pengguna'); + } +} diff --git a/be/Modules/Role/Providers/.gitkeep b/be/Modules/Role/Providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Providers/EventServiceProvider.php b/be/Modules/Role/Providers/EventServiceProvider.php new file mode 100644 index 0000000..46dc56e --- /dev/null +++ b/be/Modules/Role/Providers/EventServiceProvider.php @@ -0,0 +1,27 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/Modules/Role/Providers/RoleServiceProvider.php b/be/Modules/Role/Providers/RoleServiceProvider.php new file mode 100644 index 0000000..6376bb6 --- /dev/null +++ b/be/Modules/Role/Providers/RoleServiceProvider.php @@ -0,0 +1,160 @@ +registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations')); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + + // Register repository binding + $this->app->bind( + \Modules\Role\Repositories\Contracts\RoleRepositoryInterface::class, + \Modules\Role\Repositories\RoleRepository::class + ); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, 'Lang')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, 'Resources/Views'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } +} diff --git a/be/Modules/Role/Providers/RouteServiceProvider.php b/be/Modules/Role/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..4f003a8 --- /dev/null +++ b/be/Modules/Role/Providers/RouteServiceProvider.php @@ -0,0 +1,50 @@ +mapApiRoutes(); + + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + */ + protected function mapWebRoutes(): void + { + Route::middleware('web')->group(module_path($this->name, '/Routes/web.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + */ + protected function mapApiRoutes(): void + { + Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php')); + } +} diff --git a/be/Modules/Role/Repositories/.gitkeep b/be/Modules/Role/Repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Repositories/Contracts/.gitkeep b/be/Modules/Role/Repositories/Contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Repositories/Contracts/RoleRepositoryInterface.php b/be/Modules/Role/Repositories/Contracts/RoleRepositoryInterface.php new file mode 100644 index 0000000..ad9741a --- /dev/null +++ b/be/Modules/Role/Repositories/Contracts/RoleRepositoryInterface.php @@ -0,0 +1,48 @@ +model = $model; + } + + /** + * Get all roles + */ + public function all() + { + return $this->model->where('name', '!=', 'DEVELOPER')->get(); + } + + /** + * Find role by ID + */ + public function find($id) + { + return $this->model->findOrFail($id); + } + + /** + * Find role by ID (alias for find) + */ + public function findById($id) + { + return $this->find($id); + } + + /** + * Create a new role + */ + public function create(array $data) + { + return DB::transaction(function () use ($data) { + $role = $this->model->create([ + 'name' => $data['name'], + 'guard_name' => $data['guard_name'] ?? 'api', + 'fullname' => $data['fullname'] ?? null, + 'context' => $data['context'] ?? 'member', + ]); + + if (isset($data['permissions']) && is_array($data['permissions'])) { + $this->syncPermissions($role, $data['permissions']); + } + + return $role->load('permissions'); + }); + } + + /** + * Update role + */ + public function update($id, array $data) + { + return DB::transaction(function () use ($id, $data) { + $role = $this->find($id); + + $role->update([ + 'name' => $data['name'], + 'guard_name' => $data['guard_name'] ?? $role->guard_name, + 'fullname' => $data['fullname'] ?? $role->fullname, + 'context' => $data['context'] ?? $role->context ?? 'member', + ]); + + if (isset($data['permissions']) && is_array($data['permissions'])) { + $this->syncPermissions($role, $data['permissions']); + } + + return $role->load('permissions'); + }); + } + + /** + * Delete role + */ + public function delete($id) + { + $role = $this->find($id); + return $role->delete(); + } + + /** + * Get roles with permissions + */ + public function withPermissions($search = '') + { + $query = $this->model->with('permissions') + ->where('name', '!=', 'DEVELOPER'); + + if (!empty($search)) { + $query->where(function ($q) use ($search) { + $q->where('name', 'ILIKE', "%{$search}%") + ->orWhere('fullname', 'ILIKE', "%{$search}%") + ->orWhere('guard_name', 'ILIKE', "%{$search}%"); + }); + } + + return $query->get(); + } + + /** + * Sync permissions for a role + */ + public function syncPermissions(Role $role, array $permissionIds) + { + $permissionIds = array_map(fn ($id) => (string) $id, $permissionIds); + + $permissions = Permission::whereIn('id', $permissionIds)->get(); + + if ($permissions->count() !== count($permissionIds)) { + throw new \InvalidArgumentException('One or more selected permissions do not exist.'); + } + + $role->syncPermissions($permissions); + + return $role; + } +} diff --git a/be/Modules/Role/Routes/.gitkeep b/be/Modules/Role/Routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Routes/api.php b/be/Modules/Role/Routes/api.php new file mode 100644 index 0000000..d75036b --- /dev/null +++ b/be/Modules/Role/Routes/api.php @@ -0,0 +1,18 @@ +group(function () { + Route::get('public/roles', [RoleController::class, 'publicRole']); +}); + +// Protected routes (auth required) +Route::middleware(['auth:sanctum'])->prefix('v1')->group(function () { + Route::apiResource('roles', RoleController::class)->names('role'); + + // Permission management routes + Route::get('permissions', [RoleController::class, 'permissions'])->name('permissions.index'); + Route::post('roles/{role}/permissions', [RoleController::class, 'assignPermissions'])->name('roles.permissions.assign'); +}); \ No newline at end of file diff --git a/be/Modules/Role/Services/.gitkeep b/be/Modules/Role/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Tests/Feature/.gitkeep b/be/Modules/Role/Tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Tests/Unit/.gitkeep b/be/Modules/Role/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Transformers/.gitkeep b/be/Modules/Role/Transformers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/Role/Transformers/RoleResource.php b/be/Modules/Role/Transformers/RoleResource.php new file mode 100644 index 0000000..e920b01 --- /dev/null +++ b/be/Modules/Role/Transformers/RoleResource.php @@ -0,0 +1,34 @@ + $this->id, + 'name' => $this->name, + 'guard_name' => $this->guard_name, + 'fullname' => $this->fullname, + 'context' => $this->context ?? 'member', + 'permissions' => $this->whenLoaded('permissions', function () { + return $this->permissions->map(function ($permission) { + return [ + 'id' => $permission->id, + 'name' => $permission->name, + 'guard_name' => $permission->guard_name, + 'route_name' => $permission->route_name, + ]; + }); + }), + 'created_at' => $this->created_at?->toISOString(), + 'updated_at' => $this->updated_at?->toISOString(), + ]; + } +} diff --git a/be/Modules/Role/composer.json b/be/Modules/Role/composer.json new file mode 100644 index 0000000..8701f4d --- /dev/null +++ b/be/Modules/Role/composer.json @@ -0,0 +1,30 @@ +{ + "name": "nwidart/role", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\Role\\": "App", + "Modules\\Role\\Database\\Factories\\": "database/factories/", + "Modules\\Role\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\Role\\Tests\\": "tests/" + } + } +} diff --git a/be/Modules/Role/module.json b/be/Modules/Role/module.json new file mode 100644 index 0000000..58e1436 --- /dev/null +++ b/be/Modules/Role/module.json @@ -0,0 +1,11 @@ +{ + "name": "Role", + "alias": "role", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\Role\\Providers\\RoleServiceProvider" + ], + "files": [] +} diff --git a/be/Modules/Role/package.json b/be/Modules/Role/package.json new file mode 100644 index 0000000..d6fbfc8 --- /dev/null +++ b/be/Modules/Role/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.1.2", + "laravel-vite-plugin": "^0.7.5", + "sass": "^1.69.5", + "postcss": "^8.3.7", + "vite": "^4.0.0" + } +} diff --git a/be/Modules/User/Actions/.gitkeep b/be/Modules/User/Actions/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Config/.gitkeep b/be/Modules/User/Config/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Config/config.php b/be/Modules/User/Config/config.php new file mode 100644 index 0000000..566de5a --- /dev/null +++ b/be/Modules/User/Config/config.php @@ -0,0 +1,5 @@ + 'User', +]; diff --git a/be/Modules/User/Console/.gitkeep b/be/Modules/User/Console/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Database/Factories/.gitkeep b/be/Modules/User/Database/Factories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Database/Migrations/.gitkeep b/be/Modules/User/Database/Migrations/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Database/Seeders/.gitkeep b/be/Modules/User/Database/Seeders/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Database/Seeders/UserDatabaseSeeder.php b/be/Modules/User/Database/Seeders/UserDatabaseSeeder.php new file mode 100644 index 0000000..d39578c --- /dev/null +++ b/be/Modules/User/Database/Seeders/UserDatabaseSeeder.php @@ -0,0 +1,16 @@ +call([]); + } +} diff --git a/be/Modules/User/Emails/.gitkeep b/be/Modules/User/Emails/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Entities/.gitkeep b/be/Modules/User/Entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Helpers/.gitkeep b/be/Modules/User/Helpers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Http/Controllers/.gitkeep b/be/Modules/User/Http/Controllers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Http/Controllers/UserController.php b/be/Modules/User/Http/Controllers/UserController.php new file mode 100644 index 0000000..78e9aa0 --- /dev/null +++ b/be/Modules/User/Http/Controllers/UserController.php @@ -0,0 +1,210 @@ +validate([ + 'roles' => 'required|array', + 'roles.*' => 'exists:roles,id,guard_name,api', + ]); + + $result = $this->userService->assignRoles($id, $request->roles); + + if (isset($result['error'])) { + $status = $result['error'] === 'User not found.' ? 404 : 400; + + return response()->json([ + 'success' => false, + 'message' => $result['error'], + ], $status); + } + + return response()->json([ + 'success' => true, + 'message' => 'Roles assigned successfully.', + 'data' => (new UserResource($result['user']))->resolve(), + ]); + } + + public function updateProfile(Request $request): JsonResponse + { + $user = $request->user(); + + $validated = $request->validate([ + 'name' => 'sometimes|string|max:255', + 'ic_number' => 'sometimes|string|max:255|unique:users,ic_number,'.$user->id, + 'position' => 'sometimes|string|max:255', + 'phone_number' => 'sometimes|string|max:255', + 'image' => 'sometimes|image|mimes:jpeg,png,jpg,gif|max:2048', + 'image_url' => 'sometimes|string|max:255', + ]); + + $user = $this->userService->updateProfile( + $user, + $validated, + $request->file('image') + ); + + return response()->json([ + 'success' => true, + 'data' => new UserResource($user), + 'message' => 'Profile updated successfully.', + ]); + } + + public function updatePassword(Request $request): JsonResponse + { + $user = $request->user(); + + $validated = $request->validate([ + 'current_password' => 'required|string', + 'password' => 'required|string|min:8|confirmed', + ]); + + $result = $this->userService->updatePassword( + $user, + $validated['current_password'], + $validated['password'] + ); + + if (isset($result['error'])) { + return response()->json([ + 'success' => false, + 'message' => $result['error'], + ], 400); + } + + return response()->json([ + 'success' => true, + 'message' => 'Password updated successfully.', + ]); + } + + public function index(Request $request): JsonResponse + { + $this->authorize('viewAny', $this->modelClass); + + try { + $perPage = min((int) $request->get('per_page', 10), 500); + $items = $this->userService->getPaginatedList( + $perPage, + $request->get('search', ''), + $request->get('status', ''), + $request->get('sort_by', 'id'), + $request->get('sort_order', 'asc') + ); + + return response()->json([ + 'success' => true, + 'data' => UserListResource::collection($items->items()), + 'pagination' => [ + 'current_page' => $items->currentPage(), + 'per_page' => $items->perPage(), + 'total' => $items->total(), + 'last_page' => $items->lastPage(), + 'from' => $items->firstItem(), + 'to' => $items->lastItem(), + 'has_more_pages' => $items->hasMorePages(), + ], + 'message' => $this->getSuccessMessage('index'), + ]); + } catch (Exception $e) { + Log::error("Error fetching {$this->resourceNamePlural}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('index'), 500); + } + } + + public function show($id): JsonResponse + { + $this->authorize('view', $this->modelClass); + + try { + $user = $this->userService->getUserWithRelations($id); + + if (! $user) { + return response()->json([ + 'success' => false, + 'message' => 'User not found.', + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => new UserResource($user), + 'message' => 'User retrieved successfully.', + ]); + } catch (Exception $e) { + Log::error('Error fetching user: '.$e->getMessage()); + + return $this->errorResponse('Failed to retrieve user.', 500); + } + } +} diff --git a/be/Modules/User/Http/Requests/.gitkeep b/be/Modules/User/Http/Requests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Http/Requests/UserRequest.php b/be/Modules/User/Http/Requests/UserRequest.php new file mode 100644 index 0000000..939fdfd --- /dev/null +++ b/be/Modules/User/Http/Requests/UserRequest.php @@ -0,0 +1,110 @@ +|string> + */ + public function rules(): array + { + // Get the user ID from the route (with null check) + $userId = $this->route() ? $this->route('user') : null; + + // Check if this is a create or update operation + $isCreate = $this->isMethod('POST'); + $isUpdate = $this->isMethod('PUT') || $this->isMethod('PATCH'); + + // If this is not a POST, PUT, or PATCH request, return minimal rules + if (! $isCreate && ! $isUpdate) { + return [ + 'name' => 'nullable|string|max:255', + 'email' => 'nullable|string|email|max:255', + 'password' => 'nullable|string|min:8', + 'ic_number' => 'nullable|string|max:255', + 'position' => 'nullable|string|max:255', + 'phone_number' => 'nullable|string|max:255', + 'image_url' => 'nullable|string|max:255', + 'status' => 'nullable|string', + ]; + } + + // Build unique email validation rule + $emailUniqueRule = $userId ? 'unique:users,email,'.$userId : 'unique:users,email'; + + // If this is an update operation, make email and password optional + if ($isUpdate) { + return [ + 'name' => 'required|string|max:255', + // 'email' => [ + // 'nullable', + // 'string', + // 'email', + // 'max:255', + // $emailUniqueRule, + // ], + 'password' => 'nullable|string|min:8', + 'ic_number' => 'required|string|max:255', + 'position' => 'required|string|max:255', + 'phone_number' => 'nullable|string|max:255', + 'image_url' => 'nullable|string|max:255', + 'status' => 'nullable|string', + ]; + } + + // Default rules for create operations + return [ + 'name' => 'required|string|max:255', + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + $emailUniqueRule, + ], + 'password' => 'nullable|string|min:8', + 'ic_number' => 'required|string|max:255', + 'position' => 'required|string|max:255', + 'phone_number' => 'nullable|string|max:255', + 'image_url' => 'nullable|string|max:255', + 'status' => 'required|string', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'name.required' => 'Nama diperlukan.', + 'name.max' => 'Nama tidak boleh melebihi 255 aksara.', + 'email.required' => 'Email diperlukan.', + 'email.email' => 'Email tidak sah.', + 'email.max' => 'Email tidak boleh melebihi 255 aksara.', + 'email.unique' => 'Email sudah wujud.', + 'password.nullable' => 'Kata laluan diperlukan.', + 'ic_number.required' => 'Nombor Kad Pengenalan diperlukan.', + 'ic_number.max' => 'Nombor Kad Pengenalan tidak boleh melebihi 255 aksara.', + 'position.required' => 'Posisi diperlukan.', + 'position.max' => 'Posisi tidak boleh melebihi 255 aksara.', + 'phone_number.max' => 'Nombor telefon tidak boleh melebihi 255 aksara.', + 'image_url.max' => 'URL imej tidak boleh melebihi 255 aksara.', + 'status.required' => 'Status diperlukan.', + 'status.string' => 'Status aktif tidak sah.', + ]; + } +} diff --git a/be/Modules/User/Jobs/.gitkeep b/be/Modules/User/Jobs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Notifications/.gitkeep b/be/Modules/User/Notifications/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Notifications/UserActivationNotification.php b/be/Modules/User/Notifications/UserActivationNotification.php new file mode 100644 index 0000000..62cee5d --- /dev/null +++ b/be/Modules/User/Notifications/UserActivationNotification.php @@ -0,0 +1,64 @@ +newUser = $newUser; + $this->sender = $sender; + } + + /** + * Get the notification's delivery channels. + */ + public function via($notifiable): array + { + return ['database']; + } + + /** + * Get the array representation of the notification. + */ + public function toArray($notifiable): array + { + return [ + 'user_id' => $this->newUser->id, + 'sender_id' => $this->sender ? $this->sender->id : null, + 'type' => 'user_activation_required', + 'message' => $this->getNotificationMessage(), + 'user_name' => $this->newUser->name ?? 'Unknown', + 'user_email' => $this->newUser->email ?? 'Unknown', + 'user_army_number' => $this->newUser->army_number ?? 'Unknown', + 'user_unit_name' => $this->newUser->unit->name ?? 'Unknown', + 'user_status' => $this->newUser->status ?? 'pending', + ]; + } + + /** + * Get notification message + */ + private function getNotificationMessage(): string + { + $userName = $this->newUser->name ?? 'Unknown'; + $userEmail = $this->newUser->email ?? 'Unknown'; + $userArmyNumber = $this->newUser->army_number ?? 'Unknown'; + $unitName = $this->newUser->unit->name ?? 'Unknown'; + + return "Pengguna baru memerlukan pengaktifan. Nama: {$userName}, Email: {$userEmail}, No. Tentera: {$userArmyNumber}, Unit: {$unitName}"; + } +} + diff --git a/be/Modules/User/Policies/.gitkeep b/be/Modules/User/Policies/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Policies/UserPolicy.php b/be/Modules/User/Policies/UserPolicy.php new file mode 100644 index 0000000..559a272 --- /dev/null +++ b/be/Modules/User/Policies/UserPolicy.php @@ -0,0 +1,59 @@ +hasPermissionTo('lihat pengguna'); + } + + /** + * Determine whether the user can view the model. + */ + public function view($user, ?User $userModel = null): bool + { + return $user->hasPermissionTo('lihat pengguna'); + } + + /** + * Determine whether the user can create models. + */ + public function create($user): bool + { + return $user->hasPermissionTo('tambah pengguna'); + } + + /** + * Determine whether the user can update the model. + */ + public function update($user, ?User $userModel = null): bool + { + return $user->hasPermissionTo('kemaskini pengguna'); + } + + /** + * Determine whether the user can delete any model. + */ + public function deleteAny($user): bool + { + return $user->hasPermissionTo('hapus pengguna'); + } + + /** + * Determine whether the user can delete the model. + */ + public function delete($user, ?User $userModel = null): bool + { + return $user->hasPermissionTo('hapus pengguna'); + } +} diff --git a/be/Modules/User/Providers/.gitkeep b/be/Modules/User/Providers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Providers/EventServiceProvider.php b/be/Modules/User/Providers/EventServiceProvider.php new file mode 100644 index 0000000..cad5007 --- /dev/null +++ b/be/Modules/User/Providers/EventServiceProvider.php @@ -0,0 +1,27 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/Modules/User/Providers/RouteServiceProvider.php b/be/Modules/User/Providers/RouteServiceProvider.php new file mode 100644 index 0000000..a987aa1 --- /dev/null +++ b/be/Modules/User/Providers/RouteServiceProvider.php @@ -0,0 +1,49 @@ +mapApiRoutes(); + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + */ + protected function mapWebRoutes(): void + { + Route::middleware('web')->group(module_path($this->name, '/Routes/web.php')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + */ + protected function mapApiRoutes(): void + { + Route::middleware('api')->name('api.')->group(module_path($this->name, '/Routes/api.php')); + } +} diff --git a/be/Modules/User/Providers/UserServiceProvider.php b/be/Modules/User/Providers/UserServiceProvider.php new file mode 100644 index 0000000..e135007 --- /dev/null +++ b/be/Modules/User/Providers/UserServiceProvider.php @@ -0,0 +1,172 @@ +registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, 'Database/Migrations')); + $this->registerPolicies(); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + + // Register repository binding + $this->app->bind( + \Modules\User\Repositories\Contracts\UserRepositoryInterface::class, + \Modules\User\Repositories\UserRepository::class + ); + } + + /** + * Register policies. + */ + protected function registerPolicies(): void + { + Gate::policy(User::class, UserPolicy::class); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, 'Lang'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, 'Lang')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, 'Resources/Views'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\'.$this->name.'\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } +} diff --git a/be/Modules/User/Repositories/.gitkeep b/be/Modules/User/Repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Repositories/Contracts/.gitkeep b/be/Modules/User/Repositories/Contracts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php b/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php new file mode 100644 index 0000000..6d4784e --- /dev/null +++ b/be/Modules/User/Repositories/Contracts/UserRepositoryInterface.php @@ -0,0 +1,50 @@ +user())->excludeDevelopersUnlessDeveloper()->orderBy('name'); + + if (! empty($search)) { + $query->where(function ($q) use ($search) { + $q->where('name', 'ILIKE', "%{$search}%"); + $q->orWhere('ic_number', 'ILIKE', "%{$search}%"); + $q->orWhere('email', 'ILIKE', "%{$search}%"); + $q->orWhere('phone_number', 'ILIKE', "%{$search}%"); + $q->orWhereHas('roles', function ($roleQuery) use ($search) { + $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); + }); + }); + } + + return $query->paginate($perPage); + } + + /** + * Get all Users with their relationships and pagination with search + */ + public function getAllWithRelationsPaginated( + int $perPage = 10, + string $search = '', + string $status = '', + string $sortBy = 'name', + string $sortOrder = 'asc' + ) { + $allowedSortColumns = [ + 'id', + 'name', + 'email', + 'position', + 'status', + 'ic_number', + 'phone_number', + 'created_at', + ]; + $sortBy = in_array($sortBy, $allowedSortColumns, true) ? $sortBy : 'name'; + $sortOrder = strtolower($sortOrder) === 'desc' ? 'desc' : 'asc'; + + $query = User::visibleTo(auth()->user())->excludeDevelopersUnlessDeveloper() + ->with([ + 'roles:id,name,guard_name' + ]) + ->orderBy($sortBy, $sortOrder); + + if (! empty($search)) { + $query->where(function ($q) use ($search) { + $q->where('name', 'ILIKE', "%{$search}%") + ->orWhere('ic_number', 'ILIKE', "%{$search}%") + ->orWhere('email', 'ILIKE', "%{$search}%") + ->orWhere('phone_number', 'ILIKE', "%{$search}%") + ->orWhereHas('roles', function ($roleQuery) use ($search) { + $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); + }); + $q->orWhere('ic_number', 'ILIKE', "%{$search}%"); + $q->orWhere('email', 'ILIKE', "%{$search}%"); + $q->orWhere('phone_number', 'ILIKE', "%{$search}%"); + $q->orWhereHas('roles', function ($roleQuery) use ($search) { + $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); + }); + }); + } + + // Add status filter + if (! empty($status)) { + $query->where('status', $status); + } + + return $query->paginate($perPage); + } + + /** + * Get all Users with their relationships and search + */ + public function getAllWithRelations(string $search = ''): Collection + { + $query = User::visibleTo(auth()->user())->excludeDevelopersUnlessDeveloper()->orderBy('name'); + + if (! empty($search)) { + $query->where(function ($q) use ($search) { + $q->where('name', 'ILIKE', "%{$search}%") + ->orWhere('ic_number', 'ILIKE', "%{$search}%") + ->orWhere('email', 'ILIKE', "%{$search}%") + ->orWhere('phone_number', 'ILIKE', "%{$search}%") + ->orWhereHas('roles', function ($roleQuery) use ($search) { + $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); + }); + }); + } + + return $query->get(); + } + + /** + * Create a new User + */ + public function create(array $data): User + { + return User::create($data); + } + + /** + * Find User by ID + */ + public function findById(string $id): ?User + { + return User::with(['roles'])->find($id); + } + + /** + * Delete User (soft delete) + */ + public function delete(string $id): bool + { + $User = User::find($id); + if ($User) { + return $User->delete(); + } + + return false; + } + + /** + * Get all Ranks + */ + public function all(string $search = ''): Collection + { + $query = User::visibleTo(auth()->user())->excludeDevelopersUnlessDeveloper()->orderBy('name'); + + if (! empty($search)) { + $query->where(function ($q) use ($search) { + $q->where('name', 'ILIKE', "%{$search}%") + ->orWhere('ic_number', 'ILIKE', "%{$search}%") + ->orWhere('email', 'ILIKE', "%{$search}%") + ->orWhere('phone_number', 'ILIKE', "%{$search}%") + ->orWhereHas('roles', function ($roleQuery) use ($search) { + $roleQuery->whereRaw('LOWER(name) ILIKE ?', ['%' . strtolower($search) . '%']); + }); + }); + } + + return $query->get(); + } +} diff --git a/be/Modules/User/Routes/.gitkeep b/be/Modules/User/Routes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Routes/api.php b/be/Modules/User/Routes/api.php new file mode 100644 index 0000000..d09e4f1 --- /dev/null +++ b/be/Modules/User/Routes/api.php @@ -0,0 +1,21 @@ +prefix('v1')->group(function () { + Route::apiResource('users', UserController::class)->names('user'); + + // User role management routes + Route::post('users/{user}/roles', [UserController::class, 'assignRoles'])->name('users.roles.assign'); + + // User profile management routes + Route::post('/profile', [UserController::class, 'updateProfile']); + Route::put('/profile/password', [UserController::class, 'updatePassword']); + + // Impersonation routes + Route::get('/impersonate/take/{id}', [ImpersonateController::class, 'take'])->name('impersonate'); + Route::get('/impersonate/leave', [ImpersonateController::class, 'leave'])->name('impersonate.leave'); + Route::get('/impersonate/status', [ImpersonateController::class, 'status'])->name('impersonate.status'); +}); diff --git a/be/Modules/User/Services/.gitkeep b/be/Modules/User/Services/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Services/UserService.php b/be/Modules/User/Services/UserService.php new file mode 100644 index 0000000..0a9d9bf --- /dev/null +++ b/be/Modules/User/Services/UserService.php @@ -0,0 +1,116 @@ +repository->getAllWithRelationsPaginated( + $perPage, + $search, + $status, + $sortBy, + $sortOrder + ); + } + + public function getUserWithRelations(string $id): ?User + { + $user = $this->repository->findById($id); + + if ($user) { + $user->load(['roles.permissions']); + } + + return $user; + } + + /** + * @return array{user: User}|array{error: string} + */ + public function assignRoles(string $id, array $roleIds): array + { + $user = $this->repository->findById($id); + + if (! $user) { + return ['error' => 'User not found.']; + } + + $roles = Role::whereIn('id', $roleIds) + ->where('guard_name', 'api') + ->get(); + + if ($roles->isEmpty()) { + return ['error' => 'No valid roles found.']; + } + + $user->syncRoles($roles); + $user->load(['roles.permissions']); + + return ['user' => $user]; + } + + public function updateProfile(User $user, array $validated, ?UploadedFile $image = null): User + { + if ($image) { + if ($user->image_url && Storage::disk('public')->exists($user->image_url)) { + Storage::disk('public')->delete($user->image_url); + } + + $validated['image_url'] = $image->store('user-images', 'public'); + } + + unset($validated['image']); + + $data = $this->prepareProfileUpdateData($validated); + $user->update($data); + $user->load(['roles.permissions']); + + return $user; + } + + /** + * @return array{}|array{error: string} + */ + public function updatePassword(User $user, string $currentPassword, string $newPassword): array + { + if (! Hash::check($currentPassword, $user->password)) { + return ['error' => 'Current password is incorrect.']; + } + + $user->update([ + 'password' => Hash::make($newPassword), + ]); + + return []; + } + + protected function prepareProfileUpdateData(array $validated): array + { + unset($validated['uuid']); + + if (! array_key_exists('email', $validated)) { + unset($validated['email']); + } + + return $validated; + } +} diff --git a/be/Modules/User/Tests/Feature/.gitkeep b/be/Modules/User/Tests/Feature/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Tests/Unit/.gitkeep b/be/Modules/User/Tests/Unit/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Transformers/.gitkeep b/be/Modules/User/Transformers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/be/Modules/User/Transformers/UserListResource.php b/be/Modules/User/Transformers/UserListResource.php new file mode 100644 index 0000000..5426fee --- /dev/null +++ b/be/Modules/User/Transformers/UserListResource.php @@ -0,0 +1,35 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'ic_number' => $this->ic_number, + 'position' => $this->position, + 'phone_number' => $this->phone_number, + 'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null, + 'status' => $this->status, + 'email_verified_at' => $this->email_verified_at, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'deleted_at' => $this->deleted_at, + 'roles' => $this->roles->map(function ($role) { + return [ + 'id' => $role->id, + 'name' => $role->name, + 'guard_name' => $role->guard_name, + ]; + }), + ]; + } +} diff --git a/be/Modules/User/Transformers/UserResource.php b/be/Modules/User/Transformers/UserResource.php new file mode 100644 index 0000000..b7021e4 --- /dev/null +++ b/be/Modules/User/Transformers/UserResource.php @@ -0,0 +1,54 @@ + $this->id, + 'name' => $this->name, + 'email' => $this->email, + 'ic_number' => $this->ic_number, + 'position' => $this->position, + 'phone_number' => $this->phone_number, + 'image_url' => $this->image_url ? Storage::disk('public')->url($this->image_url) : null, + 'status' => $this->status, + 'two_factor_secret' => $this->two_factor_secret, + 'two_factor_recovery_codes' => $this->two_factor_recovery_codes, + 'two_factor_confirmed_at' => $this->two_factor_confirmed_at, + 'email_verified_at' => $this->email_verified_at, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'deleted_at' => $this->deleted_at, + 'roles' => $this->whenLoaded('roles', function () { + return $this->roles->map(function ($role) { + return [ + 'id' => $role->id, + 'name' => $role->name, + 'guard_name' => $role->guard_name, + 'permissions' => $this->when($role->relationLoaded('permissions'), function () use ($role) { + return $role->permissions->map(function ($permission) { + return [ + 'id' => $permission->id, + 'name' => $permission->name, + 'guard_name' => $permission->guard_name, + 'route_name' => $permission->route_name, + 'created_at' => $permission->created_at, + 'updated_at' => $permission->updated_at, + ]; + }); + }), + 'created_at' => $role->created_at, + 'updated_at' => $role->updated_at, + ]; + }); + }), + ]; + } +} diff --git a/be/Modules/User/composer.json b/be/Modules/User/composer.json new file mode 100644 index 0000000..dd75115 --- /dev/null +++ b/be/Modules/User/composer.json @@ -0,0 +1,30 @@ +{ + "name": "nwidart/user", + "description": "", + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "Modules\\User\\": "App", + "Modules\\User\\Database\\Factories\\": "database/factories/", + "Modules\\User\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "Modules\\User\\Tests\\": "tests/" + } + } +} diff --git a/be/Modules/User/module.json b/be/Modules/User/module.json new file mode 100644 index 0000000..09e540d --- /dev/null +++ b/be/Modules/User/module.json @@ -0,0 +1,11 @@ +{ + "name": "User", + "alias": "user", + "description": "", + "keywords": [], + "priority": 0, + "providers": [ + "Modules\\User\\Providers\\UserServiceProvider" + ], + "files": [] +} diff --git a/be/Modules/User/package.json b/be/Modules/User/package.json new file mode 100644 index 0000000..d6fbfc8 --- /dev/null +++ b/be/Modules/User/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "devDependencies": { + "axios": "^1.1.2", + "laravel-vite-plugin": "^0.7.5", + "sass": "^1.69.5", + "postcss": "^8.3.7", + "vite": "^4.0.0" + } +} diff --git a/be/README.md b/be/README.md new file mode 100644 index 0000000..75c347a --- /dev/null +++ b/be/README.md @@ -0,0 +1,61 @@ +

Laravel Logo

+ +

+Build Status +Total Downloads +Latest Stable Version +License +

+ +## About Laravel + +Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as: + +- [Simple, fast routing engine](https://laravel.com/docs/routing). +- [Powerful dependency injection container](https://laravel.com/docs/container). +- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage. +- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent). +- Database agnostic [schema migrations](https://laravel.com/docs/migrations). +- [Robust background job processing](https://laravel.com/docs/queues). +- [Real-time event broadcasting](https://laravel.com/docs/broadcasting). + +Laravel is accessible, powerful, and provides tools required for large, robust applications. + +## Learning Laravel + +Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework. + +You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch. + +If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains thousands of video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library. + +## Laravel Sponsors + +We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the [Laravel Partners program](https://partners.laravel.com). + +### Premium Partners + +- **[Vehikl](https://vehikl.com)** +- **[Tighten Co.](https://tighten.co)** +- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)** +- **[64 Robots](https://64robots.com)** +- **[Curotec](https://www.curotec.com/services/technologies/laravel)** +- **[DevSquad](https://devsquad.com/hire-laravel-developers)** +- **[Redberry](https://redberry.international/laravel-development)** +- **[Active Logic](https://activelogic.com)** + +## Contributing + +Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions). + +## Code of Conduct + +In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). + +## Security Vulnerabilities + +If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed. + +## License + +The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT). diff --git a/be/app/Console/Commands/AutoGenerateWeeklyReportForUnit.php b/be/app/Console/Commands/AutoGenerateWeeklyReportForUnit.php new file mode 100644 index 0000000..2020eba --- /dev/null +++ b/be/app/Console/Commands/AutoGenerateWeeklyReportForUnit.php @@ -0,0 +1,56 @@ +argument('unit'); + + $unit = Unit::where('name', $unitName)->first(); + + if (! $unit) { + $this->error("Unit '{$unitName}' not found."); + + return 1; + } + + $month = strtolower(now()->format('F')); + $week = (int) ceil(now()->day / 7); + + if (! $this->option('force')) { + $existingReports = KJCReport::where('unit_id', $unit->id) + ->where('report_type', 'weekly') + ->where('report_month', $month) + ->where('report_week', $week) + ->exists(); + + if ($existingReports) { + $this->info("Reports for {$unitName} - {$month} week {$week} already exist. Skipping."); + + return 0; + } + } + + $user = User::where('unit_id', $unit->id)->first(); + + GenerateTeamWeeklyReports::dispatch($unit->id, $month, $week, $user?->id); + + $this->info("✓ Queued weekly report generation for {$unitName} - {$month} week {$week}"); + + return 0; + } +} diff --git a/be/app/Console/Commands/TriggerKJCHistoricalDataCapture.php b/be/app/Console/Commands/TriggerKJCHistoricalDataCapture.php new file mode 100644 index 0000000..c8380de --- /dev/null +++ b/be/app/Console/Commands/TriggerKJCHistoricalDataCapture.php @@ -0,0 +1,68 @@ +option('month'); + $year = $this->option('year'); + $useQueue = $this->option('queue'); + + // If no month/year provided, use previous month + if (! $month || ! $year) { + $lastMonth = Carbon::now()->subMonth(); + $month = $month ?: $lastMonth->format('m'); + $year = $year ?: $lastMonth->format('Y'); + } + + $this->info("Triggering KJC historical data capture for {$month}/{$year}..."); + + try { + if ($useQueue) { + // Dispatch to queue + CaptureKJCHistoricalDataJob::dispatch($month, $year); + $this->info('✓ KJC historical data capture job dispatched to queue'); + $this->info('You can monitor the job in Horizon dashboard'); + } else { + // Run immediately + $job = new CaptureKJCHistoricalDataJob($month, $year); + $job->handle(app(KJCHistoricalDataService::class)); + $this->info('✓ KJC historical data capture completed immediately'); + } + + } catch (\Exception $e) { + $this->error('Error: '.$e->getMessage()); + + return 1; + } + + return 0; + } +} diff --git a/be/app/Console/Commands/TriggerPKJHistoricalDataCapture.php b/be/app/Console/Commands/TriggerPKJHistoricalDataCapture.php new file mode 100644 index 0000000..b3c638b --- /dev/null +++ b/be/app/Console/Commands/TriggerPKJHistoricalDataCapture.php @@ -0,0 +1,55 @@ +option('month'); + $year = $this->option('year'); + $useQueue = $this->option('queue'); + + // If no month/year provided, use previous month + if (! $month || ! $year) { + $lastMonth = Carbon::now()->subMonth(); + $month = $month ?: $lastMonth->format('m'); + $year = $year ?: $lastMonth->format('Y'); + } + + $this->info("Triggering PKJ historical data capture for {$month}/{$year}..."); + + try { + if ($useQueue) { + // Dispatch to queue + CapturePKJHistoricalDataJob::dispatch($month, $year); + $this->info('✓ PKJ historical data capture job dispatched to queue'); + $this->info('You can monitor the job in Horizon dashboard'); + } else { + // Run immediately + $job = new CapturePKJHistoricalDataJob($month, $year); + $job->handle(app(PKJHistoricalDataService::class)); + $this->info('✓ PKJ historical data capture completed immediately'); + } + + } catch (\Exception $e) { + $this->error('Error: '.$e->getMessage()); + + return 1; + } + + return 0; + } +} diff --git a/be/app/Http/Controllers/BaseCrudController.php b/be/app/Http/Controllers/BaseCrudController.php new file mode 100644 index 0000000..8281cb6 --- /dev/null +++ b/be/app/Http/Controllers/BaseCrudController.php @@ -0,0 +1,378 @@ +repository = $repository; + } + + /** + * Display a listing of the resource. + */ + public function index(Request $request): JsonResponse + { + $this->authorize('viewAny', $this->modelClass); + + try { + $perPage = $request->get('per_page', 10) ?? 10; + $perPage = min($perPage, 1000); // Limit max per page to 1000 + $search = (string) ($request->get('search', '') ?? ''); + $sortBy = (string) ($request->get('sort_by', 'id') ?? 'id'); + $sortOrder = (string) ($request->get('sort_order', 'asc') ?? 'asc'); + + $items = $this->getIndexData($request, $perPage, $search, $sortBy, $sortOrder); + + // Check if the result is paginated + if ($items instanceof LengthAwarePaginator) { + return response()->json([ + 'success' => true, + 'data' => $this->resourceClass::collection($items->items()), + 'pagination' => [ + 'current_page' => $items->currentPage(), + 'per_page' => $items->perPage(), + 'total' => $items->total(), + 'last_page' => $items->lastPage(), + 'from' => $items->firstItem(), + 'to' => $items->lastItem(), + 'has_more_pages' => $items->hasMorePages(), + ], + 'message' => $this->getSuccessMessage('index'), + ]); + } + + // Fallback for non-paginated results + return response()->json([ + 'success' => true, + 'data' => $this->resourceClass::collection($items), + 'message' => $this->getSuccessMessage('index'), + ]); + } catch (Exception $e) { + Log::error("Error fetching {$this->resourceNamePlural}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('index'), 500); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request): JsonResponse + { + $this->authorize('create', $this->modelClass); + + try { + $validated = $this->validateRequest($request); + $data = $this->prepareStoreData($validated); + + $item = $this->repository->create($data); + + ActivityLogger::log("Created {$this->resourceName}: {$this->getItemName($item)}", $item); + + return response()->json([ + 'success' => true, + 'data' => new $this->resourceClass($item), + 'message' => $this->getSuccessMessage('store'), + ], 201); + + } catch (Exception $e) { + Log::error("Error creating {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('store').': '.$e->getMessage(), 500); + } + } + + /** + * Display the specified resource. + */ + public function show(string $id): JsonResponse + { + $this->authorize('view', $this->modelClass); + + try { + $item = $this->repository->findById($id); + + if (! $item) { + return $this->errorResponse($this->getNotFoundMessage(), 404); + } + + $item = $this->loadShowRelations($item); + + return response()->json([ + 'success' => true, + 'data' => new $this->resourceClass($item), + 'message' => $this->getSuccessMessage('show'), + ]); + } catch (Exception $e) { + Log::error("Error fetching {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('show'), 500); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, string $id): JsonResponse + { + $this->authorize('update', $this->modelClass); + + try { + $item = $this->repository->findById($id); + + if (! $item) { + return $this->errorResponse($this->getNotFoundMessage(), 404); + } + + $validated = $this->validateRequest($request); + $data = $this->prepareUpdateData($validated, $item); + + $item->update($data); + + ActivityLogger::log("Updated {$this->resourceName}: {$this->getItemName($item)}", $item); + + return response()->json([ + 'success' => true, + 'data' => new $this->resourceClass($item), + 'message' => $this->getSuccessMessage('update'), + ]); + + } catch (Exception $e) { + Log::error("Error updating {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('update').': '.$e->getMessage(), 500); + } + } + + /** + * Partially update the specified resource in storage (PATCH). + */ + public function patch(Request $request, string $id): JsonResponse + { + // PATCH uses the same logic as update + return $this->update($request, $id); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(string $id): JsonResponse + { + $this->authorize('deleteAny', $this->modelClass); + + try { + $item = $this->repository->findById($id); + + if (! $item) { + return $this->errorResponse($this->getNotFoundMessage(), 404); + } + + // Check for dependencies before deletion + $dependencyCheck = $this->checkDependencies($item); + if ($dependencyCheck !== null) { + return $dependencyCheck; + } + + $this->repository->delete($id); + + ActivityLogger::log("Deleted {$this->resourceName}: {$this->getItemName($item)}", $item); + + return response()->json([ + 'success' => true, + 'message' => $this->getSuccessMessage('destroy'), + ]); + + } catch (Exception $e) { + Log::error("Error deleting {$this->resourceName}: ".$e->getMessage()); + + return $this->errorResponse($this->getErrorMessage('destroy').': '.$e->getMessage(), 500); + } + } + + /** + * Get data for index method - can be overridden by child classes + */ + protected function getIndexData(Request $request, int $perPage = 10, string $search = '', string $sortBy = 'id', string $sortOrder = 'asc') + { + // Always try paginated methods first when perPage is specified + if ($perPage > 0) { + if (method_exists($this->repository, 'getAllWithRelationsPaginated')) { + return $this->repository->getAllWithRelationsPaginated($perPage, $search, $sortBy, $sortOrder); + } + + if (method_exists($this->repository, 'getAllPaginated')) { + return $this->repository->getAllPaginated($perPage, $search, $sortBy, $sortOrder); + } + } + + // Fallback to non-paginated methods + if (method_exists($this->repository, 'getAllWithRelations')) { + return $this->repository->getAllWithRelations($search, $sortBy, $sortOrder); + } + + return $this->repository->all($search, $sortBy, $sortOrder); + } + + /** + * Validate request data - can be overridden by child classes + */ + protected function validateRequest(Request $request): array + { + if ($this->requestClass) { + // For multipart/form-data, use direct validation to avoid FormRequest parsing issues + if (str_contains($request->header('Content-Type', ''), 'multipart/form-data')) { + return $request->validate(app($this->requestClass)->rules()); + } + + // Create FormRequest instance with the current request + $formRequest = $this->requestClass::createFrom($request); + $formRequest->setContainer(app()); + $formRequest->setRedirector(app('redirect')); + + // Validate the request + $formRequest->validateResolved(); + + return $formRequest->validated(); + } + + return $request->all(); + } + + /** + * Prepare data for store method - can be overridden by child classes + */ + protected function prepareStoreData(array $validated): array + { + return $validated; + } + + /** + * Prepare data for update method - can be overridden by child classes + */ + protected function prepareUpdateData(array $validated, $item): array + { + return $validated; + } + + /** + * Load relations for show method - can be overridden by child classes + */ + protected function loadShowRelations($item) + { + return $item; + } + + /** + * Check dependencies before deletion - can be overridden by child classes + * Return null if deletion is allowed, or a JsonResponse if not + */ + protected function checkDependencies($item): ?JsonResponse + { + return null; + } + + /** + * Get the name of the item for logging - can be overridden by child classes + */ + protected function getItemName($item): string + { + return $item->name ?? $item->id; + } + + /** + * Get success message for different operations + */ + protected function getSuccessMessage(string $operation): string + { + $messages = [ + 'index' => ucfirst($this->resourceNamePlural).' berjaya dimuatkan', + 'store' => ucfirst($this->resourceName).' berjaya ditambah', + 'show' => ucfirst($this->resourceName).' berjaya dimuatkan', + 'update' => ucfirst($this->resourceName).' berjaya dikemaskini', + 'destroy' => ucfirst($this->resourceName).' berjaya dipadam', + ]; + + return $messages[$operation] ?? 'Operasi berjaya'; + } + + /** + * Get error message for different operations + */ + protected function getErrorMessage(string $operation): string + { + $messages = [ + 'index' => 'Gagal memuatkan '.$this->resourceNamePlural, + 'store' => 'Gagal menambah '.$this->resourceName, + 'show' => 'Gagal memuatkan '.$this->resourceName, + 'update' => 'Gagal mengemaskini '.$this->resourceName, + 'destroy' => 'Gagal memadam '.$this->resourceName, + ]; + + return $messages[$operation] ?? 'Operasi gagal'; + } + + /** + * Get not found message + */ + protected function getNotFoundMessage(): string + { + return ucfirst($this->resourceName).' tidak dijumpai.'; + } + + /** + * Return a standardized error response + */ + protected function errorResponse(string $message, int $statusCode = 400): JsonResponse + { + return response()->json([ + 'success' => false, + 'message' => $message, + ], $statusCode); + } +} diff --git a/be/app/Http/Controllers/ContactController.php b/be/app/Http/Controllers/ContactController.php new file mode 100644 index 0000000..5fae078 --- /dev/null +++ b/be/app/Http/Controllers/ContactController.php @@ -0,0 +1,209 @@ +contactService = $contactService; + } + + /** + * Get all active contact persons + */ + public function getContacts(): JsonResponse + { + try { + $contacts = $this->contactService->getActiveContacts(); + + return response()->json([ + 'success' => true, + 'data' => $contacts, + 'message' => 'Contact persons retrieved successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve contact persons', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get contact settings for admin (including inactive) + */ + public function getContactSettings(): JsonResponse + { + try { + $settings = $this->contactService->getAllContactSettings(); + + return response()->json([ + 'success' => true, + 'data' => $settings, + 'message' => 'Contact settings retrieved successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve contact settings', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Update contact settings (Admin only) + */ + public function updateContacts(Request $request): JsonResponse + { + $request->validate([ + 'contacts' => 'required|array', + 'contacts.*.name' => 'required|string|max:100', + 'contacts.*.position' => 'nullable|string|max:100', + 'contacts.*.email' => 'nullable|email|max:255', + 'contacts.*.phone' => 'nullable|string|max:50', + 'contacts.*.department' => 'nullable|string|max:100', + 'contacts.*.is_active' => 'boolean', + 'contacts.*.sort_order' => 'integer|min:0' + ]); + + try { + DB::beginTransaction(); + + $result = $this->contactService->updateContactSettings($request->contacts); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Contact settings updated successfully' + ]); + } catch (\Exception $e) { + DB::rollBack(); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to update contact settings', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Add a new contact person (Admin only) + */ + public function addContact(Request $request): JsonResponse + { + $request->validate([ + 'name' => 'required|string|max:100', + 'position' => 'nullable|string|max:100', + 'email' => 'nullable|email|max:255', + 'phone' => 'nullable|string|max:50', + 'department' => 'nullable|string|max:100', + 'is_active' => 'boolean', + 'sort_order' => 'integer|min:0' + ]); + + try { + $result = $this->contactService->addContactPerson($request->all()); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Contact person added successfully' + ], 201); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to add contact person', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Update a specific contact person (Admin only) + */ + public function updateContact(Request $request, int $id): JsonResponse + { + $request->validate([ + 'name' => 'required|string|max:100', + 'position' => 'nullable|string|max:100', + 'email' => 'nullable|email|max:255', + 'phone' => 'nullable|string|max:50', + 'department' => 'nullable|string|max:100', + 'is_active' => 'boolean', + 'sort_order' => 'integer|min:0' + ]); + + try { + $result = $this->contactService->updateContactPerson($id, $request->all()); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Contact person updated successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to update contact person', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Delete a contact person (Admin only) + */ + public function deleteContact(int $id): JsonResponse + { + try { + $result = $this->contactService->deleteContactPerson($id); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Contact person deleted successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to delete contact person', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Toggle active status of a contact person (Admin only) + */ + public function toggleContact(int $id): JsonResponse + { + try { + $result = $this->contactService->toggleContactPerson($id); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Contact person status toggled successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to toggle contact person status', + 'error' => $e->getMessage() + ], 500); + } + } +} diff --git a/be/app/Http/Controllers/Controller.php b/be/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..8677cd5 --- /dev/null +++ b/be/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +json([ + 'success' => true, + 'data' => $countries, + 'message' => 'Countries fetched successfully', + ]); + return response()->json([ + 'success' => true, + 'data' => $countries, + 'message' => 'Countries fetched successfully', + ]); + } +} \ No newline at end of file diff --git a/be/app/Http/Controllers/ImpersonateController.php b/be/app/Http/Controllers/ImpersonateController.php new file mode 100644 index 0000000..34cdbbc --- /dev/null +++ b/be/app/Http/Controllers/ImpersonateController.php @@ -0,0 +1,151 @@ +user(); + + if (! $admin->can('impersonate.user', $target)) { + return $this->error('Anda tidak mempunyai keizinan untuk menyamar pengguna.', 403); + } + + if ($request->hasCookie(AuthCookie::originalUserCookieName())) { + return $this->error('Anda sudah menyamar sebagai pengguna.', 400); + } + + $issued = $this->issueToken($target, self::IMPERSONATION_TOKEN); + + return AuthCookie::attachAuthToken( + response()->json([ + 'success' => true, + 'message' => "Anda sekarang menyamar sebagai {$target->name}", + 'impersonated_user' => $this->userSummary($target), + ...$this->sessionPayload($target, $issued['accessToken']), + ]), + $issued['plainTextToken'] + )->withCookie(AuthCookie::makeOriginalUserId($admin->id)); + } + + public function leave(Request $request): JsonResponse + { + $originalUserId = $request->cookie(AuthCookie::originalUserCookieName()); + + if (! $originalUserId) { + return $this->error('Maklumat pengguna asal tidak ditemui.', 400); + } + + $original = User::findOrFail($originalUserId); + + $request->user()?->tokens()->where('name', self::IMPERSONATION_TOKEN)->delete(); + $original->tokens()->whereIn('name', [self::IMPERSONATION_TOKEN, ...self::AUTH_TOKEN_NAMES])->delete(); + + $issued = $this->issueToken($original, 'auth-token', expires: true); + + $response = response()->json([ + 'success' => true, + 'message' => 'Anda telah kembali ke akaun anda', + 'original_user' => $this->userSummary($original), + ...$this->sessionPayload($original, $issued['accessToken']), + ]); + + if (AuthCookie::shouldExposeTokenInResponse()) { + $response->setData(array_merge($response->getData(true), [ + 'original_token' => $issued['plainTextToken'], + ])); + } + + return AuthCookie::attachAuthToken($response, $issued['plainTextToken']) + ->withCookie(AuthCookie::forgetOriginalUserId()); + } + + public function status(Request $request): JsonResponse + { + if (! $request->hasCookie(AuthCookie::originalUserCookieName())) { + return response()->json(['is_impersonating' => false]); + } + + $user = $request->user(); + + return response()->json([ + 'is_impersonating' => true, + 'impersonated_user' => $user ? $this->userSummary($user) : null, + ]); + } + + /** + * @return array{plainTextToken: string, accessToken: PersonalAccessToken} + */ + private function issueToken(User $user, string $name, bool $expires = false): array + { + $user->loadMissing(['roles.permissions']); + + $result = $user->createToken( + $name, + ['*'], + $expires ? now()->addMinutes((int) config('auth_cookie.lifetime_minutes', 720)) : null + ); + + ActiveRoleService::assignDefaultToToken($user, $result->accessToken); + + return [ + 'plainTextToken' => $result->plainTextToken, + 'accessToken' => $result->accessToken, + ]; + } + + /** + * @return array + */ + private function sessionPayload(User $user, PersonalAccessToken $accessToken): array + { + $user->loadMissing(['roles.permissions']); + + $activeRole = $user->roles->firstWhere('id', $accessToken->active_role_id) + ?? ActiveRoleService::resolveDefaultRole($user); + + return [ + 'data' => new UserResource($user), + 'active_role' => ActiveRoleService::formatRole($activeRole), + 'can_switch_role' => $user->roles->count() > 1, + 'redirect_path' => $activeRole + ? ActiveRoleService::redirectPathForRole($activeRole) + : config('active_role.member_redirect', '/profile'), + ]; + } + + /** + * @return array{id: mixed, name: string, email: string} + */ + private function userSummary(User $user): array + { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + ]; + } + + private function error(string $message, int $status): JsonResponse + { + return response()->json([ + 'success' => false, + 'message' => $message, + ], $status); + } +} diff --git a/be/app/Http/Controllers/OnlineUsersController.php b/be/app/Http/Controllers/OnlineUsersController.php new file mode 100644 index 0000000..3e53297 --- /dev/null +++ b/be/app/Http/Controllers/OnlineUsersController.php @@ -0,0 +1,183 @@ +onlineUsersService = $onlineUsersService; + } + + /** + * Get list of online users + * + * @param Request $request + * @return JsonResponse + */ + public function index(Request $request): JsonResponse + { + try { + $timeoutMinutes = 5; // Fixed to 5 minutes + + $onlineUsers = $this->onlineUsersService->getOnlineUsers($timeoutMinutes); + + return response()->json([ + 'success' => true, + 'data' => $onlineUsers, + 'meta' => [ + 'timeout_minutes' => $timeoutMinutes, + 'total_online' => $onlineUsers->count(), + 'timestamp' => now()->toISOString() + ], + 'message' => 'Online users retrieved successfully.' + ]); + + } catch (\Exception $e) { + Log::error('Error fetching online users: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve online users.', + 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' + ], 500); + } + } + + /** + * Get online users count + * + * @param Request $request + * @return JsonResponse + */ + public function count(Request $request): JsonResponse + { + try { + $timeoutMinutes = 5; // Fixed to 5 minutes + + $count = $this->onlineUsersService->getOnlineUsersCount($timeoutMinutes); + + return response()->json([ + 'success' => true, + 'data' => [ + 'count' => $count, + 'timeout_minutes' => $timeoutMinutes + ], + 'message' => 'Online users count retrieved successfully.' + ]); + + } catch (\Exception $e) { + Log::error('Error fetching online users count: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve online users count.', + 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' + ], 500); + } + } + + /** + * Get online users statistics + * + * @return JsonResponse + */ + public function stats(): JsonResponse + { + try { + $stats = $this->onlineUsersService->getOnlineUsersStats(); + + return response()->json([ + 'success' => true, + 'data' => $stats, + 'message' => 'Online users statistics retrieved successfully.' + ]); + + } catch (\Exception $e) { + Log::error('Error fetching online users stats: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve online users statistics.', + 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' + ], 500); + } + } + + /** + * Get current user's session information + * + * @param Request $request + * @return JsonResponse + */ + public function mySession(Request $request): JsonResponse + { + try { + $user = $request->user(); + + if (!$user) { + return response()->json([ + 'success' => false, + 'message' => 'User not authenticated.' + ], 401); + } + + $sessionInfo = $this->onlineUsersService->getUserSessionInfo($user->id); + + if (!$sessionInfo) { + return response()->json([ + 'success' => false, + 'message' => 'No active session found.' + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => $sessionInfo, + 'message' => 'Session information retrieved successfully.' + ]); + + } catch (\Exception $e) { + Log::error('Error fetching user session info: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve session information.', + 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' + ], 500); + } + } + + /** + * Clear online users cache (admin only) + * + * @return JsonResponse + */ + public function clearCache(): JsonResponse + { + try { + $this->onlineUsersService->clearCache(); + + return response()->json([ + 'success' => true, + 'message' => 'Online users cache cleared successfully.' + ]); + + } catch (\Exception $e) { + Log::error('Error clearing online users cache: ' . $e->getMessage()); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to clear cache.', + 'error' => config('app.debug') ? $e->getMessage() : 'Internal server error' + ], 500); + } + } +} diff --git a/be/app/Http/Controllers/SocialMediaController.php b/be/app/Http/Controllers/SocialMediaController.php new file mode 100644 index 0000000..3421812 --- /dev/null +++ b/be/app/Http/Controllers/SocialMediaController.php @@ -0,0 +1,206 @@ +socialMediaService = $socialMediaService; + } + + /** + * Get all active social media platforms + */ + public function getSocialMedia(): JsonResponse + { + try { + $socialMedia = $this->socialMediaService->getActiveSocialMedia(); + + return response()->json([ + 'success' => true, + 'data' => $socialMedia, + 'message' => 'Social media platforms retrieved successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve social media platforms', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Update social media settings (Admin only) + */ + public function updateSocialMedia(Request $request): JsonResponse + { + $request->validate([ + 'social_media' => 'required|array', + 'social_media.*.platform' => 'required|string|max:50', + 'social_media.*.name' => 'required|string|max:100', + 'social_media.*.url' => 'required|url|max:255', + 'social_media.*.icon' => 'nullable|string|max:100', + 'social_media.*.is_active' => 'boolean', + 'social_media.*.sort_order' => 'integer|min:0' + ]); + + try { + DB::beginTransaction(); + + $result = $this->socialMediaService->updateSocialMediaSettings($request->social_media); + + DB::commit(); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Social media settings updated successfully' + ]); + } catch (\Exception $e) { + DB::rollBack(); + + return response()->json([ + 'success' => false, + 'message' => 'Failed to update social media settings', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Get social media settings for admin (including inactive) + */ + public function getSocialMediaSettings(): JsonResponse + { + try { + $settings = $this->socialMediaService->getAllSocialMediaSettings(); + + return response()->json([ + 'success' => true, + 'data' => $settings, + 'message' => 'Social media settings retrieved successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to retrieve social media settings', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Add a new social media platform (Admin only) + */ + public function addSocialMedia(Request $request): JsonResponse + { + $request->validate([ + 'platform' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'url' => 'required|url|max:255', + 'icon' => 'nullable|string|max:100', + 'is_active' => 'boolean', + 'sort_order' => 'integer|min:0' + ]); + + try { + $result = $this->socialMediaService->addSocialMediaPlatform($request->all()); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Social media platform added successfully' + ], 201); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to add social media platform', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Update a specific social media platform (Admin only) + */ + public function updateSocialMediaPlatform(Request $request, int $id): JsonResponse + { + $request->validate([ + 'platform' => 'required|string|max:50', + 'name' => 'required|string|max:100', + 'url' => 'required|url|max:255', + 'icon' => 'nullable|string|max:100', + 'is_active' => 'boolean', + 'sort_order' => 'integer|min:0' + ]); + + try { + $result = $this->socialMediaService->updateSocialMediaPlatform($id, $request->all()); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Social media platform updated successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to update social media platform', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Delete a social media platform (Admin only) + */ + public function deleteSocialMediaPlatform(int $id): JsonResponse + { + try { + $result = $this->socialMediaService->deleteSocialMediaPlatform($id); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Social media platform deleted successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to delete social media platform', + 'error' => $e->getMessage() + ], 500); + } + } + + /** + * Toggle active status of a social media platform (Admin only) + */ + public function toggleSocialMediaPlatform(int $id): JsonResponse + { + try { + $result = $this->socialMediaService->toggleSocialMediaPlatform($id); + + return response()->json([ + 'success' => true, + 'data' => $result, + 'message' => 'Social media platform status toggled successfully' + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to toggle social media platform status', + 'error' => $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/be/app/Http/Middleware/ApiKeyAuthenticationMiddleware.php b/be/app/Http/Middleware/ApiKeyAuthenticationMiddleware.php new file mode 100644 index 0000000..0c9fca4 --- /dev/null +++ b/be/app/Http/Middleware/ApiKeyAuthenticationMiddleware.php @@ -0,0 +1,114 @@ +extractApiKey($request); + + if (!$apiKey) { + return response()->json([ + 'success' => false, + 'message' => 'API key is required for external system access.', + 'error' => 'API_KEY_REQUIRED' + ], 401); + } + + // Validate API key + if (!$this->isValidApiKey($apiKey)) { + // Log invalid API key attempt + if (config('api_security.log_api_key_usage', true)) { + Log::warning('Invalid API key attempt', [ + 'ip' => $request->ip(), + 'user_agent' => $request->header('User-Agent'), + 'endpoint' => $request->fullUrl(), + 'method' => $request->method(), + 'api_key_prefix' => substr($apiKey, 0, 8) . '...', + ]); + } + + return response()->json([ + 'success' => false, + 'message' => 'Invalid API key provided.', + 'error' => 'API_KEY_INVALID' + ], 401); + } + + // Log successful API key usage + if (config('api_security.log_api_key_usage', true)) { + Log::info('API key authenticated', [ + 'ip' => $request->ip(), + 'user_agent' => $request->header('User-Agent'), + 'endpoint' => $request->fullUrl(), + 'method' => $request->method(), + 'api_key_prefix' => substr($apiKey, 0, 8) . '...', + ]); + } + + // Add API key info to request for downstream use + $request->merge(['_api_key' => $apiKey]); + + return $next($request); + } + + /** + * Extract API key from request headers + */ + private function extractApiKey(Request $request): ?string + { + // Check multiple header formats + $apiKeyHeaders = config('api_security.api_key_headers', [ + 'X-API-Key', + 'API-Key', + 'Authorization' + ]); + + foreach ($apiKeyHeaders as $header) { + $value = $request->header($header); + + if ($value) { + // Handle Bearer token format + if ($header === 'Authorization' && preg_match('/Bearer\s+(.*)$/i', $value, $matches)) { + return $matches[1]; + } + + // Direct API key + return $value; + } + } + + return null; + } + + /** + * Validate API key against configured valid keys + */ + private function isValidApiKey(string $apiKey): bool + { + $validApiKeys = config('api_security.valid_api_keys', []); + + if (empty($validApiKeys)) { + return false; + } + + return in_array($apiKey, $validApiKeys); + } +} diff --git a/be/app/Http/Middleware/AuthenticateFromCookie.php b/be/app/Http/Middleware/AuthenticateFromCookie.php new file mode 100644 index 0000000..7bb3eab --- /dev/null +++ b/be/app/Http/Middleware/AuthenticateFromCookie.php @@ -0,0 +1,27 @@ +bearerToken()) { + $token = $request->cookie(AuthCookie::name()); + + if (is_string($token) && $token !== '') { + $request->headers->set('Authorization', 'Bearer '.$token); + } + } + + return $next($request); + } +} diff --git a/be/app/Http/Middleware/BlockApiToolsMiddleware.php b/be/app/Http/Middleware/BlockApiToolsMiddleware.php new file mode 100644 index 0000000..1ba8126 --- /dev/null +++ b/be/app/Http/Middleware/BlockApiToolsMiddleware.php @@ -0,0 +1,157 @@ +is('api/external*')) { + return $next($request); + } + + // Only block in production environment + if (app()->environment('production')) { + $userAgent = strtolower($request->header('User-Agent', '')); + $clientIp = $request->ip(); + + // Check if request has valid API key - bypass blocking for external systems + if ($this->hasValidApiKey($request)) { + return $next($request); + } + + // Check if IP is in allowed list + $allowedIps = config('api_security.allowed_ips', []); + if (!empty($allowedIps) && in_array($clientIp, $allowedIps)) { + return $next($request); + } + + // Check if User-Agent is in allowed list + $allowedUserAgents = config('api_security.allowed_user_agents', []); + foreach ($allowedUserAgents as $allowedAgent) { + if (str_contains($userAgent, strtolower($allowedAgent))) { + return $next($request); + } + } + + // Check if the User-Agent matches any blocked patterns + $blockedUserAgents = config('api_security.blocked_user_agents', []); + foreach ($blockedUserAgents as $blockedAgent) { + if (str_contains($userAgent, $blockedAgent)) { + return response()->json([ + 'success' => false, + 'message' => config('api_security.blocked_message', 'API access is restricted in production environment. Please use the web interface.'), + 'error' => 'API_TOOLS_BLOCKED' + ], 403); + } + } + + // Additional check for requests without proper browser User-Agent + // This catches tools that might not be in our list but don't look like browsers + if ($this->isSuspiciousUserAgent($userAgent)) { + return response()->json([ + 'success' => false, + 'message' => config('api_security.blocked_message', 'API access is restricted in production environment. Please use the web interface.'), + 'error' => 'API_TOOLS_BLOCKED' + ], 403); + } + } + + return $next($request); + } + + /** + * Check if the User-Agent looks suspicious (not a real browser) + */ + private function isSuspiciousUserAgent(string $userAgent): bool + { + $minLength = config('api_security.min_user_agent_length', 10); + + // If User-Agent is empty or very short, it's suspicious + if (empty($userAgent) || strlen($userAgent) < $minLength) { + return true; + } + + // Check for common browser patterns + $browserPatterns = [ + 'mozilla', + 'chrome', + 'safari', + 'firefox', + 'edge', + 'opera', + 'webkit', + 'gecko', + 'trident', + 'msie', + ]; + + $hasBrowserPattern = false; + foreach ($browserPatterns as $pattern) { + if (str_contains($userAgent, $pattern)) { + $hasBrowserPattern = true; + break; + } + } + + // If no browser pattern is found, it's likely an API tool + return !$hasBrowserPattern; + } + + /** + * Check if the request has a valid API key for external system access + */ + private function hasValidApiKey(Request $request): bool + { + // Check for API key in headers (X-API-Key, Authorization Bearer, or API-Key) + $apiKey = $request->header('X-API-Key') + ?? $request->header('API-Key') + ?? $this->extractBearerToken($request->header('Authorization')); + + if (!$apiKey) { + return false; + } + + // Get valid API keys from configuration + $validApiKeys = config('api_security.valid_api_keys', []); + + // If no API keys configured, return false + if (empty($validApiKeys)) { + return false; + } + + // Check if the provided API key is valid + return in_array($apiKey, $validApiKeys); + } + + /** + * Extract Bearer token from Authorization header + */ + private function extractBearerToken(?string $authorization): ?string + { + if (!$authorization) { + return null; + } + + if (preg_match('/Bearer\s+(.*)$/i', $authorization, $matches)) { + return $matches[1]; + } + + return null; + } +} diff --git a/be/app/Http/Middleware/SingleSessionMiddleware.php b/be/app/Http/Middleware/SingleSessionMiddleware.php new file mode 100644 index 0000000..323244b --- /dev/null +++ b/be/app/Http/Middleware/SingleSessionMiddleware.php @@ -0,0 +1,279 @@ +user(); + + if ($user) { + try { + $this->enforceSingleSession($user, $request); + } catch (\Throwable $e) { + // Log error but don't block the request + Log::error('SingleSessionMiddleware error', [ + 'user_id' => $user->id, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + } + } + + return $next($request); + } + + /** + * Enforce single session per user + */ + private function enforceSingleSession(User $user, Request $request): void + { + if (app()->environment('local')) { + return; + } + + // Skip single session enforcement during impersonation + if ($this->isImpersonationRequest($request)) { + return; + } + + // Skip single session enforcement for post-impersonation requests + if ($this->isPostImpersonationRequest($request)) { + return; + } + + // Get current token from request (Bearer header or HttpOnly cookie) + $currentToken = $this->resolveToken($request); + if (!$currentToken) { + return; + } + + // Parse the token to get the token ID and hash + $tokenParts = explode('|', $currentToken); + if (count($tokenParts) !== 2) { + return; + } + + $tokenId = $tokenParts[0]; + $tokenHash = $tokenParts[1]; + + // Find current token record with caching + $currentTokenRecord = $this->getTokenRecord($tokenId, $tokenHash); + + if (!$currentTokenRecord) { + return; + } + + // Check if this is an impersonation token - skip enforcement + if ($currentTokenRecord->name === 'impersonation-token') { + return; + } + + // Skip enforcement for fresh login tokens (SSO login or fresh auth-token) + if ($this->isFreshLoginToken($currentTokenRecord, $user)) { + return; + } + + // Use atomic cache lock per user to prevent race conditions + $lockKey = "single_session_user_{$user->id}"; + $lock = Cache::lock($lockKey, self::PROCESSING_LOCK_TTL); + + if (!$lock->get()) { + return; + } + + try { + // Get all active tokens for this user (excluding impersonation), ordered by newest first + $activeTokens = PersonalAccessToken::where('tokenable_type', get_class($user)) + ->where('tokenable_id', $user->id) + ->where('name', '!=', 'impersonation-token') + ->where(function ($query) { + $query->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }) + ->orderByDesc('created_at') + ->orderByDesc('id') + ->get(); + + // Keep only the most recently created token (latest login); revoke all others + if ($activeTokens->count() > 1) { + $latestToken = $activeTokens->first(); + $idsToDelete = $activeTokens->where('id', '!=', $latestToken->id)->pluck('id'); + + PersonalAccessToken::whereIn('id', $idsToDelete)->delete(); + + $this->notifyConcurrentSession($user, $request); + } + } finally { + $lock->release(); + } + } + + /** + * Get token record with caching + */ + private function getTokenRecord(string $tokenId, string $tokenHash): ?PersonalAccessToken + { + $cacheKey = "token_record_{$tokenId}"; + + return Cache::remember($cacheKey, self::TOKEN_NAME_CACHE_TTL, function () use ($tokenId, $tokenHash) { + return PersonalAccessToken::where('id', $tokenId) + ->where('token', hash('sha256', $tokenHash)) + ->first(); + }); + } + + /** + * Check if this is an impersonation-related request + */ + private function isImpersonationRequest(Request $request): bool + { + $path = $request->path(); + + // Check if the request is to impersonation endpoints + if (str_contains($path, 'impersonate')) { + return true; + } + + // Check if there's an impersonation header + if ($request->hasHeader('X-Original-User-Id')) { + return true; + } + + // Check if the request has impersonation token + $currentToken = $this->resolveToken($request); + if ($currentToken) { + $tokenParts = explode('|', $currentToken); + if (count($tokenParts) === 2) { + $tokenId = $tokenParts[0]; + $tokenHash = $tokenParts[1]; + + // Use cached token record to avoid duplicate queries + $tokenRecord = $this->getTokenRecord($tokenId, $tokenHash); + + if ($tokenRecord && $tokenRecord->name === 'impersonation-token') { + return true; + } + } + } + + return false; + } + + /** + * Check if this is a post-impersonation request (after leaving impersonation) + */ + private function isPostImpersonationRequest(Request $request): bool + { + $path = $request->path(); + + // Check if this is a request after leaving impersonation + // Look for requests that have X-Original-User-Id header but are not impersonation endpoints + if ($request->hasHeader('X-Original-User-Id') && !str_contains($path, 'impersonate')) { + return true; + } + + return false; + } + + /** + * Check if the current token is a fresh login token (SSO login or post-impersonation) + * This prevents false positives when a user legitimately logs in + */ + private function isFreshLoginToken(PersonalAccessToken $currentTokenRecord, User $user): bool + { + // Check if token was created recently (within threshold) + $tokenAge = $currentTokenRecord->created_at->diffInSeconds(now()); + + if ($tokenAge > self::FRESH_TOKEN_AGE) { + return false; // Token is not fresh + } + + // For SSO tokens, if they're fresh, skip enforcement (legitimate login) + if ($currentTokenRecord->name === 'sso-token') { + return true; + } + + // Check for both 'authToken' (camelCase - regular login) and 'auth-token' (kebab-case - post-impersonation) + $isAuthToken = in_array($currentTokenRecord->name, ['authToken', 'auth-token'], true); + + if ($isAuthToken) { + // Check if there are any other auth tokens for this user that were created before this one + // When logging out properly, all tokens should be deleted, so if there are no older + // auth tokens, this is likely a fresh login or post-impersonation + $olderAuthTokens = PersonalAccessToken::where('tokenable_type', get_class($user)) + ->where('tokenable_id', $user->id) + ->where('id', '!=', $currentTokenRecord->id) + ->whereIn('name', ['authToken', 'auth-token']) // Check for both naming conventions + ->where('created_at', '<', $currentTokenRecord->created_at) + ->where(function ($query) { + $query->whereNull('expires_at') + ->orWhere('expires_at', '>', now()); + }) + ->exists(); // Use exists() instead of count() for better performance + + // If there are no older auth tokens, this is a fresh login or post-impersonation + if (!$olderAuthTokens) { + return true; + } + } + + return false; + } + + /** + * Notify user about concurrent session + */ + private function notifyConcurrentSession(User $user, Request $request): void + { + // Notification creation removed per user request + // Concurrent session detection still works, but no notification is created + } + + /** + * Resolve Sanctum plain-text token from Authorization header or auth cookie. + */ + private function resolveToken(Request $request): ?string + { + $token = $request->bearerToken(); + + if (is_string($token) && $token !== '') { + return $token; + } + + $cookieToken = $request->cookie(AuthCookie::name()); + + return is_string($cookieToken) && $cookieToken !== '' ? $cookieToken : null; + } +} diff --git a/be/app/Jobs/CaptureKJCHistoricalDataJob.php b/be/app/Jobs/CaptureKJCHistoricalDataJob.php new file mode 100644 index 0000000..2d71057 --- /dev/null +++ b/be/app/Jobs/CaptureKJCHistoricalDataJob.php @@ -0,0 +1,102 @@ +month = $month; + $this->year = $year; + + // Set queue name for Horizon monitoring + $this->onQueue('kjc-historical-data'); + } + + /** + * Execute the job. + */ + public function handle(KJCHistoricalDataService $kjcHistoricalService) + { + try { + Log::info('Starting KJC historical data capture job', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + ]); + + $success = $kjcHistoricalService->captureMonthlySnapshots($this->month, $this->year); + + if ($success) { + Log::info('KJC historical data capture job completed successfully', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + ]); + } else { + Log::error('KJC historical data capture job failed', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + ]); + + throw new \Exception('KJC historical data capture failed'); + } + + } catch (\Exception $e) { + Log::error('KJC historical data capture job exception', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + throw $e; + } + } + + /** + * Handle a job failure. + */ + public function failed(\Throwable $exception) + { + Log::error('KJC historical data capture job failed permanently', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + 'error' => $exception->getMessage(), + ]); + } + + /** + * Get the tags that should be assigned to the job. + */ + public function tags() + { + return ['kjc-historical-data', "month-{$this->month}", "year-{$this->year}"]; + } +} diff --git a/be/app/Jobs/CapturePKJHistoricalDataJob.php b/be/app/Jobs/CapturePKJHistoricalDataJob.php new file mode 100644 index 0000000..eee5f0d --- /dev/null +++ b/be/app/Jobs/CapturePKJHistoricalDataJob.php @@ -0,0 +1,102 @@ +month = $month; + $this->year = $year; + + // Set queue name for Horizon monitoring + $this->onQueue('pkj-historical-data'); + } + + /** + * Execute the job. + */ + public function handle(PKJHistoricalDataService $historicalService) + { + try { + Log::info('Starting PKJ historical data capture job', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + ]); + + $success = $historicalService->captureMonthlySnapshots($this->month, $this->year); + + if ($success) { + Log::info('PKJ historical data capture job completed successfully', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + ]); + } else { + Log::error('PKJ historical data capture job failed', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + ]); + + throw new \Exception('PKJ historical data capture failed'); + } + + } catch (\Exception $e) { + Log::error('PKJ historical data capture job exception', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + throw $e; + } + } + + /** + * Handle a job failure. + */ + public function failed(\Throwable $exception) + { + Log::error('PKJ historical data capture job failed permanently', [ + 'month' => $this->month, + 'year' => $this->year, + 'job_id' => $this->job ? $this->job->getJobId() : 'manual-execution', + 'error' => $exception->getMessage(), + ]); + } + + /** + * Get the tags that should be assigned to the job. + */ + public function tags() + { + return ['pkj-historical-data', "month-{$this->month}", "year-{$this->year}"]; + } +} diff --git a/be/app/Models/Country.php b/be/app/Models/Country.php new file mode 100644 index 0000000..431c4bf --- /dev/null +++ b/be/app/Models/Country.php @@ -0,0 +1,12 @@ +belongsTo(Role::class, 'active_role_id'); + } +} diff --git a/be/app/Policies/ImpersonatePolicy.php b/be/app/Policies/ImpersonatePolicy.php new file mode 100644 index 0000000..9c4d6aa --- /dev/null +++ b/be/app/Policies/ImpersonatePolicy.php @@ -0,0 +1,49 @@ +hasRole('DEVELOPER')) { + return true; + } + + return $user->hasPermissionTo('menyamar pengguna'); + } + + /** + * Determine whether the user can impersonate a specific target user. + */ + public function impersonateUser(User $user, User $targetUser): bool + { + // DEVELOPER cannot be impersonated by anyone + if ($targetUser->hasRole('DEVELOPER')) { + return false; + } + + // DEVELOPER role bypasses all permission checks + if ($user->hasRole('DEVELOPER')) { + return true; + } + + // Check if user has permission to impersonate + if (!$user->hasPermissionTo('menyamar pengguna')) { + return false; + } + + // Users with permission can impersonate other users (except DEVELOPER) + return true; + } +} + diff --git a/be/app/Providers/AppServiceProvider.php b/be/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..489fa86 --- /dev/null +++ b/be/app/Providers/AppServiceProvider.php @@ -0,0 +1,34 @@ +app->register(AuthServiceProvider::class); + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); + + // Register impersonate policy + Gate::define('impersonate', [ImpersonatePolicy::class, 'impersonate']); + Gate::define('impersonate.user', [ImpersonatePolicy::class, 'impersonateUser']); + } +} diff --git a/be/app/Providers/FortifyServiceProvider.php b/be/app/Providers/FortifyServiceProvider.php new file mode 100644 index 0000000..f9abbaa --- /dev/null +++ b/be/app/Providers/FortifyServiceProvider.php @@ -0,0 +1,92 @@ +app->singleton(LoginResponse::class, ApiLoginResponse::class); + $this->app->singleton(LogoutResponse::class, ApiLogoutResponse::class); + $this->app->singleton(RegisterResponse::class, ApiRegisterResponse::class); + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + Fortify::createUsersUsing(CreateNewUser::class); + Fortify::updateUserProfileInformationUsing(UpdateUserProfileInformation::class); + Fortify::updateUserPasswordsUsing(UpdateUserPassword::class); + Fortify::resetUserPasswordsUsing(ResetUserPassword::class); + Fortify::redirectUserForTwoFactorAuthenticationUsing(RedirectIfTwoFactorAuthenticatable::class); + + RateLimiter::for('login', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + + RateLimiter::for('two-factor', function (Request $request) { + return Limit::perMinute(5)->by($request->session()->get('login.id')); + }); + + RateLimiter::for('email-verification', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input('email', '')).'|'.$request->ip()); + + return Limit::perMinute(5)->by($throttleKey); + }); + + RateLimiter::for('email-verification-resend', function (Request $request) { + $throttleKey = Str::transliterate(Str::lower($request->input('email', '')).'|'.$request->ip()); + + return Limit::perMinute(1)->by($throttleKey); + }); + + Fortify::authenticateUsing(function (Request $request) { + $user = User::where('email', $request->email)->first(); + + // Bypass password check in local or development environments + $shouldBypassPassword = config('app.env', 'local'); + + if ($user && ($shouldBypassPassword || Hash::check($request->password, $user->password))) { + if (! $user->hasVerifiedEmail()) { + return $user; + } + + if (! $user->canAuthenticate()) { + throw ValidationException::withMessages([ + 'email' => [$user->getLoginRestrictionMessage()], + ]); + } + + return $user; + } + }); + } +} diff --git a/be/app/Providers/HorizonServiceProvider.php b/be/app/Providers/HorizonServiceProvider.php new file mode 100644 index 0000000..b5a6268 --- /dev/null +++ b/be/app/Providers/HorizonServiceProvider.php @@ -0,0 +1,32 @@ + true); + } +} diff --git a/be/app/Providers/TelescopeServiceProvider.php b/be/app/Providers/TelescopeServiceProvider.php new file mode 100644 index 0000000..470ef17 --- /dev/null +++ b/be/app/Providers/TelescopeServiceProvider.php @@ -0,0 +1,64 @@ +hideSensitiveRequestDetails(); + + $isLocal = $this->app->environment('local'); + + Telescope::filter(function (IncomingEntry $entry) use ($isLocal) { + return $isLocal || + $entry->isReportableException() || + $entry->isFailedRequest() || + $entry->isFailedJob() || + $entry->isScheduledTask() || + $entry->hasMonitoredTag(); + }); + } + + /** + * Prevent sensitive request details from being logged by Telescope. + */ + protected function hideSensitiveRequestDetails(): void + { + if ($this->app->environment('local')) { + return; + } + + Telescope::hideRequestParameters(['_token']); + + Telescope::hideRequestHeaders([ + 'cookie', + 'x-csrf-token', + 'x-xsrf-token', + ]); + } + + /** + * Register the Telescope gate. + * + * This gate determines who can access Telescope in non-local environments. + */ + protected function gate(): void + { + Gate::define('viewTelescope', function ($user) { + return in_array($user->email, [ + // + ]); + }); + } +} diff --git a/be/app/Services/ActiveRoleService.php b/be/app/Services/ActiveRoleService.php new file mode 100644 index 0000000..6652829 --- /dev/null +++ b/be/app/Services/ActiveRoleService.php @@ -0,0 +1,131 @@ +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|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 + */ + 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'), + ]; + } +} diff --git a/be/app/Services/ActivityLogger.php b/be/app/Services/ActivityLogger.php new file mode 100644 index 0000000..e775ab2 --- /dev/null +++ b/be/app/Services/ActivityLogger.php @@ -0,0 +1,92 @@ +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'); + } +} diff --git a/be/app/Services/ContactService.php b/be/app/Services/ContactService.php new file mode 100644 index 0000000..e98d14a --- /dev/null +++ b/be/app/Services/ContactService.php @@ -0,0 +1,198 @@ +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; + } + } +} diff --git a/be/app/Services/DocumentService.php b/be/app/Services/DocumentService.php new file mode 100644 index 0000000..4aaaee8 --- /dev/null +++ b/be/app/Services/DocumentService.php @@ -0,0 +1,174 @@ +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; + } +} diff --git a/be/app/Services/OnlineUsersService.php b/be/app/Services/OnlineUsersService.php new file mode 100644 index 0000000..8d6a3a4 --- /dev/null +++ b/be/app/Services/OnlineUsersService.php @@ -0,0 +1,212 @@ +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; + } +} diff --git a/be/app/Services/SocialMediaService.php b/be/app/Services/SocialMediaService.php new file mode 100644 index 0000000..433dc8b --- /dev/null +++ b/be/app/Services/SocialMediaService.php @@ -0,0 +1,193 @@ +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; + } + } +} diff --git a/be/app/Services/VisibilityService.php b/be/app/Services/VisibilityService.php new file mode 100644 index 0000000..dad6050 --- /dev/null +++ b/be/app/Services/VisibilityService.php @@ -0,0 +1,304 @@ +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 []; + } +} + diff --git a/be/app/Support/AuthCookie.php b/be/app/Support/AuthCookie.php new file mode 100644 index 0000000..2d7704e --- /dev/null +++ b/be/app/Support/AuthCookie.php @@ -0,0 +1,96 @@ +withCookie(self::make($plainTextToken)); + } + + public static function clearAuthCookies(JsonResponse $response): JsonResponse + { + return $response + ->withCookie(self::forget()) + ->withCookie(self::forgetOriginalUserId()); + } + + public static function shouldExposeTokenInResponse(): bool + { + return (bool) config('auth_cookie.expose_token_in_response', false); + } +} diff --git a/be/app/Traits/HasDocuments.php b/be/app/Traits/HasDocuments.php new file mode 100644 index 0000000..e4580a3 --- /dev/null +++ b/be/app/Traits/HasDocuments.php @@ -0,0 +1,91 @@ +morphMany(Document::class, 'documentable'); + } + + /** + * Get documents of a specific type. + */ + public function documentsOfType($type) + { + return $this->documents()->where('document_type', $type); + } + + /** + * Upload a document for this model. + */ + public function uploadDocument(UploadedFile $file, $documentType = 'general', $description = null) + { + // Generate unique filename + $fileName = time().'_'.$file->getClientOriginalName(); + + // Store file in a folder named after the model + $folderName = strtolower(class_basename($this)); + $filePath = $file->storeAs("documents/{$folderName}", $fileName); + + // Create document record + return $this->documents()->create([ + '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 by ID. + */ + public function deleteDocument($documentId) + { + $document = $this->documents()->findOrFail($documentId); + + return $document->delete(); + } + + /** + * Get documents count. + */ + public function getDocumentsCountAttribute() + { + return $this->documents()->count(); + } + + /** + * Get documents count by type. + */ + public function getDocumentsCountByType($type) + { + return $this->documentsOfType($type)->count(); + } + + /** + * Check if model has documents. + */ + public function hasDocuments() + { + return $this->documents()->exists(); + } + + /** + * Check if model has documents of specific type. + */ + public function hasDocumentsOfType($type) + { + return $this->documentsOfType($type)->exists(); + } +} diff --git a/be/app/Traits/HasVisibility.php b/be/app/Traits/HasVisibility.php new file mode 100644 index 0000000..1d42fd0 --- /dev/null +++ b/be/app/Traits/HasVisibility.php @@ -0,0 +1,58 @@ +includeUnitsWithoutFormation + : false; + + // Check if model has a direct unit column + $unitColumn = property_exists($this, 'unitColumn') ? $this->unitColumn : 'unit_id'; + + // Check if model has an indirect unit relationship + $unitRelationship = property_exists($this, 'unitRelationship') ? $this->unitRelationship : null; + + if ($unitRelationship) { + return $this->getVisibilityService()->applyVisibilityToIndirectQuery( + $query, + $user, + $unitRelationship, + $includeUnitsWithoutFormation + ); + } + + return $this->getVisibilityService()->applyVisibilityToQuery( + $query, + $user, + $unitColumn, + $includeUnitsWithoutFormation + ); + } + + /** + * 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 + * + * @deprecated Use VisibilityService::getUnitsUnderGovernment() instead + */ + protected function getUnitsUnderGovernment($userUnit) + { + return $this->getVisibilityService()->getUnitsUnderGovernment($userUnit); + } +} \ No newline at end of file diff --git a/be/app/Traits/HttpClientTrait.php b/be/app/Traits/HttpClientTrait.php new file mode 100644 index 0000000..0177084 --- /dev/null +++ b/be/app/Traits/HttpClientTrait.php @@ -0,0 +1,104 @@ +withoutVerifying(); + } + + return $client; + } + + /** + * Get a configured HTTP client with authentication. + * + * @param string $token Bearer token for authentication + * @param array $additionalHeaders Additional headers to add + * @param bool $withoutVerifying Whether to skip SSL verification + * @return \Illuminate\Http\Client\PendingRequest + */ + protected function getAuthenticatedHttpClient(string $token, array $additionalHeaders = [], bool $withoutVerifying = false) + { + $headers = array_merge([ + 'Authorization' => 'Bearer '.$token, + 'Accept' => 'application/json', + 'Content-Type' => 'application/json', + ], $additionalHeaders); + + return $this->getHttpClient($headers, $withoutVerifying); + } + + /** + * Log HTTP request details for debugging. + * + * @param string $method HTTP method + * @param string $url Request URL + * @param array $data Request data + * @param array $headers Request headers + */ + protected function logHttpRequest(string $method, string $url, array $data = [], array $headers = []) + { + Log::debug('HTTP Request', [ + 'method' => $method, + 'url' => $url, + 'data' => $data, + 'headers' => array_keys($headers), // Don't log sensitive header values + 'environment' => config('app.env'), + 'proxy_enabled' => $this->isProxyEnabled(), + ]); + } + + /** + * Log HTTP response details for debugging. + * + * @param \Illuminate\Http\Client\Response $response + * @param string $context Additional context + */ + protected function logHttpResponse($response, string $context = '') + { + Log::debug('HTTP Response'.($context ? " - {$context}" : ''), [ + 'status' => $response->status(), + 'successful' => $response->successful(), + 'body_length' => strlen($response->body()), + 'headers' => $response->headers(), + ]); + } + + /** + * Check if proxy is enabled for current environment. + */ + protected function isProxyEnabled(): bool + { + $config = config('http'); + $proxyConfig = $config['proxy'] ?? []; + + return isset($proxyConfig['enabled']) && $proxyConfig['enabled'] && + in_array(config('app.env'), $proxyConfig['environments'] ?? []); + } + + /** + * Get current proxy configuration. + */ + protected function getProxyConfig(): array + { + $config = config('http'); + + return $config['proxy'] ?? []; + } +} diff --git a/be/app/Traits/NotifiesAdmins.php b/be/app/Traits/NotifiesAdmins.php new file mode 100644 index 0000000..0b808c8 --- /dev/null +++ b/be/app/Traits/NotifiesAdmins.php @@ -0,0 +1,55 @@ +whereIn('name', ['PENTADBIR', 'DEVELOPER']); + })->get(); + } + + /** + * Merge admin users with specific role users and return unique collection + */ + protected function mergeWithAdmins(Collection $specificUsers): Collection + { + $adminUsers = $this->getAdminUsers(); + return $specificUsers->merge($adminUsers)->unique('id'); + } + + /** + * Get users with specific roles and merge with admins + */ + protected function getUsersWithRolesAndAdmins(array $roles): Collection + { + $specificUsers = User::whereHas('roles', function ($query) use ($roles) { + $query->whereIn('name', $roles); + })->get(); + + return $this->mergeWithAdmins($specificUsers); + } + + /** + * Get user IDs from a collection and merge with admin user IDs + */ + protected function mergeUserIdsWithAdmins($userIds): Collection + { + $adminIds = $this->getAdminUsers()->pluck('id'); + + if ($userIds instanceof Collection) { + return $userIds->merge($adminIds)->unique()->filter(); + } + + return collect($userIds)->merge($adminIds)->unique()->filter(); + } +} + diff --git a/be/artisan b/be/artisan new file mode 100755 index 0000000..c35e31d --- /dev/null +++ b/be/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/be/bootstrap/app.php b/be/bootstrap/app.php new file mode 100644 index 0000000..7746dd5 --- /dev/null +++ b/be/bootstrap/app.php @@ -0,0 +1,124 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + apiPrefix: '', + // health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // HttpOnly cookie → Bearer for Sanctum (SPA on mykopkb.com → api.mykopkb.com) + $middleware->api(prepend: [ + \App\Http\Middleware\AuthenticateFromCookie::class, + ]); + + // Single session enforcement for authenticated routes + $middleware->alias([ + 'single.session' => \App\Http\Middleware\SingleSessionMiddleware::class, + 'block.api.tools' => \App\Http\Middleware\BlockApiToolsMiddleware::class, + 'api.key.auth' => \App\Http\Middleware\ApiKeyAuthenticationMiddleware::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions): void { + // Customize authorization exception message to Malay + $exceptions->render(function (Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException $e, $request) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Tindakan ini tidak dibenarkan.', + ], 403); + } + }); + + // Also handle AuthorizationException for backward compatibility + $exceptions->render(function (Illuminate\Auth\Access\AuthorizationException $e, $request) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Tindakan ini tidak dibenarkan.', + ], 403); + } + }); + + // Handle Model Not Found exceptions (404 errors) + $exceptions->render(function (Illuminate\Database\Eloquent\ModelNotFoundException $e, $request) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Data tidak dijumpai.', + ], 404); + } + }); + + // Handle Validation exceptions (422 errors) + $exceptions->render(function (Illuminate\Validation\ValidationException $e, $request) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Data tidak sah.', + 'errors' => $e->errors(), + ], 422); + } + }); + + // Handle Authentication exceptions (401 errors) + $exceptions->render(function (Illuminate\Auth\AuthenticationException $e, $request) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Anda perlu log masuk terlebih dahulu.', + ], 401); + } + return redirect()->guest('/login'); + }); + + // Handle Method Not Allowed exceptions (405 errors) + $exceptions->render(function (Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException $e, $request) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Kaedah HTTP tidak dibenarkan.', + ], 405); + } + }); + + // Handle Route Not Found exceptions (404 errors) + $exceptions->render(function (Symfony\Component\HttpKernel\Exception\NotFoundHttpException $e, $request) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Halaman tidak dijumpai.', + ], 404); + } + }); + + // Handle Too Many Requests exceptions (429 errors) + $exceptions->render(function (Illuminate\Http\Exceptions\ThrottleRequestsException $e, $request) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'message' => 'Terlalu banyak permintaan. Sila cuba lagi nanti.', + ], 429); + } + }); + + // Handle general exceptions (500 errors) - only in production + $exceptions->render(function (Throwable $e, $request) { + if ($request->expectsJson()) { + $message = app()->environment('production') + ? 'Ralat dalaman pelayan. Sila hubungi pentadbir sistem.' + : $e->getMessage(); + + return response()->json([ + 'success' => false, + 'message' => $message, + ], 500); + } + }); + })->create(); diff --git a/be/bootstrap/cache/.gitignore b/be/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/be/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/be/bootstrap/providers.php b/be/bootstrap/providers.php new file mode 100644 index 0000000..8dc618d --- /dev/null +++ b/be/bootstrap/providers.php @@ -0,0 +1,8 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dompdf/dompdf", + "version": "v3.1.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "b3493e35d31a5e76ec24c3b64a29b0034b2f32a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/b3493e35d31a5e76ec24c3b64a29b0034b2f32a6", + "reference": "b3493e35d31a5e76ec24c3b64a29b0034b2f32a6", + "shasum": "" + }, + "require": { + "dompdf/php-font-lib": "^1.0.0", + "dompdf/php-svg-lib": "^1.0.0", + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v3.1.2" + }, + "time": "2025-09-23T03:06:41+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "reference": "6137b7d4232b7f16c882c75e4ca3991dbcf6fe2d", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3 || ^4 || ^5 || ^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "The FontLib Community", + "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/dompdf/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.1" + }, + "time": "2024-12-02T14:37:59+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "reference": "eb045e518185298eb6ff8d80d0d0c6b17aecd9af", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.0" + }, + "time": "2024-04-29T13:26:35+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "8c784d071debd117328803d86b2097615b457500" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/8c784d071debd117328803d86b2097615b457500", + "reference": "8c784d071debd117328803d86b2097615b457500", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2024-10-09T13:47:03+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "ezyang/htmlpurifier", + "version": "v4.18.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "cb56001e54359df7ae76dc522d08845dc741621b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/cb56001e54359df7ae76dc522d08845dc741621b", + "reference": "cb56001e54359df7ae76dc522d08845dc741621b", + "shasum": "" + }, + "require": { + "php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "cerdic/css-tidy": "^1.7 || ^2.0", + "simpletest/simpletest": "dev-master" + }, + "suggest": { + "cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.", + "ext-bcmath": "Used for unit conversion and imagecrash protection", + "ext-iconv": "Converts text to and from non-UTF-8 encodings", + "ext-tidy": "Used for pretty-printing HTML" + }, + "type": "library", + "autoload": { + "files": [ + "library/HTMLPurifier.composer.php" + ], + "psr-0": { + "HTMLPurifier": "library/" + }, + "exclude-from-classmap": [ + "/library/HTMLPurifier/Language/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "support": { + "issues": "https://github.com/ezyang/htmlpurifier/issues", + "source": "https://github.com/ezyang/htmlpurifier/tree/v4.18.0" + }, + "time": "2024-11-01T03:51:45+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/3d158f36e7875e2f040f37bc0573956240a5a38b", + "reference": "3d158f36e7875e2f040f37bc0573956240a5a38b", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0", + "symfony/http-foundation": "^4.4|^5.4|^6|^7" + }, + "require-dev": { + "phpstan/phpstan": "^1.4", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.3.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2023-10-12T05:21:21+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.3", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945", + "reference": "3ba905c11371512af9d9bdd27d99b782216b6945", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:45:45+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.10.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "reference": "b51ac707cfa420b7bfd4e4d5e510ba8008e822b4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.3", + "guzzlehttp/psr7": "^2.8", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.39 || ^9.6.20", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.10.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2025-08-23T22:36:01+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "481557b130ef3790cf82b713667b43030dc9c957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/481557b130ef3790cf82b713667b43030dc9c957", + "reference": "481557b130ef3790cf82b713667b43030dc9c957", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.3.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:34:08+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "21dc724a0583619cd1652f673303492272778051" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", + "reference": "21dc724a0583619cd1652f673303492272778051", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "0.9.0", + "phpunit/phpunit": "^8.5.44 || ^9.6.25" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.8.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2025-08-23T21:21:41+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, + { + "name": "lab404/laravel-impersonate", + "version": "1.7.7", + "source": { + "type": "git", + "url": "https://github.com/404labfr/laravel-impersonate.git", + "reference": "5033f3433a55ca8bb2cc3e4a018a39dd8a327a9f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/404labfr/laravel-impersonate/zipball/5033f3433a55ca8bb2cc3e4a018a39dd8a327a9f", + "reference": "5033f3433a55ca8bb2cc3e4a018a39dd8a327a9f", + "shasum": "" + }, + "require": { + "laravel/framework": "^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0 | ^11.0 | ^12.0", + "php": "^7.2 | ^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "^4.0 | ^5.0 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0", + "phpunit/phpunit": "^7.5 | ^8.0 | ^9.0 | ^10.0 | ^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Lab404\\Impersonate\\ImpersonateServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Lab404\\Impersonate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marceau Casals", + "email": "marceau@casals.fr" + } + ], + "description": "Laravel Impersonate is a plugin that allows to you to authenticate as your users.", + "keywords": [ + "auth", + "impersonate", + "impersonation", + "laravel", + "laravel-package", + "laravel-plugin", + "package", + "plugin", + "user" + ], + "support": { + "issues": "https://github.com/404labfr/laravel-impersonate/issues", + "source": "https://github.com/404labfr/laravel-impersonate/tree/1.7.7" + }, + "time": "2025-02-24T16:18:38+00:00" + }, + { + "name": "laravel/fortify", + "version": "v1.30.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/fortify.git", + "reference": "005f4d535ae671312d267d942b964807fc0ef6f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/fortify/zipball/005f4d535ae671312d267d942b964807fc0ef6f8", + "reference": "005f4d535ae671312d267d942b964807fc0ef6f8", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "ext-json": "*", + "illuminate/support": "^10.0|^11.0|^12.0", + "php": "^8.1", + "pragmarx/google2fa": "^8.0", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^8.16|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.4|^11.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Fortify\\FortifyServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Fortify\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Backend controllers and scaffolding for Laravel authentication.", + "keywords": [ + "auth", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/fortify/issues", + "source": "https://github.com/laravel/fortify" + }, + "time": "2025-08-29T20:15:47+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.29.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/a9e4c73086f5ba38383e9c1d74b84fe46aac730b", + "reference": "a9e4c73086f5ba38383e9c1d74b84fe46aac730b", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13|^0.14", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^3.2.1|^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^1.3|^2.0", + "league/commonmark": "^2.7", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "phiki/phiki": "v2.0.0", + "php": "^8.2", + "psr/container": "^1.1.1|^2.0.1", + "psr/log": "^1.0|^2.0|^3.0", + "psr/simple-cache": "^1.0|^2.0|^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.2.0", + "symfony/error-handler": "^7.2.0", + "symfony/finder": "^7.2.0", + "symfony/http-foundation": "^7.2.0", + "symfony/http-kernel": "^7.2.0", + "symfony/mailer": "^7.2.0", + "symfony/mime": "^7.2.0", + "symfony/polyfill-php83": "^1.33", + "symfony/polyfill-php84": "^1.33", + "symfony/polyfill-php85": "^1.33", + "symfony/process": "^7.2.0", + "symfony/routing": "^7.2.0", + "symfony/uid": "^7.2.0", + "symfony/var-dumper": "^7.2.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1|2.0", + "psr/log-implementation": "1.0|2.0|3.0", + "psr/simple-cache-implementation": "1.0|2.0|3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/psr7": "^2.4", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^10.6.5", + "pda/pheanstalk": "^5.0.6|^7.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^10.5.35|^11.5.3|^12.0.1", + "predis/predis": "^2.3|^3.0", + "resend/resend-php": "^0.10.0", + "symfony/cache": "^7.2.0", + "symfony/http-client": "^7.2.0", + "symfony/psr-http-message-bridge": "^7.2.0", + "symfony/translation": "^7.2.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0|^5.0|^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^10.5.35|^11.5.3|^12.0.1).", + "predis/predis": "Required to use the predis connector (^2.3|^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.2).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.2).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.2).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.2).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.2).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-09-16T14:15:03+00:00" + }, + { + "name": "laravel/horizon", + "version": "v5.36.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/horizon.git", + "reference": "eccc804c9da78064c97a8f506bb148f05c816409" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/horizon/zipball/eccc804c9da78064c97a8f506bb148f05c816409", + "reference": "eccc804c9da78064c97a8f506bb148f05c816409", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-pcntl": "*", + "ext-posix": "*", + "illuminate/contracts": "^9.21|^10.0|^11.0|^12.0", + "illuminate/queue": "^9.21|^10.0|^11.0|^12.0", + "illuminate/support": "^9.21|^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.17|^3.0", + "php": "^8.0", + "ramsey/uuid": "^4.0", + "symfony/console": "^6.0|^7.0", + "symfony/error-handler": "^6.0|^7.0", + "symfony/polyfill-php83": "^1.28", + "symfony/process": "^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.10|^2.0", + "phpunit/phpunit": "^9.0|^10.4|^11.5|^12.0", + "predis/predis": "^1.1|^2.0|^3.0" + }, + "suggest": { + "ext-redis": "Required to use the Redis PHP driver.", + "predis/predis": "Required when not using the Redis PHP driver (^1.1|^2.0|^3.0)." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Horizon": "Laravel\\Horizon\\Horizon" + }, + "providers": [ + "Laravel\\Horizon\\HorizonServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Horizon\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Dashboard and code-driven configuration for Laravel queues.", + "keywords": [ + "laravel", + "queue" + ], + "support": { + "issues": "https://github.com/laravel/horizon/issues", + "source": "https://github.com/laravel/horizon/tree/v5.36.0" + }, + "time": "2025-10-10T13:44:39+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.6", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "symfony/console": "^6.2|^7.0" + }, + "conflict": { + "illuminate/console": ">=10.17.0 <10.25.0", + "laravel/framework": ">=10.17.0 <10.25.0" + }, + "require-dev": { + "illuminate/collections": "^10.0|^11.0|^12.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4", + "phpstan/phpstan": "^1.11", + "phpstan/phpstan-mockery": "^1.1" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Add beautiful and user-friendly forms to your command-line applications.", + "support": { + "issues": "https://github.com/laravel/prompts/issues", + "source": "https://github.com/laravel/prompts/tree/v0.3.6" + }, + "time": "2025-07-07T14:17:42+00:00" + }, + { + "name": "laravel/sanctum", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sanctum.git", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sanctum/zipball/fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "reference": "fd6df4f79f48a72992e8d29a9c0ee25422a0d677", + "shasum": "" + }, + "require": { + "ext-json": "*", + "illuminate/console": "^11.0|^12.0", + "illuminate/contracts": "^11.0|^12.0", + "illuminate/database": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", + "php": "^8.2", + "symfony/console": "^7.0" + }, + "require-dev": { + "mockery/mockery": "^1.6", + "orchestra/testbench": "^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sanctum\\SanctumServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sanctum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.", + "keywords": [ + "auth", + "laravel", + "sanctum" + ], + "support": { + "issues": "https://github.com/laravel/sanctum/issues", + "source": "https://github.com/laravel/sanctum" + }, + "time": "2025-07-09T19:45:24+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "reference": "b352cf0534aa1ae6b4d825d1e762e35d43f8a841", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2025-03-19T13:51:03+00:00" + }, + { + "name": "laravel/tinker", + "version": "v2.10.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/22177cc71807d38f2810c6204d8f7183d88a57d3", + "reference": "22177cc71807d38f2810c6204d8f7183d88a57d3", + "shasum": "" + }, + "require": { + "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", + "php": "^7.2.5|^8.0", + "psy/psysh": "^0.11.1|^0.12.0", + "symfony/var-dumper": "^4.3.4|^5.0|^6.0|^7.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8.5.8|^9.3.3|^10.0" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v2.10.1" + }, + "time": "2025-01-27T14:24:01+00:00" + }, + { + "name": "league/commonmark", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca", + "reference": "10732241927d3971d28e7ea7b5712721fa2296ca", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2025-07-20T12:47:49+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2203e3151755d874bb2943649dae1eb8533ac93e", + "reference": "2203e3151755d874bb2943649dae1eb8533ac93e", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.30.0" + }, + "time": "2025-06-25T13:29:59+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.30.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/6691915f77c7fb69adfb87dcd550052dc184ee10", + "reference": "6691915f77c7fb69adfb87dcd550052dc184ee10", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.30.0" + }, + "time": "2025-05-21T10:34:19+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.5.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "81fb5145d2644324614cc532b28efd0215bda430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/81fb5145d2644324614cc532b28efd0215bda430", + "reference": "81fb5145d2644324614cc532b28efd0215bda430", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.5", + "php": "^8.1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "jeremykendall/php-domain-parser": "to resolve Public Suffix and Top Level Domain", + "league/uri-components": "Needed to easily manipulate URI objects components", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.5.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:40:02+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "reference": "08cfc6c4f3d811584fb09c37e2849e6a7f9b0742", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-factory": "^1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common interfaces and classes for URI representation and interaction", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.5.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2024-12-08T08:18:47+00:00" + }, + { + "name": "maatwebsite/excel", + "version": "3.1.67", + "source": { + "type": "git", + "url": "https://github.com/SpartnerNL/Laravel-Excel.git", + "reference": "e508e34a502a3acc3329b464dad257378a7edb4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/e508e34a502a3acc3329b464dad257378a7edb4d", + "reference": "e508e34a502a3acc3329b464dad257378a7edb4d", + "shasum": "" + }, + "require": { + "composer/semver": "^3.3", + "ext-json": "*", + "illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0||^12.0", + "php": "^7.0||^8.0", + "phpoffice/phpspreadsheet": "^1.30.0", + "psr/simple-cache": "^1.0||^2.0||^3.0" + }, + "require-dev": { + "laravel/scout": "^7.0||^8.0||^9.0||^10.0", + "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0||^10.0", + "predis/predis": "^1.1" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Excel": "Maatwebsite\\Excel\\Facades\\Excel" + }, + "providers": [ + "Maatwebsite\\Excel\\ExcelServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Maatwebsite\\Excel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Patrick Brouwers", + "email": "patrick@spartner.nl" + } + ], + "description": "Supercharged Excel exports and imports in Laravel", + "keywords": [ + "PHPExcel", + "batch", + "csv", + "excel", + "export", + "import", + "laravel", + "php", + "phpspreadsheet" + ], + "support": { + "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues", + "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.67" + }, + "funding": [ + { + "url": "https://laravel-excel.com/commercial-support", + "type": "custom" + }, + { + "url": "https://github.com/patrickbrouwers", + "type": "github" + } + ], + "time": "2025-08-26T09:13:16+00:00" + }, + { + "name": "maennchen/zipstream-php", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/9712d8fa4cdf9240380b01eb4be55ad8dcf71416", + "reference": "9712d8fa4cdf9240380b01eb4be55ad8dcf71416", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.3" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^12.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-07-17T11:15:13+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, + { + "name": "masterminds/html5", + "version": "2.10.0", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fcf91eb64359852f00d921887b219479b4f21251" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251", + "reference": "fcf91eb64359852f00d921887b219479b4f21251", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.0" + }, + "time": "2025-07-25T09:04:22+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.9.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", + "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2025-03-24T10:02:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.10.3", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "reference": "8e3643dcd149ae0fe1d2ff4f2c8e4bbfad7c165f", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-09-06T13:39:36+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.2", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/da801d52f0354f70a638673c4a0f04e16529431d", + "reference": "da801d52f0354f70a638673c4a0f04e16529431d", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.4" + }, + "require-dev": { + "nette/tester": "^2.5.2", + "phpstan/phpstan-nette": "^1.0", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.2" + }, + "time": "2024-10-06T23:10:23+00:00" + }, + { + "name": "nette/utils", + "version": "v4.0.8", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/tester": "^2.5", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.0.8" + }, + "time": "2025-08-06T21:43:34+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.6.1", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "reference": "f103601b29efebd7ff4a1ca7b3eeea9e3336a2a2", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.1" + }, + "time": "2025-08-13T20:13:15+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.2.6" + }, + "require-dev": { + "illuminate/console": "^11.44.7", + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.2", + "phpstan/phpstan": "^1.12.25", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.2.6", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2025-05-08T08:14:37+00:00" + }, + { + "name": "nwidart/laravel-modules", + "version": "v12.0.4", + "source": { + "type": "git", + "url": "https://github.com/nWidart/laravel-modules.git", + "reference": "6e1f50de63366206b06ec53bbc823282977ddd06" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nWidart/laravel-modules/zipball/6e1f50de63366206b06ec53bbc823282977ddd06", + "reference": "6e1f50de63366206b06ec53bbc823282977ddd06", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-simplexml": "*", + "php": ">=8.2", + "wikimedia/composer-merge-plugin": "^2.1" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^v3.52", + "laravel/framework": "^v12.0", + "laravel/pint": "^1.16", + "mockery/mockery": "^1.6", + "orchestra/testbench": "^v10.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5.3|^12.0.", + "spatie/phpunit-snapshot-assertions": "^5.0" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Module": "Nwidart\\Modules\\Facades\\Module" + }, + "providers": [ + "Nwidart\\Modules\\LaravelModulesServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "12.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Nwidart\\Modules\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Widart", + "email": "n.widart@gmail.com", + "homepage": "https://nicolaswidart.com", + "role": "Developer" + } + ], + "description": "Laravel Module management", + "keywords": [ + "laravel", + "module", + "modules", + "nwidart", + "rad" + ], + "support": { + "issues": "https://github.com/nWidart/laravel-modules/issues", + "source": "https://github.com/nWidart/laravel-modules/tree/v12.0.4" + }, + "funding": [ + { + "url": "https://github.com/dcblogdev", + "type": "github" + }, + { + "url": "https://github.com/nwidart", + "type": "github" + } + ], + "time": "2025-06-29T09:23:53+00:00" + }, + { + "name": "paragonie/constant_time_encoding", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/paragonie/constant_time_encoding.git", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/df1e7fde177501eee2037dd159cf04f5f301a512", + "reference": "df1e7fde177501eee2037dd159cf04f5f301a512", + "shasum": "" + }, + "require": { + "php": "^8" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "vimeo/psalm": "^4|^5" + }, + "type": "library", + "autoload": { + "psr-4": { + "ParagonIE\\ConstantTime\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com", + "role": "Maintainer" + }, + { + "name": "Steve 'Sc00bz' Thomas", + "email": "steve@tobtu.com", + "homepage": "https://www.tobtu.com", + "role": "Original Developer" + } + ], + "description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)", + "keywords": [ + "base16", + "base32", + "base32_decode", + "base32_encode", + "base64", + "base64_decode", + "base64_encode", + "bin2hex", + "encoding", + "hex", + "hex2bin", + "rfc4648" + ], + "support": { + "email": "info@paragonie.com", + "issues": "https://github.com/paragonie/constant_time_encoding/issues", + "source": "https://github.com/paragonie/constant_time_encoding" + }, + "time": "2024-05-08T12:36:18+00:00" + }, + { + "name": "phiki/phiki", + "version": "v2.0.0", + "source": { + "type": "git", + "url": "https://github.com/phikiphp/phiki.git", + "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phikiphp/phiki/zipball/461f6dd7e91dc3a95463b42f549ac7d0aab4702f", + "reference": "461f6dd7e91dc3a95463b42f549ac7d0aab4702f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/commonmark": "^2.5.3", + "php": "^8.2", + "psr/simple-cache": "^3.0" + }, + "require-dev": { + "illuminate/support": "^11.45", + "laravel/pint": "^1.18.1", + "orchestra/testbench": "^9.15", + "pestphp/pest": "^3.5.1", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^7.1.6" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Phiki\\Adapters\\Laravel\\PhikiServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Phiki\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ryan Chandler", + "email": "support@ryangjchandler.co.uk", + "homepage": "https://ryangjchandler.co.uk", + "role": "Developer" + } + ], + "description": "Syntax highlighting using TextMate grammars in PHP.", + "support": { + "issues": "https://github.com/phikiphp/phiki/issues", + "source": "https://github.com/phikiphp/phiki/tree/v2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sponsors/ryangjchandler", + "type": "github" + }, + { + "url": "https://buymeacoffee.com/ryangjchandler", + "type": "other" + } + ], + "time": "2025-08-28T18:20:27+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "1.30.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "2f39286e0136673778b7a142b3f0d141e43d1714" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/2f39286e0136673778b7a142b3f0d141e43d1714", + "reference": "2f39286e0136673778b7a142b3f0d141e43d1714", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.15", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^7.4 || ^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.0" + }, + "time": "2025-08-10T06:28:02+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.4", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "reference": "638a154f8d4ee6a5cfa96d6a34dfbe0cffa9566d", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-08-21T11:53:16+00:00" + }, + { + "name": "pragmarx/google2fa", + "version": "v8.0.3", + "source": { + "type": "git", + "url": "https://github.com/antonioribeiro/google2fa.git", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/antonioribeiro/google2fa/zipball/6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "reference": "6f8d87ebd5afbf7790bde1ffc7579c7c705e0fad", + "shasum": "" + }, + "require": { + "paragonie/constant_time_encoding": "^1.0|^2.0|^3.0", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.9", + "phpunit/phpunit": "^7.5.15|^8.5|^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PragmaRX\\Google2FA\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Antonio Carlos Ribeiro", + "email": "acr@antoniocarlosribeiro.com", + "role": "Creator & Designer" + } + ], + "description": "A One Time Password Authentication package, compatible with Google Authenticator.", + "keywords": [ + "2fa", + "Authentication", + "Two Factor Authentication", + "google2fa" + ], + "support": { + "issues": "https://github.com/antonioribeiro/google2fa/issues", + "source": "https://github.com/antonioribeiro/google2fa/tree/v8.0.3" + }, + "time": "2024-09-05T11:56:40+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.10", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "reference": "6e80abe6f2257121f1eb9a4c55bf29d921025b22", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2" + }, + "suggest": { + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-pdo-sqlite": "The doc command requires SQLite to work.", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.10" + }, + "time": "2025-08-04T12:39:37+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "reference": "81f941f6f729b1e3ceea61d9d014f8b6c6800440", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.1" + }, + "time": "2025-09-04T20:59:21+00:00" + }, + { + "name": "sabberworm/php-css-parser", + "version": "v8.9.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/d8e916507b88e389e26d4ab03c904a082aa66bb9", + "reference": "d8e916507b88e389e26d4ab03c904a082aa66bb9", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^5.6.20 || ^7.0.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" + }, + "require-dev": { + "phpunit/phpunit": "5.7.27 || 6.5.14 || 7.5.20 || 8.5.41", + "rawr/cross-data-providers": "^2.0.0" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v8.9.0" + }, + "time": "2025-07-11T13:20:48+00:00" + }, + { + "name": "spatie/laravel-activitylog", + "version": "4.10.2", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "bb879775d487438ed9a99e64f09086b608990c10" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/bb879775d487438ed9a99e64f09086b608990c10", + "reference": "bb879775d487438ed9a99e64f09086b608990c10", + "shasum": "" + }, + "require": { + "illuminate/config": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "illuminate/database": "^8.69 || ^9.27 || ^10.0 || ^11.0 || ^12.0", + "illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "php": "^8.1", + "spatie/laravel-package-tools": "^1.6.3" + }, + "require-dev": { + "ext-json": "*", + "orchestra/testbench": "^6.23 || ^7.0 || ^8.0 || ^9.0 || ^10.0", + "pestphp/pest": "^1.20 || ^2.0 || ^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Activitylog\\ActivitylogServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Activitylog\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev.gummibeer@gmail.com", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "A very simple activity logger to monitor the users of your website or application", + "homepage": "https://github.com/spatie/activitylog", + "keywords": [ + "activity", + "laravel", + "log", + "spatie", + "user" + ], + "support": { + "issues": "https://github.com/spatie/laravel-activitylog/issues", + "source": "https://github.com/spatie/laravel-activitylog/tree/4.10.2" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-06-15T06:59:49+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.92.7", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/php-code-coverage": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-07-17T15:46:43+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "6.21.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/6a118e8855dfffcd90403aab77bbf35a03db51b3", + "reference": "6a118e8855dfffcd90403aab77bbf35a03db51b3", + "shasum": "" + }, + "require": { + "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0", + "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "laravel/passport": "^11.0|^12.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0", + "phpunit/phpunit": "^9.4|^10.1|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "6.x-dev", + "dev-master": "6.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 8.0 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/6.21.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-07-23T16:08:05+00:00" + }, + { + "name": "symfony/clock", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "reference": "b81435fbd6648ea425d1ee96a2d8e68f4ceacd24", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/clock": "^1.0", + "symfony/polyfill-php83": "^1.28" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/console", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "reference": "cb0102a1c5ac3807cf3fdf8bea96007df7fdbea7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.2" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-25T06:35:40+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "reference": "601a5ce9aaad7bf10797e3663faefce9e26c24e2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "reference": "0b31a944fcd8759ae294da4d2808cbc53aebd0c3", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^6.4|^7.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/serializer": "^6.4|^7.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-07T08:17:57+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "reference": "b7dc69e71de420ac04bc9ab830cf3ffebba48191", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/error-handler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-13T11:49:31+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/2a6614966ba1074fa93dae0bc804227422df4dfe", + "reference": "2a6614966ba1074fa93dae0bc804227422df4dfe", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/7475561ec27020196c49bb7c4f178d33d7d3dc00", + "reference": "7475561ec27020196c49bb7c4f178d33d7d3dc00", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "doctrine/dbal": "<3.6", + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^3.6|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/clock": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/mime": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-20T08:04:18+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/72c304de37e1a1cec6d5d12b81187ebd4850a17b", + "reference": "72c304de37e1a1cec6d5d12b81187ebd4850a17b", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^7.3", + "symfony/http-foundation": "^7.3", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<6.4", + "symfony/cache": "<6.4", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<6.4", + "symfony/form": "<6.4", + "symfony/http-client": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<6.4", + "symfony/messenger": "<6.4", + "symfony/translation": "<6.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<6.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.4", + "twig/twig": "<3.12" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^6.4|^7.0", + "symfony/clock": "^6.4|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/css-selector": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^7.1", + "symfony/routing": "^6.4|^7.0", + "symfony/serializer": "^7.1", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/translation": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^6.4|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0", + "symfony/var-exporter": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-29T08:23:45+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/a32f3f45f1990db8c4341d5122a7d3a381c7e575", + "reference": "a32f3f45f1990db8c4341d5122a7d3a381c7e575", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.2", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^6.4|^7.0", + "symfony/mime": "^7.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/messenger": "<6.4", + "symfony/mime": "<6.4", + "symfony/twig-bridge": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/twig-bridge": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-13T11:49:31+00:00" + }, + { + "name": "symfony/mime", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "reference": "e0a0f859148daf1edf6c60b398eb40bfc96697d1", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<6.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/property-access": "^6.4|^7.0", + "symfony/property-info": "^6.4|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T13:41:35+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", + "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T09:58:17+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "3833d7255cc303546435cb650316bff708a1c75c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", + "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-01-02T08:10:11+00:00" + }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-24T13:30:11+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "reference": "21533be36c24be3f4b1669c4725c7d1d2bab4ae2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/32241012d521e2e8a9d713adb0812bb773b907f1", + "reference": "32241012d521e2e8a9d713adb0812bb773b907f1", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-18T09:42:54+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.3.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/7614b8ca5fa89b9cd233e21b627bfc5774f586e4", + "reference": "7614b8ca5fa89b9cd233e21b627bfc5774f586e4", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/config": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/yaml": "<6.4" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/expression-language": "^6.4|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v7.3.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-15T11:36:08+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-25T09:37:31+00:00" + }, + { + "name": "symfony/string", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "reference": "17a426cce5fd1f0901fefa9b2a490d0038fd3c9c", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-intl-grapheme": "~1.0", + "symfony/polyfill-intl-normalizer": "~1.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-25T06:35:40+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "e0837b4cbcef63c754d89a4806575cada743a38d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/e0837b4cbcef63c754d89a4806575cada743a38d", + "reference": "e0837b4cbcef63c754d89a4806575cada743a38d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0", + "symfony/translation-contracts": "^2.5|^3.0" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/config": "<6.4", + "symfony/console": "<6.4", + "symfony/dependency-injection": "<6.4", + "symfony/http-client-contracts": "<2.5", + "symfony/http-kernel": "<6.4", + "symfony/service-contracts": "<2.5", + "symfony/twig-bundle": "<6.4", + "symfony/yaml": "<6.4" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0", + "symfony/console": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/finder": "^6.4|^7.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^6.4|^7.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-01T21:02:37+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "reference": "df210c7a2573f1913b2d17cc95f90f53a73d8f7d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-27T08:32:26+00:00" + }, + { + "name": "symfony/uid", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/a69f69f3159b852651a6bf45a9fdd149520525bb", + "reference": "a69f69f3159b852651a6bf45a9fdd149520525bb", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v7.3.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-27T19:55:54+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", + "reference": "34d8d4c4b9597347306d1ec8eb4e1319b1e6986f", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/uid": "^6.4|^7.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-13T11:49:31+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.3.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/0d72ac1c00084279c1816675284073c5a337c20d", + "reference": "0d72ac1c00084279c1816675284073c5a337c20d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.3.0" + }, + "time": "2024-12-21T16:25:41+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.2", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "reference": "24ac4c74f91ee2c193fa1aaa5c249cb0822809af", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.3", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.3", + "symfony/polyfill-ctype": "^1.24", + "symfony/polyfill-mbstring": "^1.24", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-04-30T23:37:27+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.0.3", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "reference": "b1d923f88091c6bf09699efcd7c8a1b1bfd7351d", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "require-dev": { + "phpunit/phpunit": "~6.0 || ~7.0 || ~9.0" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.0.3" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2024-11-21T01:49:47+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" + }, + { + "name": "wikimedia/composer-merge-plugin", + "version": "v2.1.0", + "source": { + "type": "git", + "url": "https://github.com/wikimedia/composer-merge-plugin.git", + "reference": "a03d426c8e9fb2c9c569d9deeb31a083292788bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/wikimedia/composer-merge-plugin/zipball/a03d426c8e9fb2c9c569d9deeb31a083292788bc", + "reference": "a03d426c8e9fb2c9c569d9deeb31a083292788bc", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.1||^2.0", + "php": ">=7.2.0" + }, + "require-dev": { + "composer/composer": "^1.1||^2.0", + "ext-json": "*", + "mediawiki/mediawiki-phan-config": "0.11.1", + "php-parallel-lint/php-parallel-lint": "~1.3.1", + "phpspec/prophecy": "~1.15.0", + "phpunit/phpunit": "^8.5||^9.0", + "squizlabs/php_codesniffer": "~3.7.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Wikimedia\\Composer\\Merge\\V2\\MergePlugin", + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Wikimedia\\Composer\\Merge\\V2\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bryan Davis", + "email": "bd808@wikimedia.org" + } + ], + "description": "Composer plugin to merge multiple composer.json files", + "support": { + "issues": "https://github.com/wikimedia/composer-merge-plugin/issues", + "source": "https://github.com/wikimedia/composer-merge-plugin/tree/v2.1.0" + }, + "time": "2023-04-15T19:07:00+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "reference": "8cc3d575c1f0e57eeb923f366a37528c50d2385a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0|^12.0", + "illuminate/contracts": "^10.24|^11.0|^12.0", + "illuminate/log": "^10.24|^11.0|^12.0", + "illuminate/process": "^10.24|^11.0|^12.0", + "illuminate/support": "^10.24|^11.0|^12.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0", + "laravel/pint": "^1.13", + "orchestra/testbench-core": "^8.13|^9.0|^10.0", + "pestphp/pest": "^2.20|^3.0", + "pestphp/pest-plugin-type-coverage": "^2.3|^3.0", + "phpstan/phpstan": "^1.12.27", + "symfony/var-dumper": "^6.3|^7.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2025-06-05T13:55:57+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.25.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/5016e263f95d97670d71b9a987bd8996ade6d8d9", + "reference": "5016e263f95d97670d71b9a987bd8996ade6d8d9", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.87.2", + "illuminate/view": "^11.46.0", + "larastan/larastan": "^3.7.1", + "laravel-zero/framework": "^11.45.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.1", + "pestphp/pest": "^2.36.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2025-09-19T02:57:12+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.45.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "019a2933ff4a9199f098d4259713f9bc266a874e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/019a2933ff4a9199f098d4259713f9bc266a874e", + "reference": "019a2933ff4a9199f098d4259713f9bc266a874e", + "shasum": "" + }, + "require": { + "illuminate/console": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/contracts": "^9.52.16|^10.0|^11.0|^12.0", + "illuminate/support": "^9.52.16|^10.0|^11.0|^12.0", + "php": "^8.0", + "symfony/console": "^6.0|^7.0", + "symfony/yaml": "^6.0|^7.0" + }, + "require-dev": { + "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", + "phpstan/phpstan": "^1.10" + }, + "bin": [ + "bin/sail" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Sail\\SailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Sail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Docker files for running a basic Laravel application.", + "keywords": [ + "docker", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/sail/issues", + "source": "https://github.com/laravel/sail" + }, + "time": "2025-08-25T19:28:31+00:00" + }, + { + "name": "laravel/telescope", + "version": "v5.11.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/telescope.git", + "reference": "8b7bd77857d6b1b8c9362560cde74911cee68ab8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/telescope/zipball/8b7bd77857d6b1b8c9362560cde74911cee68ab8", + "reference": "8b7bd77857d6b1b8c9362560cde74911cee68ab8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^8.37|^9.0|^10.0|^11.0|^12.0", + "php": "^8.0", + "symfony/console": "^5.3|^6.0|^7.0", + "symfony/var-dumper": "^5.0|^6.0|^7.0" + }, + "require-dev": { + "ext-gd": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "laravel/octane": "^1.4|^2.0|dev-develop", + "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0|^10.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.0|^10.5|^11.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Telescope\\TelescopeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Telescope\\": "src/", + "Laravel\\Telescope\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mohamed Said", + "email": "mohamed@laravel.com" + } + ], + "description": "An elegant debug assistant for the Laravel framework.", + "keywords": [ + "debugging", + "laravel", + "monitoring" + ], + "support": { + "issues": "https://github.com/laravel/telescope/issues", + "source": "https://github.com/laravel/telescope/tree/v5.11.4" + }, + "time": "2025-09-12T14:36:07+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.8.2", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", + "php": "^8.2.0", + "symfony/console": "^7.3.0" + }, + "conflict": { + "laravel/framework": "<11.44.2 || >=13.0.0", + "phpunit/phpunit": "<11.5.15 || >=13.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.3", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", + "laravel/tinker": "^2.10.1", + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2", + "sebastian/environment": "^7.2.1 || ^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-06-25T02:12:12+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.11", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "reference": "4f7722aa9a7b76aa775e2d9d4e95d1ea16eeeef4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.4.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^11.5.2" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/11.0.11" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2025-08-27T14:37:49+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "5.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-27T05:02:59+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^11.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:07:44+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:08:43+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:09:35+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "11.5.39", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "ad5597f79d8489d2870073ac0bc0dd0ad1fa9931" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad5597f79d8489d2870073ac0bc0dd0ad1fa9931", + "reference": "ad5597f79d8489d2870073ac0bc0dd0ad1fa9931", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.11", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.2", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.1", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.3", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "11.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.39" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-09-14T06:20:41+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:41:36+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-19T07:56:08+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:45:54+00:00" + }, + { + "name": "sebastian/comparator", + "version": "6.3.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/85c77556683e6eee4323e4c5468641ca0237e2e8", + "reference": "85c77556683e6eee4323e4c5468641ca0237e2e8", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.3-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2025-08-10T08:07:46+00:00" + }, + { + "name": "sebastian/complexity", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:49:50+00:00" + }, + { + "name": "sebastian/diff", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0", + "symfony/process": "^4.2 || ^5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:53:05+00:00" + }, + { + "name": "sebastian/environment", + "version": "7.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2025-05-21T11:55:47+00:00" + }, + { + "name": "sebastian/exporter", + "version": "6.3.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-12-05T09:17:50+00:00" + }, + { + "name": "sebastian/global-state", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:57:36+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T04:58:38+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:00:13+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:01:32+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "reference": "f6458abbf32a6c8174f8f26261475dc133b3d9dc", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:42:22+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "reference": "f77d2d4e78738c98d9a68d2596fe5e8fa380f449", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "phpunit/phpunit": "^11.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:55:48+00:00" + }, + { + "name": "sebastian/version", + "version": "5.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-10-09T05:16:32+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "symfony/yaml", + "version": "v7.3.3", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "d4f4a66866fe2451f61296924767280ab5732d9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/d4f4a66866fe2451f61296924767280ab5732d9d", + "reference": "d4f4a66866fe2451f61296924767280ab5732d9d", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0" + }, + "bin": [ + "Resources/bin/yaml-lint" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Loads and dumps YAML files", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v7.3.3" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-08-27T11:34:33+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.2.3", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.2.3" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:36:25+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.2" + }, + "platform-dev": {}, + "plugin-api-version": "2.6.0" +} diff --git a/be/config/active_role.php b/be/config/active_role.php new file mode 100644 index 0000000..44532cd --- /dev/null +++ b/be/config/active_role.php @@ -0,0 +1,17 @@ + (bool) env('ACTIVE_ROLE_PREFER_MEMBER', true), + + 'member_redirect' => env('ACTIVE_ROLE_MEMBER_REDIRECT', '/profile'), + + 'admin_redirect' => env('ACTIVE_ROLE_ADMIN_REDIRECT', '/profile'), + +]; diff --git a/be/config/api_security.php b/be/config/api_security.php new file mode 100644 index 0000000..60e42e2 --- /dev/null +++ b/be/config/api_security.php @@ -0,0 +1,167 @@ + env('BLOCK_API_TOOLS_IN_PRODUCTION', true), + + /* + |-------------------------------------------------------------------------- + | Enable API Key Authentication + |-------------------------------------------------------------------------- + | + | When enabled, external systems can use API keys to bypass API tool + | blocking and access protected endpoints. + | + */ + + 'enable_api_key_auth' => env('ENABLE_API_KEY_AUTH', true), + + /* + |-------------------------------------------------------------------------- + | Blocked User Agents + |-------------------------------------------------------------------------- + | + | List of User-Agent strings that should be blocked in production. + | These are common API testing tools and clients. + | + */ + + 'blocked_user_agents' => [ + 'insomnia', + 'postman', + 'postmanruntime', + 'curl', + 'wget', + 'httpie', + 'restclient', + 'apifox', + 'thunder client', + 'rapidapi', + 'swagger', + 'openapi', + 'api tester', + 'api client', + 'rest client', + 'http client', + 'api explorer', + 'api documentation', + 'soapui', + 'jmeter', + 'newman', + 'paw', + 'charles', + 'fiddler', + 'burp', + 'zap', + 'mitmproxy', + ], + + /* + |-------------------------------------------------------------------------- + | Minimum User-Agent Length + |-------------------------------------------------------------------------- + | + | Minimum length for User-Agent strings. Shorter strings are considered + | suspicious and will be blocked. + | + */ + + 'min_user_agent_length' => env('MIN_USER_AGENT_LENGTH', 10), + + /* + |-------------------------------------------------------------------------- + | Custom Error Message + |-------------------------------------------------------------------------- + | + | Custom error message to display when API tools are blocked. + | + */ + + 'blocked_message' => env('API_BLOCKED_MESSAGE', 'API access is restricted in production environment. Please use the web interface.'), + + /* + |-------------------------------------------------------------------------- + | Allowed IP Addresses + |-------------------------------------------------------------------------- + | + | List of IP addresses that are allowed to bypass API tool blocking. + | Useful for monitoring tools or legitimate API clients. + | + */ + + 'allowed_ips' => array_filter(explode(',', env('API_ALLOWED_IPS', ''))), + + /* + |-------------------------------------------------------------------------- + | Allowed User Agents + |-------------------------------------------------------------------------- + | + | List of User-Agent patterns that are allowed even if they match + | blocked patterns. Useful for legitimate monitoring tools. + | + */ + + 'allowed_user_agents' => array_filter(explode(',', env('API_ALLOWED_USER_AGENTS', ''))), + + /* + |-------------------------------------------------------------------------- + | Valid API Keys + |-------------------------------------------------------------------------- + | + | List of valid API keys for external system access. These keys allow + | external systems to bypass API tool blocking when making requests. + | + | Format: Comma-separated list of API keys + | Example: API_VALID_KEYS=key1,key2,key3 + | + */ + + 'valid_api_keys' => array_filter(explode(',', env('API_VALID_KEYS', ''))), + + /* + |-------------------------------------------------------------------------- + | API Key Header Names + |-------------------------------------------------------------------------- + | + | List of header names that can contain API keys. The middleware will + | check these headers for valid API keys. + | + */ + + 'api_key_headers' => [ + 'X-API-Key', + 'API-Key', + 'Authorization', // Bearer token format + ], + + /* + |-------------------------------------------------------------------------- + | API Key Logging + |-------------------------------------------------------------------------- + | + | Whether to log API key usage for monitoring and security purposes. + | When enabled, logs will include API key usage (without exposing the key). + | + */ + + 'log_api_key_usage' => env('API_LOG_KEY_USAGE', true), +]; diff --git a/be/config/app.php b/be/config/app.php new file mode 100644 index 0000000..423eed5 --- /dev/null +++ b/be/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/be/config/auth.php b/be/config/auth.php new file mode 100644 index 0000000..3f1a624 --- /dev/null +++ b/be/config/auth.php @@ -0,0 +1,125 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + 'api' => [ + 'driver' => 'sanctum', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', Modules\Auth\Entities\User::class), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + + /* + |-------------------------------------------------------------------------- + | Email Verification OTP + |-------------------------------------------------------------------------- + */ + + 'email_verification' => [ + 'expiry_minutes' => (int) env('EMAIL_VERIFICATION_OTP_EXPIRY_MINUTES', 10), + 'max_attempts' => (int) env('EMAIL_VERIFICATION_OTP_MAX_ATTEMPTS', 5), + ], + +]; diff --git a/be/config/auth_cookie.php b/be/config/auth_cookie.php new file mode 100644 index 0000000..5f0f000 --- /dev/null +++ b/be/config/auth_cookie.php @@ -0,0 +1,29 @@ + env('AUTH_COOKIE_NAME', 'auth_token'), + + 'lifetime_minutes' => (int) env('AUTH_COOKIE_LIFETIME', 60 * 12), + + 'secure' => env('AUTH_COOKIE_SECURE', env('APP_ENV') !== 'local'), + + 'same_site' => env('AUTH_COOKIE_SAME_SITE', 'lax'), + + 'original_user_cookie' => env('AUTH_COOKIE_ORIGINAL_USER', 'original_user_id'), + + /* + | Include token in JSON login/SSO responses (disable in production SPA flow). + */ + 'expose_token_in_response' => env('AUTH_COOKIE_EXPOSE_TOKEN', false), + +]; diff --git a/be/config/cache.php b/be/config/cache.php new file mode 100644 index 0000000..c2d927d --- /dev/null +++ b/be/config/cache.php @@ -0,0 +1,108 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "octane", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + +]; diff --git a/be/config/cors.php b/be/config/cors.php new file mode 100644 index 0000000..28c3045 --- /dev/null +++ b/be/config/cors.php @@ -0,0 +1,38 @@ + ['/*', 'sanctum/csrf-cookie'], + + 'allowed_methods' => ['*'], + + 'allowed_origins' => [ + 'http://localhost:5173', + 'http://127.0.0.1:5173', + 'http://20.11.32.49', + 'http://20.11.32.50', + ], + + 'allowed_origins_patterns' => ['*'], + + 'allowed_headers' => ['*'], + + 'exposed_headers' => [], + + 'max_age' => 0, + + 'supports_credentials' => true, +]; diff --git a/be/config/database.php b/be/config/database.php new file mode 100644 index 0000000..53dcae0 --- /dev/null +++ b/be/config/database.php @@ -0,0 +1,183 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => 'prefer', + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/be/config/filesystems.php b/be/config/filesystems.php new file mode 100644 index 0000000..3d671bd --- /dev/null +++ b/be/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => env('APP_URL').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/be/config/fortify.php b/be/config/fortify.php new file mode 100644 index 0000000..dfc5deb --- /dev/null +++ b/be/config/fortify.php @@ -0,0 +1,159 @@ + 'web', + + /* + |-------------------------------------------------------------------------- + | Fortify Password Broker + |-------------------------------------------------------------------------- + | + | Here you may specify which password broker Fortify can use when a user + | is resetting their password. This configured value should match one + | of your password brokers setup in your "auth" configuration file. + | + */ + + 'passwords' => 'users', + + /* + |-------------------------------------------------------------------------- + | Username / Email + |-------------------------------------------------------------------------- + | + | This value defines which model attribute should be considered as your + | application's "username" field. Typically, this might be the email + | address of the users but you are free to change this value here. + | + | Out of the box, Fortify expects forgot password and reset password + | requests to have a field named 'email'. If the application uses + | another name for the field you may define it below as needed. + | + */ + + 'username' => 'email', + + 'email' => 'email', + + /* + |-------------------------------------------------------------------------- + | Lowercase Usernames + |-------------------------------------------------------------------------- + | + | This value defines whether usernames should be lowercased before saving + | them in the database, as some database system string fields are case + | sensitive. You may disable this for your application if necessary. + | + */ + + 'lowercase_usernames' => true, + + /* + |-------------------------------------------------------------------------- + | Home Path + |-------------------------------------------------------------------------- + | + | Here you may configure the path where users will get redirected during + | authentication or password reset when the operations are successful + | and the user is authenticated. You are free to change this value. + | + */ + + 'home' => '/home', + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Prefix / Subdomain + |-------------------------------------------------------------------------- + | + | Here you may specify which prefix Fortify will assign to all the routes + | that it registers with the application. If necessary, you may change + | subdomain under which all of the Fortify routes will be available. + | + */ + + 'prefix' => '', + + 'domain' => null, + + /* + |-------------------------------------------------------------------------- + | Fortify Routes Middleware + |-------------------------------------------------------------------------- + | + | Here you may specify which middleware Fortify will assign to the routes + | that it registers with the application. If necessary, you may change + | these middleware but typically this provided default is preferred. + | + */ + + 'middleware' => ['web'], + + /* + |-------------------------------------------------------------------------- + | Rate Limiting + |-------------------------------------------------------------------------- + | + | By default, Fortify will throttle logins to five requests per minute for + | every email and IP address combination. However, if you would like to + | specify a custom rate limiter to call then you may specify it here. + | + */ + + 'limiters' => [ + 'login' => 'login', + 'two-factor' => 'two-factor', + ], + + /* + |-------------------------------------------------------------------------- + | Register View Routes + |-------------------------------------------------------------------------- + | + | Here you may specify if the routes returning views should be disabled as + | you may not need them when building your own application. This may be + | especially true if you're writing a custom single-page application. + | + */ + + 'views' => false, + + /* + |-------------------------------------------------------------------------- + | Features + |-------------------------------------------------------------------------- + | + | Some of the Fortify features are optional. You may disable the features + | by removing them from this array. You're free to only remove some of + | these features or you can even remove all of these if you need to. + | + */ + + 'features' => [ + Features::registration(), + Features::resetPasswords(), + // Features::emailVerification(), + Features::updateProfileInformation(), + Features::updatePasswords(), + Features::twoFactorAuthentication([ + 'confirm' => true, + 'confirmPassword' => true, + // 'window' => 0, + ]), + ], + +]; diff --git a/be/config/horizon.php b/be/config/horizon.php new file mode 100644 index 0000000..d171010 --- /dev/null +++ b/be/config/horizon.php @@ -0,0 +1,230 @@ + env('HORIZON_NAME'), + + /* + |-------------------------------------------------------------------------- + | Horizon Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Horizon will be accessible from. If this + | setting is null, Horizon will reside under the same domain as the + | application. Otherwise, this value will serve as the subdomain. + | + */ + + 'domain' => env('HORIZON_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Horizon Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Horizon will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('HORIZON_PATH', 'horizon'), + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Connection + |-------------------------------------------------------------------------- + | + | This is the name of the Redis connection where Horizon will store the + | meta information required for it to function. It includes the list + | of supervisors, failed jobs, job metrics, and other information. + | + */ + + 'use' => 'default', + + /* + |-------------------------------------------------------------------------- + | Horizon Redis Prefix + |-------------------------------------------------------------------------- + | + | This prefix will be used when storing all Horizon data in Redis. You + | may modify the prefix when you are running multiple installations + | of Horizon on the same server so that they don't have problems. + | + */ + + 'prefix' => env( + 'HORIZON_PREFIX', + Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:' + ), + + /* + |-------------------------------------------------------------------------- + | Horizon Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will get attached onto each Horizon route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => ['auth.basic'], + + /* + |-------------------------------------------------------------------------- + | Queue Wait Time Thresholds + |-------------------------------------------------------------------------- + | + | This option allows you to configure when the LongWaitDetected event + | will be fired. Every connection / queue combination may have its + | own, unique threshold (in seconds) before this event is fired. + | + */ + + 'waits' => [ + 'redis:default' => 60, + ], + + /* + |-------------------------------------------------------------------------- + | Job Trimming Times + |-------------------------------------------------------------------------- + | + | Here you can configure for how long (in minutes) you desire Horizon to + | persist the recent and failed jobs. Typically, recent jobs are kept + | for one hour while all failed jobs are stored for an entire week. + | + */ + + 'trim' => [ + 'recent' => 60, + 'pending' => 60, + 'completed' => 60, + 'recent_failed' => 10080, + 'failed' => 10080, + 'monitored' => 10080, + ], + + /* + |-------------------------------------------------------------------------- + | Silenced Jobs + |-------------------------------------------------------------------------- + | + | Silencing a job will instruct Horizon to not place the job in the list + | of completed jobs within the Horizon dashboard. This setting may be + | used to fully remove any noisy jobs from the completed jobs list. + | + */ + + 'silenced' => [ + // App\Jobs\ExampleJob::class, + ], + + 'silenced_tags' => [ + // 'notifications', + ], + + /* + |-------------------------------------------------------------------------- + | Metrics + |-------------------------------------------------------------------------- + | + | Here you can configure how many snapshots should be kept to display in + | the metrics graph. This will get used in combination with Horizon's + | `horizon:snapshot` schedule to define how long to retain metrics. + | + */ + + 'metrics' => [ + 'trim_snapshots' => [ + 'job' => 24, + 'queue' => 24, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Fast Termination + |-------------------------------------------------------------------------- + | + | When this option is enabled, Horizon's "terminate" command will not + | wait on all of the workers to terminate unless the --wait option + | is provided. Fast termination can shorten deployment delay by + | allowing a new instance of Horizon to start while the last + | instance will continue to terminate each of its workers. + | + */ + + 'fast_termination' => false, + + /* + |-------------------------------------------------------------------------- + | Memory Limit (MB) + |-------------------------------------------------------------------------- + | + | This value describes the maximum amount of memory the Horizon master + | supervisor may consume before it is terminated and restarted. For + | configuring these limits on your workers, see the next section. + | + */ + + 'memory_limit' => 64, + + /* + |-------------------------------------------------------------------------- + | Queue Worker Configuration + |-------------------------------------------------------------------------- + | + | Here you may define the queue worker settings used by your application + | in all environments. These supervisors and settings handle all your + | queued jobs and will be provisioned by Horizon during deployment. + | + */ + + 'defaults' => [ + 'supervisor-1' => [ + 'connection' => 'redis', + 'queue' => ['default', 'kjc-historical-data', 'pkj-historical-data'], + 'balance' => 'auto', + 'autoScalingStrategy' => 'time', + 'maxProcesses' => 1, + 'maxTime' => 0, + 'maxJobs' => 0, + 'memory' => (int) env('HORIZON_WORKER_MEMORY', 512), // MB; 512 for large syncs (SPATD), was 128 + 'tries' => 1, + 'timeout' => env('HORIZON_WORKER_TIMEOUT', 60), + 'nice' => 0, + ], + ], + + 'environments' => [ + 'production' => [ + 'supervisor-1' => [ + 'maxProcesses' => 10, + 'balanceMaxShift' => 1, + 'balanceCooldown' => 3, + ], + ], + + 'local' => [ + 'supervisor-1' => [ + 'maxProcesses' => 3, + ], + ], + ], +]; diff --git a/be/config/http.php b/be/config/http.php new file mode 100644 index 0000000..9e04c7d --- /dev/null +++ b/be/config/http.php @@ -0,0 +1,33 @@ + env('HTTP_TIMEOUT', 30), + 'connect_timeout' => env('HTTP_CONNECT_TIMEOUT', 10), + + /* + |-------------------------------------------------------------------------- + | Proxy Configuration + |-------------------------------------------------------------------------- + | + | Proxy settings for HTTP clients + | + */ + + 'proxy' => [ + 'enabled' => env('HTTP_PROXY_ENABLED', false), + 'environments' => env('HTTP_PROXY_ENVIRONMENTS', 'production') ? explode(',', env('HTTP_PROXY_ENVIRONMENTS', 'production')) : [], + 'host' => env('HTTP_PROXY_HOST'), + 'port' => env('HTTP_PROXY_PORT'), + 'username' => env('HTTP_PROXY_USERNAME'), + 'password' => env('HTTP_PROXY_PASSWORD'), + ], +]; diff --git a/be/config/laravel-impersonate.php b/be/config/laravel-impersonate.php new file mode 100644 index 0000000..84ff38c --- /dev/null +++ b/be/config/laravel-impersonate.php @@ -0,0 +1,41 @@ + 'impersonated_by', + + /** + * The session key used to stored the original user guard. + */ + 'session_guard' => 'impersonator_guard', + + /** + * The session key used to stored what guard is impersonator using. + */ + 'session_guard_using' => 'impersonator_guard_using', + + /** + * The default impersonator guard used. + */ + 'default_impersonator_guard' => 'api', + + /** + * The URI to redirect after taking an impersonation. + * + * Only used in the built-in controller. + * * Use 'back' to redirect to the previous page + */ + 'take_redirect_to' => '/', + + /** + * The URI to redirect after leaving an impersonation. + * + * Only used in the built-in controller. + * Use 'back' to redirect to the previous page + */ + 'leave_redirect_to' => '/', + +]; diff --git a/be/config/logging.php b/be/config/logging.php new file mode 100644 index 0000000..9e998a4 --- /dev/null +++ b/be/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/be/config/mail.php b/be/config/mail.php new file mode 100644 index 0000000..522b284 --- /dev/null +++ b/be/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', 'Example'), + ], + +]; diff --git a/be/config/modules.php b/be/config/modules.php new file mode 100644 index 0000000..9511890 --- /dev/null +++ b/be/config/modules.php @@ -0,0 +1,301 @@ + 'Modules', + + /* + |-------------------------------------------------------------------------- + | Module Stubs + |-------------------------------------------------------------------------- + | + | Default module stubs. + | + */ + 'stubs' => [ + 'enabled' => true, + 'path' => base_path('stubs/nwidart-stubs'), + 'files' => [ + 'Routes/web' => 'Routes/web.php', + 'Routes/api' => 'Routes/api.php', + // Disabled for API-only modules + // 'views/index' => 'resources/views/index.blade.php', + // 'views/master' => 'resources/views/components/layouts/master.blade.php', + 'scaffold/config' => 'config/config.php', + 'composer' => 'composer.json', + // Disabled for API-only modules + // 'assets/js/app' => 'resources/assets/js/app.js', + // 'assets/sass/app' => 'resources/assets/sass/app.scss', + // 'vite' => 'vite.config.js', + 'package' => 'package.json', + ], + 'replacements' => [ + /** + * Define custom replacements for each section. + * You can specify a closure for dynamic values. + * + * Example: + * + * 'composer' => [ + * 'CUSTOM_KEY' => fn (\Nwidart\Modules\Generators\ModuleGenerator $generator) => $generator->getModule()->getLowerName() . '-module', + * 'CUSTOM_KEY2' => fn () => 'custom text', + * 'LOWER_NAME', + * 'STUDLY_NAME', + * // ... + * ], + * + * Note: Keys should be in UPPERCASE. + */ + 'Routes/web' => ['LOWER_NAME', 'STUDLY_NAME', 'PLURAL_LOWER_NAME', 'KEBAB_NAME', 'MODULE_NAMESPACE', 'CONTROLLER_NAMESPACE'], + 'Routes/api' => ['LOWER_NAME', 'STUDLY_NAME', 'PLURAL_LOWER_NAME', 'KEBAB_NAME', 'MODULE_NAMESPACE', 'CONTROLLER_NAMESPACE'], + 'vite' => ['LOWER_NAME', 'STUDLY_NAME', 'KEBAB_NAME'], + 'json' => ['LOWER_NAME', 'STUDLY_NAME', 'KEBAB_NAME', 'MODULE_NAMESPACE', 'PROVIDER_NAMESPACE'], + 'views/index' => ['LOWER_NAME'], + 'views/master' => ['LOWER_NAME', 'STUDLY_NAME', 'KEBAB_NAME'], + 'scaffold/config' => ['STUDLY_NAME'], + 'composer' => [ + 'LOWER_NAME', + 'STUDLY_NAME', + 'VENDOR', + 'AUTHOR_NAME', + 'AUTHOR_EMAIL', + 'MODULE_NAMESPACE', + 'PROVIDER_NAMESPACE', + 'APP_FOLDER_NAME', + ], + ], + 'gitkeep' => true, + ], + 'paths' => [ + /* + |-------------------------------------------------------------------------- + | Modules path + |-------------------------------------------------------------------------- + | + | This path is used to save the generated module. + | This path will also be added automatically to the list of scanned folders. + | + */ + 'modules' => base_path('Modules'), + + /* + |-------------------------------------------------------------------------- + | Modules assets path + |-------------------------------------------------------------------------- + | + | Here you may update the modules' assets path. + | + */ + 'assets' => public_path('modules'), + + /* + |-------------------------------------------------------------------------- + | The migrations' path + |-------------------------------------------------------------------------- + | + | Where you run the 'module:publish-migration' command, where do you publish the + | the migration files? + | + */ + 'migration' => base_path('database/migrations'), + + /* + |-------------------------------------------------------------------------- + | The app path + |-------------------------------------------------------------------------- + | + | app folder name + | for example can change it to 'src' or 'App' + */ + 'app_folder' => 'App', + + /* + |-------------------------------------------------------------------------- + | Generator path + |-------------------------------------------------------------------------- + | Customise the paths where the folders will be generated. + | Setting the generate key to false will not generate that folder + */ + 'generator' => [ + // Actions/ + 'actions' => ['path' => 'Actions', 'generate' => true], + 'casts' => ['path' => 'Casts', 'generate' => false], + 'channels' => ['path' => 'Broadcasting', 'generate' => false], + 'class' => ['path' => 'Classes', 'generate' => false], + 'command' => ['path' => 'Console', 'generate' => true], + 'component-class' => ['path' => 'View/Components', 'generate' => false], + 'emails' => ['path' => 'Emails', 'generate' => true], + 'event' => ['path' => 'Events', 'generate' => false], + 'enums' => ['path' => 'Enums', 'generate' => false], + 'exceptions' => ['path' => 'Exceptions', 'generate' => false], + 'jobs' => ['path' => 'Jobs', 'generate' => true], + 'helpers' => ['path' => 'Helpers', 'generate' => true], + 'interfaces' => ['path' => 'Interfaces', 'generate' => false], + 'listener' => ['path' => 'Listeners', 'generate' => false], + 'model' => ['path' => 'Entities', 'generate' => true], + 'notifications' => ['path' => 'Notifications', 'generate' => true], + 'observer' => ['path' => 'Observers', 'generate' => false], + 'policies' => ['path' => 'Policies', 'generate' => true], + 'provider' => ['path' => 'Providers', 'generate' => true], + 'repository' => ['path' => 'Repositories', 'generate' => true], + 'repository-interface' => ['path' => 'Repositories/Contracts', 'generate' => true], + 'resource' => ['path' => 'Transformers', 'generate' => true], + 'route-provider' => ['path' => 'Providers', 'generate' => true], + 'rules' => ['path' => 'Rules', 'generate' => false], + 'services' => ['path' => 'Services', 'generate' => true], + 'scopes' => ['path' => 'Entities/Scopes', 'generate' => false], + 'traits' => ['path' => 'Traits', 'generate' => false], + + // Http/ + 'controller' => ['path' => 'Http/Controllers', 'generate' => true], + 'filter' => ['path' => 'Http/Middleware', 'generate' => false], + 'request' => ['path' => 'Http/Requests', 'generate' => true], + + // Config/ + 'config' => ['path' => 'Config', 'generate' => true], + + // Database/ + 'factory' => ['path' => 'Database/Factories', 'generate' => true], + 'migration' => ['path' => 'Database/Migrations', 'generate' => true], + 'seeder' => ['path' => 'Database/Seeders', 'generate' => true], + + // Lang/ + 'lang' => ['path' => 'Lang', 'generate' => false], + + // Resources/ (disabled for API-only modules) + 'assets' => ['path' => 'Resources/Assets', 'generate' => false], + 'component-view' => ['path' => 'Resources/Views/Components', 'generate' => false], + 'views' => ['path' => 'Resources/Views', 'generate' => false], + + // Routes/ + 'routes' => ['path' => 'Routes', 'generate' => true], + + // Tests/ + 'test-feature' => ['path' => 'Tests/Feature', 'generate' => true], + 'test-unit' => ['path' => 'Tests/Unit', 'generate' => true], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Auto Discover of Modules + |-------------------------------------------------------------------------- + | + | Here you configure auto discover of module + | This is useful for simplify module providers. + | + */ + 'auto-discover' => [ + /* + |-------------------------------------------------------------------------- + | Migrations + |-------------------------------------------------------------------------- + | + | This option for register migration automatically. + | + */ + 'migrations' => true, + + /* + |-------------------------------------------------------------------------- + | Translations + |-------------------------------------------------------------------------- + | + | This option for register lang file automatically. + | + */ + 'translations' => false, + + ], + + /* + |-------------------------------------------------------------------------- + | Package commands + |-------------------------------------------------------------------------- + | + | Here you can define which commands will be visible and used in your + | application. You can add your own commands to merge section. + | + */ + 'commands' => ConsoleServiceProvider::defaultCommands() + ->merge([ + // New commands go here + ])->toArray(), + + /* + |-------------------------------------------------------------------------- + | Scan Path + |-------------------------------------------------------------------------- + | + | Here you define which folder will be scanned. By default will scan vendor + | directory. This is useful if you host the package in packagist website. + | + */ + 'scan' => [ + 'enabled' => false, + 'paths' => [ + base_path('vendor/*/*'), + ], + ], + + /* + |-------------------------------------------------------------------------- + | Composer File Template + |-------------------------------------------------------------------------- + | + | Here is the config for the composer.json file, generated by this package + | + */ + 'composer' => [ + 'vendor' => env('MODULE_VENDOR', 'nwidart'), + 'author' => [ + 'name' => env('MODULE_AUTHOR_NAME', 'Nicolas Widart'), + 'email' => env('MODULE_AUTHOR_EMAIL', 'n.widart@gmail.com'), + ], + 'composer-output' => false, + ], + + /* + |-------------------------------------------------------------------------- + | Choose what laravel-modules will register as custom namespaces. + | Setting one to false will require you to register that part + | in your own Service Provider class. + |-------------------------------------------------------------------------- + */ + 'register' => [ + 'translations' => true, + /** + * load files on boot or register method + */ + 'files' => 'register', + ], + + /* + |-------------------------------------------------------------------------- + | Activators + |-------------------------------------------------------------------------- + | + | You can define new types of activators here, file, database, etc. The only + | required parameter is 'class'. + | The file activator will store the activation status in storage/installed_modules + */ + 'activators' => [ + 'file' => [ + 'class' => FileActivator::class, + 'statuses-file' => base_path('modules_statuses.json'), + ], + ], + + 'activator' => 'file', +]; diff --git a/be/config/permission.php b/be/config/permission.php new file mode 100644 index 0000000..a1ad8e5 --- /dev/null +++ b/be/config/permission.php @@ -0,0 +1,202 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => App\Models\Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Modules\Role\Entities\Role::class, + + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttached + * \Spatie\Permission\Events\RoleDetached + * \Spatie\Permission\Events\PermissionAttached + * \Spatie\Permission\Events\PermissionDetached + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/be/config/queue.php b/be/config/queue.php new file mode 100644 index 0000000..116bd8d --- /dev/null +++ b/be/config/queue.php @@ -0,0 +1,112 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/be/config/services.php b/be/config/services.php new file mode 100644 index 0000000..6182e4b --- /dev/null +++ b/be/config/services.php @@ -0,0 +1,38 @@ + [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/be/config/session.php b/be/config/session.php new file mode 100644 index 0000000..d2bb5a6 --- /dev/null +++ b/be/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 30), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug(env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain and all subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + +]; diff --git a/be/config/sso.php b/be/config/sso.php new file mode 100644 index 0000000..ccc0b60 --- /dev/null +++ b/be/config/sso.php @@ -0,0 +1,15 @@ + env('SSO_SECRET'), + 'validation_url' => env('SSO_VALIDATION_URL'), + 'proxy_url' => env('SSO_PROXY_URL', 'http://20.9.91.1:3128'), + 'proxy_enabled' => env('SSO_PROXY_ENABLED', true), + 'max_attempts' => env('SSO_MAX_ATTEMPTS', 5), + 'token_expiration' => env('SSO_TOKEN_EXPIRATION', 300), + 'auto_activate_users' => env('SSO_AUTO_ACTIVATE_USERS', true), + 'log_activities' => env('SSO_LOG_ACTIVITIES', true), + 'connection_timeout' => env('SSO_CONNECTION_TIMEOUT', 10), + 'response_timeout' => env('SSO_RESPONSE_TIMEOUT', 30), + 'enable_fallback' => env('SSO_ENABLE_FALLBACK', true), +]; diff --git a/be/config/telescope.php b/be/config/telescope.php new file mode 100644 index 0000000..af8ca1b --- /dev/null +++ b/be/config/telescope.php @@ -0,0 +1,207 @@ + env('TELESCOPE_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Telescope Domain + |-------------------------------------------------------------------------- + | + | This is the subdomain where Telescope will be accessible from. If the + | setting is null, Telescope will reside under the same domain as the + | application. Otherwise, this value will be used as the subdomain. + | + */ + + 'domain' => env('TELESCOPE_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | Telescope Path + |-------------------------------------------------------------------------- + | + | This is the URI path where Telescope will be accessible from. Feel free + | to change this path to anything you like. Note that the URI will not + | affect the paths of its internal API that aren't exposed to users. + | + */ + + 'path' => env('TELESCOPE_PATH', 'telescope'), + + /* + |-------------------------------------------------------------------------- + | Telescope Storage Driver + |-------------------------------------------------------------------------- + | + | This configuration options determines the storage driver that will + | be used to store Telescope's data. In addition, you may set any + | custom options as needed by the particular driver you choose. + | + */ + + 'driver' => env('TELESCOPE_DRIVER', 'database'), + + 'storage' => [ + 'database' => [ + 'connection' => env('DB_CONNECTION', 'mysql'), + 'chunk' => 1000, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Queue + |-------------------------------------------------------------------------- + | + | This configuration options determines the queue connection and queue + | which will be used to process ProcessPendingUpdate jobs. This can + | be changed if you would prefer to use a non-default connection. + | + */ + + 'queue' => [ + 'connection' => env('TELESCOPE_QUEUE_CONNECTION'), + 'queue' => env('TELESCOPE_QUEUE'), + 'delay' => env('TELESCOPE_QUEUE_DELAY', 10), + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Route Middleware + |-------------------------------------------------------------------------- + | + | These middleware will be assigned to every Telescope route, giving you + | the chance to add your own middleware to this list or change any of + | the existing middleware. Or, you can simply stick with this list. + | + */ + + 'middleware' => [ + 'web', + Authorize::class, + ], + + /* + |-------------------------------------------------------------------------- + | Allowed / Ignored Paths & Commands + |-------------------------------------------------------------------------- + | + | The following array lists the URI paths and Artisan commands that will + | not be watched by Telescope. In addition to this list, some Laravel + | commands, like migrations and queue commands, are always ignored. + | + */ + + 'only_paths' => [ + // 'api/*' + ], + + 'ignore_paths' => [ + 'livewire*', + 'nova-api*', + 'pulse*', + ], + + 'ignore_commands' => [ + // + ], + + /* + |-------------------------------------------------------------------------- + | Telescope Watchers + |-------------------------------------------------------------------------- + | + | The following array lists the "watchers" that will be registered with + | Telescope. The watchers gather the application's profile data when + | a request or task is executed. Feel free to customize this list. + | + */ + + 'watchers' => [ + Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true), + + Watchers\CacheWatcher::class => [ + 'enabled' => env('TELESCOPE_CACHE_WATCHER', true), + 'hidden' => [], + 'ignore' => [], + ], + + Watchers\ClientRequestWatcher::class => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true), + + Watchers\CommandWatcher::class => [ + 'enabled' => env('TELESCOPE_COMMAND_WATCHER', true), + 'ignore' => [], + ], + + Watchers\DumpWatcher::class => [ + 'enabled' => env('TELESCOPE_DUMP_WATCHER', true), + 'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false), + ], + + Watchers\EventWatcher::class => [ + 'enabled' => env('TELESCOPE_EVENT_WATCHER', true), + 'ignore' => [], + ], + + Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true), + + Watchers\GateWatcher::class => [ + 'enabled' => env('TELESCOPE_GATE_WATCHER', true), + 'ignore_abilities' => [], + 'ignore_packages' => true, + 'ignore_paths' => [], + ], + + Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true), + + Watchers\LogWatcher::class => [ + 'enabled' => env('TELESCOPE_LOG_WATCHER', true), + 'level' => 'error', + ], + + Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true), + + Watchers\ModelWatcher::class => [ + 'enabled' => env('TELESCOPE_MODEL_WATCHER', true), + 'events' => ['eloquent.*'], + 'hydrations' => true, + ], + + Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true), + + Watchers\QueryWatcher::class => [ + 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), + 'ignore_packages' => true, + 'ignore_paths' => [], + 'slow' => 100, + ], + + Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true), + + Watchers\RequestWatcher::class => [ + 'enabled' => env('TELESCOPE_REQUEST_WATCHER', true), + 'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64), + 'ignore_http_methods' => [], + 'ignore_status_codes' => [], + ], + + Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true), + Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true), + ], +]; diff --git a/be/database/.gitignore b/be/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/be/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/be/database/factories/UserFactory.php b/be/database/factories/UserFactory.php new file mode 100644 index 0000000..584104c --- /dev/null +++ b/be/database/factories/UserFactory.php @@ -0,0 +1,44 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/be/database/migrations/0001_01_01_000000_create_users_table.php b/be/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..c056f7c --- /dev/null +++ b/be/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,54 @@ +uuid('id')->primary(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->string('ic_number')->nullable(); + $table->string('position')->nullable(); + $table->string('phone_number')->nullable(); + $table->string('image_url')->nullable(); + $table->rememberToken(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->foreignUuid('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/be/database/migrations/0001_01_01_000001_create_cache_table.php b/be/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..b9c106b --- /dev/null +++ b/be/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/be/database/migrations/0001_01_01_000002_create_jobs_table.php b/be/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..e8ec6ec --- /dev/null +++ b/be/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +uuid('id')->primary(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->uuid('id')->primary(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/be/database/migrations/2025_07_25_051334_create_countries_table.php b/be/database/migrations/2025_07_25_051334_create_countries_table.php new file mode 100644 index 0000000..76ad330 --- /dev/null +++ b/be/database/migrations/2025_07_25_051334_create_countries_table.php @@ -0,0 +1,30 @@ +uuid('id')->primary(); + $table->string('code', 2); + $table->string('name', 100); + $table->timestamps(); + $table->softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('countries'); + } +}; diff --git a/be/database/migrations/2025_08_02_061334_create_documents_table.php b/be/database/migrations/2025_08_02_061334_create_documents_table.php new file mode 100644 index 0000000..7530ea0 --- /dev/null +++ b/be/database/migrations/2025_08_02_061334_create_documents_table.php @@ -0,0 +1,39 @@ +uuid('id')->primary(); + $table->string('documentable_type'); + $table->unsignedBigInteger('documentable_id'); + $table->string('name'); + $table->string('path', 500); + $table->unsignedBigInteger('file_size')->nullable(); + $table->string('mime_type', 100)->nullable(); + $table->string('type', 100)->nullable()->default('general')->index(); + $table->text('description')->nullable(); + $table->uuid('uploaded_by')->foreign('users')->onDelete('restrict'); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['documentable_type', 'documentable_id']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('documents'); + } +}; diff --git a/be/database/migrations/2025_09_16_215723_create_personal_access_tokens_table.php b/be/database/migrations/2025_09_16_215723_create_personal_access_tokens_table.php new file mode 100644 index 0000000..62a379f --- /dev/null +++ b/be/database/migrations/2025_09_16_215723_create_personal_access_tokens_table.php @@ -0,0 +1,33 @@ +id(); + $table->uuidMorphs('tokenable'); + $table->text('name'); + $table->string('token', 64)->unique(); + $table->text('abilities')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('expires_at')->nullable()->index(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('personal_access_tokens'); + } +}; diff --git a/be/database/migrations/2025_09_19_133435_add_two_factor_columns_to_users_table.php b/be/database/migrations/2025_09_19_133435_add_two_factor_columns_to_users_table.php new file mode 100644 index 0000000..45739ef --- /dev/null +++ b/be/database/migrations/2025_09_19_133435_add_two_factor_columns_to_users_table.php @@ -0,0 +1,42 @@ +text('two_factor_secret') + ->after('password') + ->nullable(); + + $table->text('two_factor_recovery_codes') + ->after('two_factor_secret') + ->nullable(); + + $table->timestamp('two_factor_confirmed_at') + ->after('two_factor_recovery_codes') + ->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn([ + 'two_factor_secret', + 'two_factor_recovery_codes', + 'two_factor_confirmed_at', + ]); + }); + } +}; diff --git a/be/database/migrations/2025_09_20_093654_create_telescope_entries_table.php b/be/database/migrations/2025_09_20_093654_create_telescope_entries_table.php new file mode 100644 index 0000000..700a83f --- /dev/null +++ b/be/database/migrations/2025_09_20_093654_create_telescope_entries_table.php @@ -0,0 +1,70 @@ +getConnection()); + + $schema->create('telescope_entries', function (Blueprint $table) { + $table->bigIncrements('sequence'); + $table->uuid('uuid'); + $table->uuid('batch_id'); + $table->string('family_hash')->nullable(); + $table->boolean('should_display_on_index')->default(true); + $table->string('type', 20); + $table->longText('content'); + $table->dateTime('created_at')->nullable(); + + $table->unique('uuid'); + $table->index('batch_id'); + $table->index('family_hash'); + $table->index('created_at'); + $table->index(['type', 'should_display_on_index']); + }); + + $schema->create('telescope_entries_tags', function (Blueprint $table) { + $table->uuid('entry_uuid'); + $table->string('tag'); + + $table->primary(['entry_uuid', 'tag']); + $table->index('tag'); + + $table->foreign('entry_uuid') + ->references('uuid') + ->on('telescope_entries') + ->onDelete('cascade'); + }); + + $schema->create('telescope_monitoring', function (Blueprint $table) { + $table->string('tag')->primary(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $schema = Schema::connection($this->getConnection()); + + $schema->dropIfExists('telescope_entries_tags'); + $schema->dropIfExists('telescope_entries'); + $schema->dropIfExists('telescope_monitoring'); + } +}; diff --git a/be/database/migrations/2025_09_20_121315_create_permission_tables.php b/be/database/migrations/2025_09_20_121315_create_permission_tables.php new file mode 100644 index 0000000..1eb5585 --- /dev/null +++ b/be/database/migrations/2025_09_20_121315_create_permission_tables.php @@ -0,0 +1,136 @@ +engine('InnoDB'); + $table->uuid('id')->primary(); // permission id + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + // $table->engine('InnoDB'); + $table->uuid('id')->primary(); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format) + $table->string('guard_name'); // For MyISAM use string('guard_name', 25); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->uuid($pivotPermission); + + $table->string('model_type'); + $table->uuid($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->uuid($pivotRole); + + $table->string('model_type'); + $table->uuid($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->uuid($pivotPermission); + $table->uuid($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->onDelete('cascade'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->onDelete('cascade'); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + if (empty($tableNames)) { + throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + } + + Schema::drop($tableNames['role_has_permissions']); + Schema::drop($tableNames['model_has_roles']); + Schema::drop($tableNames['model_has_permissions']); + Schema::drop($tableNames['roles']); + Schema::drop($tableNames['permissions']); + } +}; diff --git a/be/database/migrations/2025_09_20_121526_create_activity_log_table.php b/be/database/migrations/2025_09_20_121526_create_activity_log_table.php new file mode 100644 index 0000000..72f83d9 --- /dev/null +++ b/be/database/migrations/2025_09_20_121526_create_activity_log_table.php @@ -0,0 +1,27 @@ +create(config('activitylog.table_name'), function (Blueprint $table) { + $table->bigIncrements('id'); + $table->string('log_name')->nullable(); + $table->text('description'); + $table->nullableUuidMorphs('subject', 'subject'); + $table->nullableUuidMorphs('causer', 'causer'); + $table->json('properties')->nullable(); + $table->timestamps(); + $table->index('log_name'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->dropIfExists(config('activitylog.table_name')); + } +} diff --git a/be/database/migrations/2025_09_20_121527_add_event_column_to_activity_log_table.php b/be/database/migrations/2025_09_20_121527_add_event_column_to_activity_log_table.php new file mode 100644 index 0000000..78d9a0e --- /dev/null +++ b/be/database/migrations/2025_09_20_121527_add_event_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->string('event')->nullable()->after('subject_type'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('event'); + }); + } +} diff --git a/be/database/migrations/2025_09_20_121528_add_batch_uuid_column_to_activity_log_table.php b/be/database/migrations/2025_09_20_121528_add_batch_uuid_column_to_activity_log_table.php new file mode 100644 index 0000000..320ef5c --- /dev/null +++ b/be/database/migrations/2025_09_20_121528_add_batch_uuid_column_to_activity_log_table.php @@ -0,0 +1,22 @@ +table(config('activitylog.table_name'), function (Blueprint $table) { + $table->uuid('batch_uuid')->nullable()->after('properties'); + }); + } + + public function down() + { + Schema::connection(config('activitylog.database_connection'))->table(config('activitylog.table_name'), function (Blueprint $table) { + $table->dropColumn('batch_uuid'); + }); + } +} diff --git a/be/database/migrations/2025_09_30_223214_add_route_name_to_permissions_table.php b/be/database/migrations/2025_09_30_223214_add_route_name_to_permissions_table.php new file mode 100644 index 0000000..9b905f3 --- /dev/null +++ b/be/database/migrations/2025_09_30_223214_add_route_name_to_permissions_table.php @@ -0,0 +1,28 @@ +string('route_name')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('permissions', function (Blueprint $table) { + $table->dropColumn('route_name'); + }); + } +}; diff --git a/be/database/migrations/2025_09_30_232256_create_notifications_table.php b/be/database/migrations/2025_09_30_232256_create_notifications_table.php new file mode 100644 index 0000000..965b521 --- /dev/null +++ b/be/database/migrations/2025_09_30_232256_create_notifications_table.php @@ -0,0 +1,31 @@ +uuid('id')->primary(); + $table->string('type'); + $table->uuidMorphs('notifiable'); + $table->text('data'); + $table->timestamp('read_at')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('notifications'); + } +}; diff --git a/be/database/migrations/2025_10_05_144351_add_fullname_to_roles_table.php b/be/database/migrations/2025_10_05_144351_add_fullname_to_roles_table.php new file mode 100644 index 0000000..c5a4a5f --- /dev/null +++ b/be/database/migrations/2025_10_05_144351_add_fullname_to_roles_table.php @@ -0,0 +1,28 @@ +string('fullname')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('roles', function (Blueprint $table) { + $table->dropColumn('fullname'); + }); + } +}; diff --git a/be/database/migrations/2025_10_08_104950_add_status_to_users_table.php b/be/database/migrations/2025_10_08_104950_add_status_to_users_table.php new file mode 100644 index 0000000..196e511 --- /dev/null +++ b/be/database/migrations/2025_10_08_104950_add_status_to_users_table.php @@ -0,0 +1,42 @@ +dropColumn('status'); + }); + + DB::statement("DROP TYPE IF EXISTS user_status_enum"); + } +}; \ No newline at end of file diff --git a/be/database/migrations/2025_10_21_100954_add_performance_indexes_to_users_table.php b/be/database/migrations/2025_10_21_100954_add_performance_indexes_to_users_table.php new file mode 100644 index 0000000..338c01c --- /dev/null +++ b/be/database/migrations/2025_10_21_100954_add_performance_indexes_to_users_table.php @@ -0,0 +1,41 @@ +index('status', 'users_status_index'); + + // Composite index for common filtering combinations + $table->index(['status', 'name'], 'users_status_name_index'); + + // Index for soft deletes (if using soft deletes frequently) + $table->index('deleted_at', 'users_deleted_at_index'); + + // Index for created_at (for sorting by creation date) + $table->index('created_at', 'users_created_at_index'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropIndex('users_status_index'); + $table->dropIndex('users_status_name_index'); + $table->dropIndex('users_deleted_at_index'); + $table->dropIndex('users_created_at_index'); + }); + } +}; \ No newline at end of file diff --git a/be/database/migrations/2025_11_19_035019_add_indexes_to_personal_access_tokens_table_for_single_session.php b/be/database/migrations/2025_11_19_035019_add_indexes_to_personal_access_tokens_table_for_single_session.php new file mode 100644 index 0000000..5bd0d1a --- /dev/null +++ b/be/database/migrations/2025_11_19_035019_add_indexes_to_personal_access_tokens_table_for_single_session.php @@ -0,0 +1,44 @@ +index(['tokenable_type', 'tokenable_id', 'name'], 'pat_tokenable_type_id_name_index'); + + // Index on name for filtering impersonation tokens and token types + $table->index('name', 'pat_name_index'); + + // Composite index for checking older tokens by user and creation date + // This optimizes queries checking for older tokens during fresh login detection + $table->index(['tokenable_type', 'tokenable_id', 'created_at'], 'pat_tokenable_created_index'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('personal_access_tokens', function (Blueprint $table) { + $table->dropIndex('pat_tokenable_type_id_name_index'); + $table->dropIndex('pat_name_index'); + $table->dropIndex('pat_tokenable_created_index'); + }); + } +}; diff --git a/be/database/migrations/2026_05_25_000001_add_active_role_id_to_personal_access_tokens_table.php b/be/database/migrations/2026_05_25_000001_add_active_role_id_to_personal_access_tokens_table.php new file mode 100644 index 0000000..ab53145 --- /dev/null +++ b/be/database/migrations/2026_05_25_000001_add_active_role_id_to_personal_access_tokens_table.php @@ -0,0 +1,27 @@ +uuid('active_role_id')->nullable()->after('expires_at'); + $table->foreign('active_role_id') + ->references('id') + ->on('roles') + ->nullOnDelete(); + }); + } + + public function down(): void + { + Schema::table('personal_access_tokens', function (Blueprint $table) { + $table->dropForeign(['active_role_id']); + $table->dropColumn('active_role_id'); + }); + } +}; diff --git a/be/database/migrations/2026_05_25_000002_add_context_to_roles_table.php b/be/database/migrations/2026_05_25_000002_add_context_to_roles_table.php new file mode 100644 index 0000000..04eb97c --- /dev/null +++ b/be/database/migrations/2026_05_25_000002_add_context_to_roles_table.php @@ -0,0 +1,28 @@ +string('context', 20)->default('member')->after('fullname'); + }); + + // Backfill known admin roles; all others remain member + DB::table('roles') + ->whereIn('name', ['DEVELOPER', 'PENTADBIR']) + ->update(['context' => 'admin']); + } + + public function down(): void + { + Schema::table('roles', function (Blueprint $table) { + $table->dropColumn('context'); + }); + } +}; diff --git a/be/database/migrations/2026_06_04_000001_create_email_verification_otps_table.php b/be/database/migrations/2026_06_04_000001_create_email_verification_otps_table.php new file mode 100644 index 0000000..b514553 --- /dev/null +++ b/be/database/migrations/2026_06_04_000001_create_email_verification_otps_table.php @@ -0,0 +1,33 @@ +uuid('id')->primary(); + $table->foreignUuid('user_id')->constrained('users')->cascadeOnDelete(); + $table->string('code'); + $table->timestamp('expires_at'); + $table->unsignedTinyInteger('attempts')->default(0); + $table->timestamps(); + + $table->index(['user_id', 'expires_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('email_verification_otps'); + } +}; diff --git a/be/database/seeders/DatabaseSeeder.php b/be/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..b7da91a --- /dev/null +++ b/be/database/seeders/DatabaseSeeder.php @@ -0,0 +1,30 @@ +create(); + + // User::factory()->create([ + // 'name' => 'Test User', + // 'email' => 'test@example.com', + // ]); + $needsSeeder = [ + PermissionsSeeder::class, + RolesSeeder::class, + RoleHasPermissionsSeeder::class, + UserSeeder::class, + ]; + $this->call($needsSeeder); + } +} diff --git a/be/database/seeders/PermissionsSeeder.php b/be/database/seeders/PermissionsSeeder.php new file mode 100644 index 0000000..e4ac213 --- /dev/null +++ b/be/database/seeders/PermissionsSeeder.php @@ -0,0 +1,29 @@ +insert( + [ + 'id' => Str::uuid(), + 'name' => $permission['name'], + 'guard_name' => $permission['guard_name'], + 'created_at' => $permission['created_at'], + 'updated_at' => $permission['updated_at'], + 'route_name' => $permission['route_name'], + ], + ); + } + } +} diff --git a/be/database/seeders/RoleHasPermissionsSeeder.php b/be/database/seeders/RoleHasPermissionsSeeder.php new file mode 100644 index 0000000..569c4a8 --- /dev/null +++ b/be/database/seeders/RoleHasPermissionsSeeder.php @@ -0,0 +1,35 @@ +where('name', $item['permission_name']) + ->where('guard_name', $guardName) + ->first(); + + $role = DB::table('roles') + ->where('name', $item['role_name']) + ->where('guard_name', $guardName) + ->first(); + + DB::table('role_has_permissions')->insert([ + 'permission_id' => $permission->id, + 'role_id' => $role->id, + ]); + } + } +} diff --git a/be/database/seeders/RolesSeeder.php b/be/database/seeders/RolesSeeder.php new file mode 100644 index 0000000..4bd9f23 --- /dev/null +++ b/be/database/seeders/RolesSeeder.php @@ -0,0 +1,29 @@ +insert( + [ + 'id' => Str::uuid(), + 'name' => $role['name'], + 'guard_name' => $role['guard_name'], + 'created_at' => $role['created_at'], + 'updated_at' => $role['updated_at'], + 'fullname' => $role['fullname'], + ] + ); + } + } +} diff --git a/be/database/seeders/UserSeeder.php b/be/database/seeders/UserSeeder.php new file mode 100644 index 0000000..7940c6e --- /dev/null +++ b/be/database/seeders/UserSeeder.php @@ -0,0 +1,33 @@ + 'ismail@developer.com'], + [ + 'name' => 'DEVELOPER', + 'phone_number' => '0123456789', + 'password' => bcrypt('Topazthegoat'), + 'status' => 'active', + ] + ); + + // Find the DEVELOPER role + $developerRole = Role::findByName('DEVELOPER', 'api'); + + if (! $user->hasRole('DEVELOPER')) { + $user->assignRole($developerRole); + $this->command->info('DEVELOPER role assigned to user: '.$user->email); + } else { + $this->command->info('User already has DEVELOPER role: '.$user->email); + } + } +} diff --git a/be/database/seeders/VisibilityPermissionsSeeder.php b/be/database/seeders/VisibilityPermissionsSeeder.php new file mode 100644 index 0000000..47257dd --- /dev/null +++ b/be/database/seeders/VisibilityPermissionsSeeder.php @@ -0,0 +1,37 @@ + 'akses peringkat formasi', + 'guard_name' => 'api', + 'description' => 'Can view all data under their government level' + ], + [ + 'name' => 'akses peringkat divisyen', + 'guard_name' => 'api', + 'description' => 'Can view all data under their formation (DIV) level' + ] + ]; + + foreach ($permissions as $permission) { + Permission::firstOrCreate( + ['name' => $permission['name'], 'guard_name' => $permission['guard_name']], + $permission + ); + } + + $this->command->info('Visibility permissions created successfully!'); + } +} diff --git a/be/database/seeders/json/permissions_20251014.json b/be/database/seeders/json/permissions_20251014.json new file mode 100644 index 0000000..1646a64 --- /dev/null +++ b/be/database/seeders/json/permissions_20251014.json @@ -0,0 +1,11 @@ +{ + "permissions": [ + { + "name": "DEVELOPER", + "guard_name": "api", + "created_at": "2025-07-13T00:03:02.000Z", + "updated_at": "2025-07-13T00:03:02.000Z", + "route_name": "superadminRoutes" + } + ] +} \ No newline at end of file diff --git a/be/database/seeders/json/role_has_permissions_20251014.json b/be/database/seeders/json/role_has_permissions_20251014.json new file mode 100644 index 0000000..7df15df --- /dev/null +++ b/be/database/seeders/json/role_has_permissions_20251014.json @@ -0,0 +1,9 @@ +{ + "role_has_permissions": [ + { + "role_name": "DEVELOPER", + "permission_name": "DEVELOPER", + "guard_name": "api" + } + ] +} \ No newline at end of file diff --git a/be/database/seeders/json/roles_20251014.json b/be/database/seeders/json/roles_20251014.json new file mode 100644 index 0000000..e5ea8f2 --- /dev/null +++ b/be/database/seeders/json/roles_20251014.json @@ -0,0 +1,12 @@ +{ + "roles": [ + { + "name": "DEVELOPER", + "guard_name": "api", + "created_at": "2025-06-21T22:49:54.000Z", + "updated_at": "2025-07-22T05:38:08.000Z", + "deleted_at": null, + "fullname": null + } + ] +} \ No newline at end of file diff --git a/be/docker-compose.yml b/be/docker-compose.yml new file mode 100644 index 0000000..c0b34fc --- /dev/null +++ b/be/docker-compose.yml @@ -0,0 +1,89 @@ +services: + laravel.test: + build: + context: "./vendor/laravel/sail/runtimes/8.4" + dockerfile: Dockerfile + args: + WWWGROUP: "${WWWGROUP}" + image: "sail-8.4/app" + extra_hosts: + - "host.docker.internal:host-gateway" + ports: + - "${APP_PORT:-80}:80" + - "${VITE_PORT:-5173}:${VITE_PORT:-5173}" + environment: + WWWUSER: "${WWWUSER}" + LARAVEL_SAIL: 1 + XDEBUG_MODE: "${SAIL_XDEBUG_MODE:-off}" + XDEBUG_CONFIG: "${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}" + IGNITION_LOCAL_SITES_PATH: "${PWD}" + volumes: + - ".:/var/www/html" + networks: + - sail + depends_on: + - pgsql + - redis + - memcached + - mailpit + pgsql: + image: "postgres:17-alpine" + ports: + - "${FORWARD_DB_PORT:-5432}:5432" + environment: + PGPASSWORD: "${DB_PASSWORD:-secret}" + POSTGRES_DB: "${DB_DATABASE}" + POSTGRES_USER: "${DB_USERNAME}" + POSTGRES_PASSWORD: "${DB_PASSWORD:-secret}" + volumes: + - "sail-pgsql:/var/lib/postgresql/data" + - "./vendor/laravel/sail/database/pgsql/create-testing-database.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql" + networks: + - sail + healthcheck: + test: + - CMD + - pg_isready + - "-q" + - "-d" + - "${DB_DATABASE}" + - "-U" + - "${DB_USERNAME}" + retries: 3 + timeout: 5s + redis: + image: "redis:alpine" + ports: + - "${FORWARD_REDIS_PORT:-6379}:6379" + volumes: + - "sail-redis:/data" + networks: + - sail + healthcheck: + test: + - CMD + - redis-cli + - ping + retries: 3 + timeout: 5s + memcached: + image: "memcached:alpine" + ports: + - "${FORWARD_MEMCACHED_PORT:-11211}:11211" + networks: + - sail + mailpit: + image: "axllent/mailpit:latest" + ports: + - "${FORWARD_MAILPIT_PORT:-1025}:1025" + - "${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025" + networks: + - sail +networks: + sail: + driver: bridge +volumes: + sail-pgsql: + driver: local + sail-redis: + driver: local diff --git a/be/docker/common/php-fpm/supervisor/historical-data-capture.conf b/be/docker/common/php-fpm/supervisor/historical-data-capture.conf new file mode 100644 index 0000000..c4394e0 --- /dev/null +++ b/be/docker/common/php-fpm/supervisor/historical-data-capture.conf @@ -0,0 +1,12 @@ +[program:historical-data-capture] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/artisan historical:capture +autostart=true +autorestart=true +user=www-data +redirect_stderr=true +stdout_logfile=/var/www/storage/logs/historical-data-capture.log +stopwaitsecs=3600 +stopasgroup=true +killasgroup=true +numprocs=1 \ No newline at end of file diff --git a/be/docker/common/php-fpm/supervisor/historical-data-scheduler.conf b/be/docker/common/php-fpm/supervisor/historical-data-scheduler.conf new file mode 100644 index 0000000..2f9f738 --- /dev/null +++ b/be/docker/common/php-fpm/supervisor/historical-data-scheduler.conf @@ -0,0 +1,12 @@ +[program:historical-data-scheduler] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/artisan schedule:work +autostart=true +autorestart=true +user=www-data +redirect_stderr=true +stdout_logfile=/var/www/storage/logs/historical-data-scheduler.log +stopwaitsecs=3600 +stopasgroup=true +killasgroup=true +numprocs=1 \ No newline at end of file diff --git a/be/docker/common/php-fpm/supervisor/laravel-horizon.conf b/be/docker/common/php-fpm/supervisor/laravel-horizon.conf new file mode 100644 index 0000000..c763d4e --- /dev/null +++ b/be/docker/common/php-fpm/supervisor/laravel-horizon.conf @@ -0,0 +1,12 @@ +[program:laravel-horizon] +process_name=%(program_name)s +command=php /var/www/artisan horizon +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +redirect_stderr=true +stdout_logfile=/var/www/storage/logs/horizon.log +stopwaitsecs=3600 +startsecs=10 \ No newline at end of file diff --git a/be/docker/common/php-fpm/supervisor/laravel-scheduler.conf b/be/docker/common/php-fpm/supervisor/laravel-scheduler.conf new file mode 100644 index 0000000..d82b6d4 --- /dev/null +++ b/be/docker/common/php-fpm/supervisor/laravel-scheduler.conf @@ -0,0 +1,8 @@ +[program:laravel-scheduler] +process_name=%(program_name)s +command=bash -c "while [ true ]; do (php /var/www/artisan schedule:run --verbose --no-interaction &); sleep 60; done" +autostart=true +autorestart=true +user=www-data +redirect_stderr=true +stdout_logfile=/var/www/storage/logs/scheduler.log \ No newline at end of file diff --git a/be/docker/common/php-fpm/supervisor/laravel-worker.conf b/be/docker/common/php-fpm/supervisor/laravel-worker.conf new file mode 100644 index 0000000..0e4cb1f --- /dev/null +++ b/be/docker/common/php-fpm/supervisor/laravel-worker.conf @@ -0,0 +1,12 @@ +[program:laravel-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +user=www-data +numprocs=2 +redirect_stderr=true +stdout_logfile=/var/www/storage/logs/worker.log +stopwaitsecs=3600 \ No newline at end of file diff --git a/be/docker/common/php-fpm/supervisor/php-fpm.conf b/be/docker/common/php-fpm/supervisor/php-fpm.conf new file mode 100644 index 0000000..7fb1199 --- /dev/null +++ b/be/docker/common/php-fpm/supervisor/php-fpm.conf @@ -0,0 +1,7 @@ +[program:php-fpm] +command=php-fpm --nodaemonize +autostart=true +autorestart=true +priority=5 +stdout_logfile=/var/www/storage/logs/php-fpm.log +stderr_logfile=/var/www/storage/logs/php-fpm-error.log \ No newline at end of file diff --git a/be/docker/common/php-fpm/supervisor/supervisord.conf b/be/docker/common/php-fpm/supervisor/supervisord.conf new file mode 100644 index 0000000..bb5bb49 --- /dev/null +++ b/be/docker/common/php-fpm/supervisor/supervisord.conf @@ -0,0 +1,18 @@ +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0700 + +[supervisord] +logfile=/var/www/storage/logs/supervisord.log +pidfile=/var/run/supervisord.pid +childlogdir=/var/www/storage/logs/ +nodaemon=true + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[include] +files = /etc/supervisor/conf.d/*.conf \ No newline at end of file diff --git a/be/docker/common/unified/Dockerfile b/be/docker/common/unified/Dockerfile new file mode 100644 index 0000000..c586227 --- /dev/null +++ b/be/docker/common/unified/Dockerfile @@ -0,0 +1,175 @@ +# Stage 0: Build Vue frontend assets with Node.js +FROM node:22 AS frontend-builder + +WORKDIR /app/frontend + +# Copy frontend package files +COPY SUTERA-frontend/package*.json ./ +COPY SUTERA-frontend/pnpm-lock.yaml* ./ + +# Install pnpm and frontend dependencies (including dev dependencies for build) +RUN npm install -g pnpm +RUN pnpm install --frozen-lockfile + +# Copy frontend source code +COPY SUTERA-frontend/ ./ + +# Build TWO frontend bundles (path-based): +# - Production: served at / +# - Training: served at /training/ +# +# This avoids runtime JS injection and allows training/prod UI differences +# while keeping a single backend image. +RUN pnpm run typecheck \ + && pnpm exec vite build --mode=production --base=/ --outDir dist-prod \ + && pnpm exec vite build --mode=training --base=/training/ --outDir dist-training + +# Clean up dev dependencies to reduce image size +ENV CI=true +RUN pnpm prune --prod + +# Stage 1: Build environment and Composer dependencies +FROM php:8.4-fpm AS builder + +LABEL maintainer="Topaz" + +# Install system dependencies and PHP extensions for Laravel with MySQL/PostgreSQL support. +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + unzip \ + libpq-dev \ + libonig-dev \ + libssl-dev \ + libxml2-dev \ + libcurl4-openssl-dev \ + libicu-dev \ + libzip-dev \ + libjpeg-dev \ + libpng-dev \ + libfreetype6-dev \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install -j$(nproc) \ + pdo_mysql \ + pdo_pgsql \ + pgsql \ + opcache \ + intl \ + zip \ + bcmath \ + soap \ + gd \ + pcntl \ + && pecl install redis \ + && docker-php-ext-enable redis \ + && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Set the working directory inside the container +WORKDIR /var/www + +# Copy the entire Laravel application code into the container +COPY SUTERA-backend/ /var/www + +# Copy Composer from official image (avoids flaky getcomposer.org in CI) +COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer + +# Install PHP dependencies +RUN composer install --no-dev --optimize-autoloader --no-interaction --no-progress --prefer-dist --no-scripts \ + && rm -rf bootstrap/cache/services.php bootstrap/cache/packages.php + +# Stage 2: Unified production image with PHP-FPM + Nginx +FROM php:8.4-fpm AS base + +LABEL maintainer="Topaz" + +# Set system timezone to Asia/Kuala_Lumpur +RUN apt-get update && apt-get install -y --no-install-recommends tzdata \ + && ln -snf /usr/share/zoneinfo/Asia/Kuala_Lumpur /etc/localtime \ + && echo "Asia/Kuala_Lumpur" > /etc/timezone \ + && dpkg-reconfigure -f noninteractive tzdata \ + && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Install Nginx and all runtime dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + nginx \ + libpq-dev \ + libicu-dev \ + libzip-dev \ + libpng-dev \ + libjpeg-dev \ + libfreetype6-dev \ + libfcgi-bin \ + procps \ + netcat-openbsd \ + supervisor \ + nano \ + curl \ + && docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install gd \ + && pecl install redis \ + && docker-php-ext-enable redis \ + && apt-get autoremove -y && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +# Download and install php-fpm health check script +RUN curl -o /usr/local/bin/php-fpm-healthcheck \ + https://raw.githubusercontent.com/renatomefi/php-fpm-healthcheck/master/php-fpm-healthcheck \ + && chmod +x /usr/local/bin/php-fpm-healthcheck + +# Copy the initialization script +COPY SUTERA-backend/docker/common/unified/entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + +# Copy supervisor configuration files +COPY SUTERA-backend/docker/common/unified/supervisor/supervisord.conf /etc/supervisor/supervisord.conf + +# Create supervisor conf.d directory +RUN mkdir -p /etc/supervisor/conf.d + +# Use common supervisor and nginx config inside the image. +# If you need environment-specific behavior (training vs production), override at runtime +# by mounting files into: +# - /etc/nginx/nginx.conf +# - /etc/supervisor/conf.d/ +COPY SUTERA-backend/docker/common/unified/supervisor/unified.conf /etc/supervisor/conf.d/unified.conf +COPY SUTERA-backend/docker/common/unified/nginx/nginx.conf /etc/nginx/nginx.conf + +# Copy the initial storage structure +COPY SUTERA-backend/storage /var/www/storage-init + +# Copy PHP extensions and libraries from the builder stage +COPY --from=builder /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/ +COPY --from=builder /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/ +COPY --from=builder /usr/local/bin/docker-php-ext-* /usr/local/bin/ + +# Use the recommended production PHP configuration +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" + +# Enable PHP-FPM status page via separate config file (avoid modifying zz-docker.conf) +RUN echo '[www]' > /usr/local/etc/php-fpm.d/zzz-status.conf && \ + echo 'pm.status_path = /status' >> /usr/local/etc/php-fpm.d/zzz-status.conf + +# Copy the application code and dependencies from the build stage first +COPY --from=builder /var/www /var/www + +# Copy the built Vue frontends from frontend-builder stage +# - Production frontend at / +COPY --from=frontend-builder /app/frontend/dist-prod /var/www/public/ +# - Training frontend at /training/ +COPY --from=frontend-builder /app/frontend/dist-training /var/www/public/training/ + +# Set working directory +WORKDIR /var/www + +# Ensure correct permissions +RUN chown -R www-data:www-data /var/www + +# Create Nginx log directory +RUN mkdir -p /var/log/nginx && chown -R www-data:www-data /var/log/nginx + +# Run the entrypoint script +ENTRYPOINT ["/usr/local/bin/entrypoint.sh"] + +# Expose port 80 (Nginx) and 9000 (PHP-FPM for health checks) +EXPOSE 80 9000 + +# Start supervisor which manages both Nginx and PHP-FPM +CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/be/docker/common/unified/entrypoint.sh b/be/docker/common/unified/entrypoint.sh new file mode 100644 index 0000000..a3891da --- /dev/null +++ b/be/docker/common/unified/entrypoint.sh @@ -0,0 +1,69 @@ +#!/bin/sh +set -e + +echo "Starting Laravel entrypoint (Unified)..." + +# Step 0: Wait for DB if configured +if [ -n "${DB_HOST:-}" ]; then + echo "Waiting for DB at ${DB_HOST}:${DB_PORT:-5432}..." + # Wait up to ~60s (60 * 1s) + i=0 + while ! nc -z "$DB_HOST" "${DB_PORT:-5432}"; do + i=$((i + 1)) + if [ "$i" -ge 60 ]; then + echo "DB not reachable after 60s. Continuing anyway." + break + fi + sleep 1 + done + echo "DB check finished." +fi + +# Step 1: Initialize persistent storage if needed +if [ ! "$(ls -A /var/www/storage 2>/dev/null)" ]; then + echo "Initializing /var/www/storage..." + cp -R /var/www/storage-init/. /var/www/storage + chown -R www-data:www-data /var/www/storage +else + echo "/var/www/storage already initialized." +fi + +rm -rf /var/www/storage-init + +# Step 2: Ensure environment is ready +if [ ! -f .env ]; then + echo ".env file is missing!" + echo "Provide it at runtime (bind mount, env_file, or secret) to /var/www/.env." + exit 1 +fi + +# Ensure bootstrap/cache directory exists and writable BEFORE artisan +if [ ! -d bootstrap/cache ]; then + echo "Creating bootstrap/cache directory..." + mkdir -p bootstrap/cache +fi +chown -R www-data:www-data bootstrap/cache + +# Step 3: Laravel setup and optimization +php artisan storage:link || true +php artisan config:clear +php artisan config:cache +php artisan route:cache +php artisan event:cache +php artisan package:discover --ansi + +# Step 4: Set permissions +chown -R www-data:www-data /var/www/storage +chown -R www-data:www-data /var/www/bootstrap/cache + +# Step 5: Ensure Laravel log directory exists +mkdir -p /var/www/storage/logs +chown -R www-data:www-data /var/www/storage/logs + +# Step 6: Create supervisor log directories +mkdir -p /var/log/supervisor +chown -R root:root /var/log/supervisor + +echo "Laravel setup complete. Starting Supervisor..." +exec supervisord -c /etc/supervisor/supervisord.conf + diff --git a/be/docker/common/unified/nginx/nginx.conf b/be/docker/common/unified/nginx/nginx.conf new file mode 100644 index 0000000..474e99d --- /dev/null +++ b/be/docker/common/unified/nginx/nginx.conf @@ -0,0 +1,108 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # HTTP server (default - works for training/dev) + # For production with HTTPS, mount a custom nginx.conf that includes SSL config + server { + listen 80; + server_name _; + root /var/www/public; + index index.php index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # API routes - pass directly to Laravel with original REQUEST_URI + # (try_files would redirect to /index.php and lose the path, causing 502/bad routing) + location /api/ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # Training SPA (built with base=/training/) + # Serve training frontend routes from /var/www/public/training + location ^~ /training/ { + try_files $uri $uri/ /training/index.html; + } + + # Serve frontend assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri =404; + } + + # Handle all other routes - serve Vue app or Laravel + location / { + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri $uri/ /index.html /index.php?$query_string; + } + + # Handle PHP files - connect to localhost PHP-FPM + location ~ \.php$ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + + # Additional FastCGI parameters + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + } + + # Cache static assets + location ~* \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri =404; + } + + location ^~ /horizon { + add_header Content-Security-Policy "default-src 'self' http: https: data: blob 'unsafe-inline' 'unsafe-eval'" always; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + } + + # HTTPS server block removed from default config + # To enable HTTPS in production: + # 1. Mount SSL certificates to /etc/nginx/ssl/ + # 2. Mount a custom nginx.conf that includes HTTPS server block + # Example: - ./production-nginx.conf:/etc/nginx/nginx.conf:ro +} diff --git a/be/docker/common/unified/redis/redis.conf b/be/docker/common/unified/redis/redis.conf new file mode 100644 index 0000000..6008e97 --- /dev/null +++ b/be/docker/common/unified/redis/redis.conf @@ -0,0 +1,3 @@ +port 6379 +bind 0.0.0.0 +requirepass sutera_redis@2025 \ No newline at end of file diff --git a/be/docker/common/unified/supervisor/supervisord.conf b/be/docker/common/unified/supervisor/supervisord.conf new file mode 100644 index 0000000..a014b68 --- /dev/null +++ b/be/docker/common/unified/supervisor/supervisord.conf @@ -0,0 +1,19 @@ +[unix_http_server] +file=/var/run/supervisor.sock +chmod=0700 + +[supervisord] +nodaemon=true +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid +childlogdir=/var/log/supervisor +user=root + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface + +[supervisorctl] +serverurl=unix:///var/run/supervisor.sock + +[include] +files = /etc/supervisor/conf.d/*.conf diff --git a/be/docker/common/unified/supervisor/unified.conf b/be/docker/common/unified/supervisor/unified.conf new file mode 100644 index 0000000..48a5b94 --- /dev/null +++ b/be/docker/common/unified/supervisor/unified.conf @@ -0,0 +1,48 @@ +[program:php-fpm] +command=php-fpm -F +autostart=true +autorestart=true +stderr_logfile=/var/log/supervisor/php-fpm.err.log +stdout_logfile=/var/log/supervisor/php-fpm.out.log +user=root +priority=100 + +[program:nginx] +command=nginx -g "daemon off;" +autostart=true +autorestart=true +stderr_logfile=/var/log/supervisor/nginx.err.log +stdout_logfile=/var/log/supervisor/nginx.out.log +user=root +priority=200 + +[program:laravel-horizon] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/artisan horizon +autostart=true +autorestart=true +user=www-data +redirect_stderr=true +stdout_logfile=/var/www/storage/logs/horizon.log +stopwaitsecs=3600 +priority=300 + +[program:laravel-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 +autostart=true +autorestart=true +user=www-data +redirect_stderr=true +stdout_logfile=/var/www/storage/logs/worker.log +stopwaitsecs=3600 +priority=400 + +[program:laravel-scheduler] +command=php /var/www/artisan schedule:work +autostart=true +autorestart=true +user=www-data +redirect_stderr=true +stdout_logfile=/var/www/storage/logs/scheduler.log +priority=500 diff --git a/be/docker/production/docker-compose.production.yml b/be/docker/production/docker-compose.production.yml new file mode 100644 index 0000000..7bc1e8a --- /dev/null +++ b/be/docker/production/docker-compose.production.yml @@ -0,0 +1,77 @@ +services: + app: + build: + context: . + dockerfile: ./SUTERA-backend/docker/common/unified/Dockerfile + platforms: + - linux/amd64 + - linux/arm64 + image: git.ismailmasseran.com/topaz/sutera:v3.0.1b + container_name: sutera-app-production + restart: unless-stopped + ports: + - "${APP_PORT:-80}:80" + environment: + - APP_ENV=production + - APP_DEBUG=true + - REDIS_HOST=redis + - REDIS_PASSWORD=${REDIS_PASSWORD} + - CACHE_DRIVER=redis + - SESSION_DRIVER=redis + - QUEUE_CONNECTION=redis + volumes: + - app-storage-production:/var/www/storage + - app-logs-production:/var/www/storage/logs + - ./.env.production:/var/www/.env:ro + - ./SUTERA-backend/docker/production/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + networks: + - sutera-production-network + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # Redis for caching and sessions + redis: + image: redis:8 + container_name: sutera-redis-production + restart: unless-stopped + volumes: + - redis-data-production:/data + - ./redis-config/redis.conf:/usr/local/etc/redis/redis.conf:ro + command: redis-server /usr/local/etc/redis/redis.conf + networks: + - sutera-production-network + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + sutera-production-network: + driver: bridge + +volumes: + app-storage-production: + driver: local + app-logs-production: + driver: local + redis-data-production: + driver: local diff --git a/be/docker/production/nginx/nginx.conf b/be/docker/production/nginx/nginx.conf new file mode 100644 index 0000000..ac3c643 --- /dev/null +++ b/be/docker/production/nginx/nginx.conf @@ -0,0 +1,100 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Production HTTP server + server { + listen 80; + server_name _; + root /var/www/public; + index index.php index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # API routes - pass directly to Laravel with original REQUEST_URI + location / { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # PRODUCTION: /training/ path is disabled - return 404 + location ^~ /training/ { + return 404; + } + + # Serve frontend assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri =404; + } + + # Handle all other routes - serve Vue app or Laravel + location / { + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri $uri/ /index.html /index.php?$query_string; + } + + # Handle PHP files - connect to localhost PHP-FPM + location ~ \.php$ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + + # Additional FastCGI parameters + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + } + + # Health check endpoint for Docker + location = /health { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + } + + location ^~ /horizon { + add_header Content-Security-Policy "default-src 'self' http: https: data: blob 'unsafe-inline' 'unsafe-eval'" always; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + } +} diff --git a/be/docker/production/redis/redis.conf b/be/docker/production/redis/redis.conf new file mode 100644 index 0000000..6008e97 --- /dev/null +++ b/be/docker/production/redis/redis.conf @@ -0,0 +1,3 @@ +port 6379 +bind 0.0.0.0 +requirepass sutera_redis@2025 \ No newline at end of file diff --git a/be/docker/staging/docker-compose.staging.yml b/be/docker/staging/docker-compose.staging.yml new file mode 100644 index 0000000..4c13517 --- /dev/null +++ b/be/docker/staging/docker-compose.staging.yml @@ -0,0 +1,117 @@ +services: + app: + build: + context: . + dockerfile: ./SUTERA-backend/docker/common/unified/Dockerfile + args: + ENV: staging + platforms: + - linux/amd64 + - linux/arm64 + image: sutera-app-staging:latest + container_name: sutera-app-staging + restart: unless-stopped + ports: + - "${APP_PORT:-80}:80" + - "${HTTPS_PORT:-443}:443" + environment: + - APP_ENV=staging + - APP_DEBUG=true + - DB_HOST=postgres + - DB_PORT=${DB_PORT:-5432} + - DB_DATABASE=${DB_DATABASE} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - REDIS_HOST=redis + - REDIS_PASSWORD=${REDIS_PASSWORD} + - CACHE_DRIVER=redis + - SESSION_DRIVER=redis + - QUEUE_CONNECTION=redis + volumes: + - app-storage-staging:/var/www/storage + - app-logs-staging:/var/www/storage/logs + - /etc/letsencrypt/live/sutera.ismailmasseran.com/fullchain.pem:/etc/nginx/ssl/fullchain.pem:ro + - /etc/letsencrypt/live/sutera.ismailmasseran.com/privkey.pem:/etc/nginx/ssl/privkey.pem:ro + networks: + - sutera-staging-network + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # Redis for caching and sessions + redis: + image: redis:8.2-alpine + container_name: sutera-redis-staging + restart: unless-stopped + volumes: + - redis-data-staging:/data + - ./redis-config/redis.conf:/usr/local/etc/redis/redis.conf:ro + command: redis-server /usr/local/etc/redis/redis.conf + networks: + - sutera-staging-network + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # Optional: Database (only if you want to run DB on same server for staging) + # Uncomment if you want to run MySQL on the same server for staging + postgres: + image: postgres:17 + container_name: sutera-postgres-staging + restart: unless-stopped + ports: + - "${DB_PORT:-5432}:5432" + environment: + - POSTGRES_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + - POSTGRES_DATABASE=${DB_DATABASE} + - POSTGRES_USER=${DB_USERNAME} + - POSTGRES_PASSWORD=${DB_PASSWORD} + volumes: + - postgres-data-staging:/var/lib/postgres + - ./SUTERA-backend/docker/postgres/conf.d:/etc/postgres/conf.d:ro + networks: + - sutera-staging-network + healthcheck: + test: ["CMD", "postgres", "ping", "-h", "localhost"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + sutera-staging-network: + driver: bridge + +volumes: + app-storage-staging: + driver: local + app-logs-staging: + driver: local + redis-data-staging: + driver: local + postgres-data-staging: + driver: local diff --git a/be/docker/staging/nginx/nginx.conf b/be/docker/staging/nginx/nginx.conf new file mode 100644 index 0000000..d477bfc --- /dev/null +++ b/be/docker/staging/nginx/nginx.conf @@ -0,0 +1,100 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Production HTTP server + server { + listen 80; + server_name _; + root /var/www/public; + index index.php index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # API routes - pass directly to Laravel with original REQUEST_URI + location /api/ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # PRODUCTION: /training/ path is disabled - return 404 + location ^~ /training/ { + return 404; + } + + # Serve frontend assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri =404; + } + + # Handle all other routes - serve Vue app or Laravel + location / { + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri $uri/ /index.html /index.php?$query_string; + } + + # Handle PHP files - connect to localhost PHP-FPM + location ~ \.php$ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + + # Additional FastCGI parameters + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + } + + # Health check endpoint for Docker + location = /health { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + } + + location ^~ /horizon { + add_header Content-Security-Policy "default-src 'self' http: https: data: blob 'unsafe-inline' 'unsafe-eval'" always; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + } +} diff --git a/be/docker/staging/redis/redis.conf b/be/docker/staging/redis/redis.conf new file mode 100644 index 0000000..6008e97 --- /dev/null +++ b/be/docker/staging/redis/redis.conf @@ -0,0 +1,3 @@ +port 6379 +bind 0.0.0.0 +requirepass sutera_redis@2025 \ No newline at end of file diff --git a/be/docker/training/docker-compose.training.yml b/be/docker/training/docker-compose.training.yml new file mode 100644 index 0000000..16efeae --- /dev/null +++ b/be/docker/training/docker-compose.training.yml @@ -0,0 +1,114 @@ +services: + app: + build: + context: . + dockerfile: ./SUTERA-backend/docker/common/unified/Dockerfile + platforms: + - linux/amd64 + - linux/arm64 + image: git.ismailmasseran.com/topaz/sutera:33a53e08a641152f255f5c3fa27765200add4f45 + container_name: sutera-app-training + restart: unless-stopped + ports: + - "${APP_PORT:-80}:80" + environment: + - APP_ENV=training + - APP_DEBUG=true + - DB_HOST=postgres + - DB_PORT=${DB_PORT:-5432} + - DB_DATABASE=${DB_DATABASE} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - REDIS_HOST=redis + - REDIS_PASSWORD=${REDIS_PASSWORD} + - CACHE_DRIVER=redis + - SESSION_DRIVER=redis + - QUEUE_CONNECTION=redis + volumes: + - app-storage-training:/var/www/storage + - app-logs-training:/var/www/storage/logs + - ./.env.training:/var/www/.env:ro + - ./SUTERA-backend/docker/training/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + networks: + - sutera-training-network + depends_on: + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # Redis for caching and sessions + redis: + image: redis:8 + container_name: sutera-redis-training + restart: unless-stopped + volumes: + - redis-data-training:/data + - ./redis-config/redis.conf:/usr/local/etc/redis/redis.conf:ro + command: redis-server /usr/local/etc/redis/redis.conf + networks: + - sutera-training-network + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 10s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + + # Optional: Database (only if you want to run DB on same server for training) + # Uncomment if you want to run MySQL on the same server for training + postgres: + image: postgres:17 + container_name: sutera-postgres-training + restart: unless-stopped + ports: + - "${DB_PORT:-5432}:5432" + environment: + - POSTGRES_ROOT_PASSWORD=${DB_ROOT_PASSWORD} + - POSTGRES_DATABASE=${DB_DATABASE} + - POSTGRES_USER=${DB_USERNAME} + - POSTGRES_PASSWORD=${DB_PASSWORD} + volumes: + - postgres-data-training:/var/lib/postgres + - ./SUTERA-backend/docker/postgres/conf.d:/etc/postgres/conf.d:ro + networks: + - sutera-training-network + healthcheck: + test: ["CMD", "postgres", "ping", "-h", "localhost"] + interval: 15s + timeout: 10s + retries: 5 + start_period: 30s + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + sutera-training-network: + driver: bridge + +volumes: + app-storage-training: + driver: local + app-logs-training: + driver: local + redis-data-training: + driver: local + postgres-data-training: + driver: local diff --git a/be/docker/training/nginx/nginx.conf b/be/docker/training/nginx/nginx.conf new file mode 100644 index 0000000..8a32824 --- /dev/null +++ b/be/docker/training/nginx/nginx.conf @@ -0,0 +1,106 @@ +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + # Training HTTP server + server { + listen 80; + server_name _; + root /var/www/public; + index index.php index.html; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header X-Content-Type-Options "nosniff" always; + add_header Referrer-Policy "no-referrer-when-downgrade" always; + + # API routes - pass directly to Laravel with original REQUEST_URI + location /api/ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # Training SPA (built with base=/training/) + # Serve training frontend routes from /var/www/public/training + location ^~ /training/ { + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri $uri/ /training/index.html; + } + + # Serve assets under /training/assets/ + location ~* ^/training/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always; + try_files $uri =404; + } + + # Handle PHP files - connect to localhost PHP-FPM + location ~ \.php$ { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + include fastcgi_params; + + # Additional FastCGI parameters + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # Deny access to hidden files + location ~ /\. { + deny all; + } + + # Health check endpoint for Docker + location = /health { + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + } + + location ^~ /horizon { + add_header Content-Security-Policy "default-src 'self' http: https: data: blob 'unsafe-inline' 'unsafe-eval'" always; + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; + include fastcgi_params; + fastcgi_param HTTP_PROXY ""; + fastcgi_param HTTPS $https if_not_empty; + fastcgi_read_timeout 300; + fastcgi_send_timeout 300; + } + + # TRAINING: Root path disabled - redirect to /training/ + location = / { + return 302 /training/; + } + + # TRAINING: All other paths return 404 (production frontend disabled) + location / { + return 404; + } + } +} diff --git a/be/docker/training/redis/redis.conf b/be/docker/training/redis/redis.conf new file mode 100644 index 0000000..6008e97 --- /dev/null +++ b/be/docker/training/redis/redis.conf @@ -0,0 +1,3 @@ +port 6379 +bind 0.0.0.0 +requirepass sutera_redis@2025 \ No newline at end of file diff --git a/be/modules_statuses.json b/be/modules_statuses.json new file mode 100644 index 0000000..c645c10 --- /dev/null +++ b/be/modules_statuses.json @@ -0,0 +1,29 @@ +{ + "Auth": true, + "KJCVariant": true, + "Rank": true, + "Position": true, + "Unit": true, + "Formation": true, + "Camp": true, + "User": true, + "KJC": true, + "KJCAssetEntitlement": true, + "KJCAssetHolding": true, + "KJCSparePart": true, + "KJCRepair": true, + "KJCRepairDirective": true, + "PKJAssetEntitlement": true, + "PKJVariant": true, + "PKJAssetHolding": true, + "ActivityLog": true, + "Role": true, + "KJCReport": true, + "Dashboard": true, + "PKJReport": true, + "ExternalAPI": true, + "KJCTemporaryLoan": true, + "Notification": true, + "KJCRepairTool": true, + "Feedback": true +} \ No newline at end of file diff --git a/be/package.json b/be/package.json new file mode 100644 index 0000000..a5707d8 --- /dev/null +++ b/be/package.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "@tailwindcss/vite": "^4.0.0", + "axios": "^1.11.0", + "concurrently": "^9.0.1", + "laravel-vite-plugin": "^2.0.0", + "tailwindcss": "^4.0.0", + "vite": "^7.0.4" + } +} diff --git a/be/phpunit.xml b/be/phpunit.xml new file mode 100644 index 0000000..6b25d0d --- /dev/null +++ b/be/phpunit.xml @@ -0,0 +1,33 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + diff --git a/be/public/.htaccess b/be/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/be/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/be/public/favicon.ico b/be/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/be/public/images/logo/Logo-RAJD.png b/be/public/images/logo/Logo-RAJD.png new file mode 100644 index 0000000..37e434c Binary files /dev/null and b/be/public/images/logo/Logo-RAJD.png differ diff --git a/be/public/index.php b/be/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/be/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/be/public/robots.txt b/be/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/be/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/be/resources/css/app.css b/be/resources/css/app.css new file mode 100644 index 0000000..3e6abea --- /dev/null +++ b/be/resources/css/app.css @@ -0,0 +1,11 @@ +@import 'tailwindcss'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../**/*.blade.php'; +@source '../**/*.js'; + +@theme { + --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', + 'Segoe UI Symbol', 'Noto Color Emoji'; +} diff --git a/be/resources/js/app.js b/be/resources/js/app.js new file mode 100644 index 0000000..e59d6a0 --- /dev/null +++ b/be/resources/js/app.js @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/be/resources/js/bootstrap.js b/be/resources/js/bootstrap.js new file mode 100644 index 0000000..5f1390b --- /dev/null +++ b/be/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; +window.axios = axios; + +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/be/resources/views/errors/503.blade.php b/be/resources/views/errors/503.blade.php new file mode 100644 index 0000000..725cfb9 --- /dev/null +++ b/be/resources/views/errors/503.blade.php @@ -0,0 +1,186 @@ + + + + + + SERVIS TERGENDALA - MYKOPKB + + + +
+
+ +
+ +
+

Sistem Dalam Penyelenggaraan

+
+
+ +

+ Sistem MYKOPKB sedang menjalani penyelenggaraan berjadual untuk meningkatkan prestasi dan keselamatan. + Kami mohon maaf atas sebarang kesulitan yang dihadapi. +

+ +
+
+ + + + Status Penyelenggaraan +
+

+ Sistem dijangka aktif semula pada:
+ {{ now()->addHours(2)->format('d/m/Y \p\a\d\a H:i') }} +

+
+ +
+

🔧 Penambahbaikan sistem sedang dijalankan

+

📧 Untuk kecemasan, hubungi: mykopkb@gmail.com

+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/be/routes/api.php b/be/routes/api.php new file mode 100644 index 0000000..0c60ae2 --- /dev/null +++ b/be/routes/api.php @@ -0,0 +1,32 @@ +middleware(['auth:sanctum', 'single.session']); + +// Social Media - Admin endpoints +Route::middleware(['auth:sanctum', 'single.session'])->prefix('social-media')->group(function () { + Route::get('/settings', [SocialMediaController::class, 'getSocialMediaSettings']); + Route::post('/settings', [SocialMediaController::class, 'updateSocialMedia']); + Route::post('/', [SocialMediaController::class, 'addSocialMedia']); + Route::put('/{id}', [SocialMediaController::class, 'updateSocialMediaPlatform']); + Route::delete('/{id}', [SocialMediaController::class, 'deleteSocialMediaPlatform']); + Route::patch('/{id}/toggle', [SocialMediaController::class, 'toggleSocialMediaPlatform']); +}); + +// Contact Settings - Admin endpoints +Route::middleware(['auth:sanctum', 'single.session'])->prefix('contact')->group(function () { + Route::get('/settings', [ContactController::class, 'getContactSettings']); + Route::post('/settings', [ContactController::class, 'updateContacts']); + Route::post('/', [ContactController::class, 'addContact']); + Route::put('/{id}', [ContactController::class, 'updateContact']); + Route::delete('/{id}', [ContactController::class, 'deleteContact']); + Route::patch('/{id}/toggle', [ContactController::class, 'toggleContact']); +}); + diff --git a/be/routes/console.php b/be/routes/console.php new file mode 100644 index 0000000..aa56fe4 --- /dev/null +++ b/be/routes/console.php @@ -0,0 +1,29 @@ +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 diff --git a/be/routes/web.php b/be/routes/web.php new file mode 100644 index 0000000..bfe8a20 --- /dev/null +++ b/be/routes/web.php @@ -0,0 +1,7 @@ + + + diff --git a/be/stubs/nwidart-stubs/composer.stub b/be/stubs/nwidart-stubs/composer.stub new file mode 100644 index 0000000..d2ae161 --- /dev/null +++ b/be/stubs/nwidart-stubs/composer.stub @@ -0,0 +1,30 @@ +{ + "name": "$VENDOR$/$LOWER_NAME$", + "description": "", + "authors": [ + { + "name": "$AUTHOR_NAME$", + "email": "$AUTHOR_EMAIL$" + } + ], + "extra": { + "laravel": { + "providers": [], + "aliases": { + + } + } + }, + "autoload": { + "psr-4": { + "$MODULE_NAMESPACE$\\$STUDLY_NAME$\\": "$APP_FOLDER_NAME$", + "$MODULE_NAMESPACE$\\$STUDLY_NAME$\\Database\\Factories\\": "database/factories/", + "$MODULE_NAMESPACE$\\$STUDLY_NAME$\\Database\\Seeders\\": "database/seeders/" + } + }, + "autoload-dev": { + "psr-4": { + "$MODULE_NAMESPACE$\\$STUDLY_NAME$\\Tests\\": "tests/" + } + } +} diff --git a/be/stubs/nwidart-stubs/controller-api.stub b/be/stubs/nwidart-stubs/controller-api.stub new file mode 100644 index 0000000..025095d --- /dev/null +++ b/be/stubs/nwidart-stubs/controller-api.stub @@ -0,0 +1,59 @@ +json([]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + // + + return response()->json([]); + } + + /** + * Show the specified resource. + */ + public function show($id) + { + // + + return response()->json([]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, $id) + { + // + + return response()->json([]); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy($id) + { + // + + return response()->json([]); + } +} diff --git a/be/stubs/nwidart-stubs/controller-basecrud.stub b/be/stubs/nwidart-stubs/controller-basecrud.stub new file mode 100644 index 0000000..a0faac7 --- /dev/null +++ b/be/stubs/nwidart-stubs/controller-basecrud.stub @@ -0,0 +1,49 @@ +json([]); + } +} diff --git a/be/stubs/nwidart-stubs/controller.stub b/be/stubs/nwidart-stubs/controller.stub new file mode 100644 index 0000000..79f1b77 --- /dev/null +++ b/be/stubs/nwidart-stubs/controller.stub @@ -0,0 +1,56 @@ +> + */ + protected $listen = []; + + /** + * Indicates if events should be discovered. + * + * @var bool + */ + protected static $shouldDiscoverEvents = true; + + /** + * Configure the proper event listeners for email verification. + */ + protected function configureEmailVerification(): void {} +} diff --git a/be/stubs/nwidart-stubs/event.stub b/be/stubs/nwidart-stubs/event.stub new file mode 100644 index 0000000..048d7f8 --- /dev/null +++ b/be/stubs/nwidart-stubs/event.stub @@ -0,0 +1,31 @@ +view('view.name'); + } +} diff --git a/be/stubs/nwidart-stubs/middleware.stub b/be/stubs/nwidart-stubs/middleware.stub new file mode 100644 index 0000000..bdd192d --- /dev/null +++ b/be/stubs/nwidart-stubs/middleware.stub @@ -0,0 +1,17 @@ +id(); + $FIELDS$ + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('$TABLE$'); + } +}; diff --git a/be/stubs/nwidart-stubs/migration/delete.stub b/be/stubs/nwidart-stubs/migration/delete.stub new file mode 100644 index 0000000..788cdee --- /dev/null +++ b/be/stubs/nwidart-stubs/migration/delete.stub @@ -0,0 +1,28 @@ +id(); + $FIELDS$ + $table->timestamps(); + }); + } +}; diff --git a/be/stubs/nwidart-stubs/migration/plain.stub b/be/stubs/nwidart-stubs/migration/plain.stub new file mode 100644 index 0000000..d94404c --- /dev/null +++ b/be/stubs/nwidart-stubs/migration/plain.stub @@ -0,0 +1,18 @@ +line('The introduction to the notification.') + ->action('Notification Action', 'https://laravel.com') + ->line('Thank you for using our application!'); + } + + /** + * Get the array representation of the notification. + */ + public function toArray($notifiable): array + { + return []; + } +} diff --git a/be/stubs/nwidart-stubs/observer.stub b/be/stubs/nwidart-stubs/observer.stub new file mode 100644 index 0000000..e74fe90 --- /dev/null +++ b/be/stubs/nwidart-stubs/observer.stub @@ -0,0 +1,33 @@ +where('name', 'LIKE', "%{$search}%"); + } + + return $query->paginate($perPage); + } + + /** + * Get all $LOWER_NAME$s with their relationships and pagination with search + */ + public function getAllWithRelationsPaginated(int $perPage = 10, string $search = '') + { + $query = $MODULE$::orderBy('name'); + + if (!empty($search)) { + $query->where('name', 'LIKE', "%{$search}%"); + } + + return $query->paginate($perPage); + } + + /** + * Get all $LOWER_NAME$s with their relationships and search + */ + public function getAllWithRelations(string $search = ''): Collection + { + $query = $MODULE$::orderBy('name'); + + if (!empty($search)) { + $query->where('name', 'LIKE', "%{$search}%"); + } + + return $query->get(); + } + + /** + * Create a new $LOWER_NAME$ + */ + public function create(array $data): $MODULE$ + { + return $MODULE$::create($data); + } + + /** + * Find $LOWER_NAME$ by ID + */ + public function findById(int $id): ?$MODULE$ + { + return $MODULE$::find($id); + } + + /** + * Delete $LOWER_NAME$ (soft delete) + */ + public function delete(int $id): bool + { + $$LOWER_NAME$ = $MODULE$::find($id); + if ($$LOWER_NAME$) { + return $$LOWER_NAME$->delete(); + } + return false; + } + + /** + * Get all $LOWER_NAME$s + */ + public function all(string $search = ''): Collection + { + $query = $MODULE$::orderBy('name'); + + if (!empty($search)) { + $query->where('name', 'LIKE', "%{$search}%"); + } + + return $query->get(); + } +} diff --git a/be/stubs/nwidart-stubs/repository-interface.stub b/be/stubs/nwidart-stubs/repository-interface.stub new file mode 100644 index 0000000..b9487ff --- /dev/null +++ b/be/stubs/nwidart-stubs/repository-interface.stub @@ -0,0 +1,44 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:255', + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'name.required' => 'Nama diperlukan.', + 'name.max' => 'Nama tidak boleh melebihi 255 aksara.', + ]; + } + + /** + * Determine if the user is authorized to make this request. + */ + public function authorize(): bool + { + return true; + } +} diff --git a/be/stubs/nwidart-stubs/resource-collection.stub b/be/stubs/nwidart-stubs/resource-collection.stub new file mode 100644 index 0000000..26a4059 --- /dev/null +++ b/be/stubs/nwidart-stubs/resource-collection.stub @@ -0,0 +1,17 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'name' => $this->name, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + ]; + } +} diff --git a/be/stubs/nwidart-stubs/route-provider.stub b/be/stubs/nwidart-stubs/route-provider.stub new file mode 100644 index 0000000..173248a --- /dev/null +++ b/be/stubs/nwidart-stubs/route-provider.stub @@ -0,0 +1,50 @@ +mapApiRoutes(); + + } + + /** + * Define the "web" routes for the application. + * + * These routes all receive session state, CSRF protection, etc. + */ + protected function mapWebRoutes(): void + { + Route::middleware('web')->group(module_path($this->name, '$WEB_ROUTES_PATH$')); + } + + /** + * Define the "api" routes for the application. + * + * These routes are typically stateless. + */ + protected function mapApiRoutes(): void + { + Route::middleware('api')->name('api.')->group(module_path($this->name, '$API_ROUTES_PATH$')); + } +} diff --git a/be/stubs/nwidart-stubs/routes/api.stub b/be/stubs/nwidart-stubs/routes/api.stub new file mode 100644 index 0000000..b795051 --- /dev/null +++ b/be/stubs/nwidart-stubs/routes/api.stub @@ -0,0 +1,8 @@ +prefix('v1')->group(function () { + Route::apiResource('$PLURAL_LOWER_NAME$', $STUDLY_NAME$Controller::class)->names('$LOWER_NAME$'); +}); diff --git a/be/stubs/nwidart-stubs/routes/web.stub b/be/stubs/nwidart-stubs/routes/web.stub new file mode 100644 index 0000000..2da42a1 --- /dev/null +++ b/be/stubs/nwidart-stubs/routes/web.stub @@ -0,0 +1,8 @@ +group(function () { + Route::resource('$PLURAL_LOWER_NAME$', $STUDLY_NAME$Controller::class)->names('$LOWER_NAME$'); +}); diff --git a/be/stubs/nwidart-stubs/rule.implicit.stub b/be/stubs/nwidart-stubs/rule.implicit.stub new file mode 100644 index 0000000..635cbd2 --- /dev/null +++ b/be/stubs/nwidart-stubs/rule.implicit.stub @@ -0,0 +1,19 @@ + '$STUDLY_NAME$', +]; diff --git a/be/stubs/nwidart-stubs/scaffold/provider.stub b/be/stubs/nwidart-stubs/scaffold/provider.stub new file mode 100644 index 0000000..5312f67 --- /dev/null +++ b/be/stubs/nwidart-stubs/scaffold/provider.stub @@ -0,0 +1,160 @@ +registerCommands(); + $this->registerCommandSchedules(); + $this->registerTranslations(); + $this->registerConfig(); + $this->registerViews(); + $this->loadMigrationsFrom(module_path($this->name, '$MIGRATIONS_PATH$')); + } + + /** + * Register the service provider. + */ + public function register(): void + { + $this->app->register(EventServiceProvider::class); + $this->app->register(RouteServiceProvider::class); + + // Register repository binding + $this->app->bind( + \Modules\$MODULE$\Repositories\Contracts\$MODULE$RepositoryInterface::class, + \Modules\$MODULE$\Repositories\$MODULE$Repository::class + ); + } + + /** + * Register commands in the format of Command::class + */ + protected function registerCommands(): void + { + // $this->commands([]); + } + + /** + * Register command Schedules. + */ + protected function registerCommandSchedules(): void + { + // $this->app->booted(function () { + // $schedule = $this->app->make(Schedule::class); + // $schedule->command('inspire')->hourly(); + // }); + } + + /** + * Register translations. + */ + public function registerTranslations(): void + { + $langPath = resource_path('lang/modules/'.$this->nameLower); + + if (is_dir($langPath)) { + $this->loadTranslationsFrom($langPath, $this->nameLower); + $this->loadJsonTranslationsFrom($langPath); + } else { + $this->loadTranslationsFrom(module_path($this->name, '$PATH_LANG$'), $this->nameLower); + $this->loadJsonTranslationsFrom(module_path($this->name, '$PATH_LANG$')); + } + } + + /** + * Register config. + */ + protected function registerConfig(): void + { + $configPath = module_path($this->name, config('modules.paths.generator.config.path')); + + if (is_dir($configPath)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($configPath)); + + foreach ($iterator as $file) { + if ($file->isFile() && $file->getExtension() === 'php') { + $config = str_replace($configPath.DIRECTORY_SEPARATOR, '', $file->getPathname()); + $config_key = str_replace([DIRECTORY_SEPARATOR, '.php'], ['.', ''], $config); + $segments = explode('.', $this->nameLower.'.'.$config_key); + + // Remove duplicated adjacent segments + $normalized = []; + foreach ($segments as $segment) { + if (end($normalized) !== $segment) { + $normalized[] = $segment; + } + } + + $key = ($config === 'config.php') ? $this->nameLower : implode('.', $normalized); + + $this->publishes([$file->getPathname() => config_path($config)], 'config'); + $this->merge_config_from($file->getPathname(), $key); + } + } + } + } + + /** + * Merge config from the given path recursively. + */ + protected function merge_config_from(string $path, string $key): void + { + $existing = config($key, []); + $module_config = require $path; + + config([$key => array_replace_recursive($existing, $module_config)]); + } + + /** + * Register views. + */ + public function registerViews(): void + { + $viewPath = resource_path('views/modules/'.$this->nameLower); + $sourcePath = module_path($this->name, '$PATH_VIEWS$'); + + $this->publishes([$sourcePath => $viewPath], ['views', $this->nameLower.'-module-views']); + + $this->loadViewsFrom(array_merge($this->getPublishableViewPaths(), [$sourcePath]), $this->nameLower); + + Blade::componentNamespace(config('modules.namespace').'\\' . $this->name . '\\View\\Components', $this->nameLower); + } + + /** + * Get the services provided by the provider. + */ + public function provides(): array + { + return []; + } + + private function getPublishableViewPaths(): array + { + $paths = []; + foreach (config('view.paths') as $path) { + if (is_dir($path.'/modules/'.$this->nameLower)) { + $paths[] = $path.'/modules/'.$this->nameLower; + } + } + + return $paths; + } +} diff --git a/be/stubs/nwidart-stubs/scope.stub b/be/stubs/nwidart-stubs/scope.stub new file mode 100644 index 0000000..2bbe73f --- /dev/null +++ b/be/stubs/nwidart-stubs/scope.stub @@ -0,0 +1,15 @@ +call([]); + } +} diff --git a/be/stubs/nwidart-stubs/service-invoke.stub b/be/stubs/nwidart-stubs/service-invoke.stub new file mode 100644 index 0000000..ae29efc --- /dev/null +++ b/be/stubs/nwidart-stubs/service-invoke.stub @@ -0,0 +1,8 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/be/stubs/nwidart-stubs/tests/unit.stub b/be/stubs/nwidart-stubs/tests/unit.stub new file mode 100644 index 0000000..938e5da --- /dev/null +++ b/be/stubs/nwidart-stubs/tests/unit.stub @@ -0,0 +1,16 @@ +assertTrue(true); + } +} diff --git a/be/stubs/nwidart-stubs/trait.stub b/be/stubs/nwidart-stubs/trait.stub new file mode 100644 index 0000000..86f1151 --- /dev/null +++ b/be/stubs/nwidart-stubs/trait.stub @@ -0,0 +1,5 @@ + + + diff --git a/be/stubs/nwidart-stubs/views/index.stub b/be/stubs/nwidart-stubs/views/index.stub new file mode 100644 index 0000000..4cc0542 --- /dev/null +++ b/be/stubs/nwidart-stubs/views/index.stub @@ -0,0 +1,5 @@ + +

Hello World

+ +

Module: {!! config('$LOWER_NAME$.name') !!}

+
diff --git a/be/stubs/nwidart-stubs/views/master.stub b/be/stubs/nwidart-stubs/views/master.stub new file mode 100644 index 0000000..b97cddf --- /dev/null +++ b/be/stubs/nwidart-stubs/views/master.stub @@ -0,0 +1,30 @@ + + + + + + + + + + $STUDLY_NAME$ Module - {{ config('app.name', 'Laravel') }} + + + + + + + + + + {{-- Vite CSS --}} + {{-- {{ module_vite('build-$LOWER_NAME$', 'resources/assets/sass/app.scss') }} --}} + + + + {{ $slot }} + + {{-- Vite JS --}} + {{-- {{ module_vite('build-$LOWER_NAME$', 'resources/assets/js/app.js') }} --}} + + diff --git a/be/stubs/nwidart-stubs/vite.stub b/be/stubs/nwidart-stubs/vite.stub new file mode 100644 index 0000000..f1b0312 --- /dev/null +++ b/be/stubs/nwidart-stubs/vite.stub @@ -0,0 +1,57 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import { readdirSync, statSync } from 'fs'; +import { join,relative,dirname } from 'path'; +import { fileURLToPath } from 'url'; + +export default defineConfig({ + build: { + outDir: '../../public/build-$LOWER_NAME$', + emptyOutDir: true, + manifest: true, + }, + plugins: [ + laravel({ + publicDirectory: '../../public', + buildDirectory: 'build-$LOWER_NAME$', + input: [ + __dirname + '/resources/assets/sass/app.scss', + __dirname + '/resources/assets/js/app.js' + ], + refresh: true, + }), + ], +}); +// Scen all resources for assets file. Return array +//function getFilePaths(dir) { +// const filePaths = []; +// +// function walkDirectory(currentPath) { +// const files = readdirSync(currentPath); +// for (const file of files) { +// const filePath = join(currentPath, file); +// const stats = statSync(filePath); +// if (stats.isFile() && !file.startsWith('.')) { +// const relativePath = 'Modules/$STUDLY_NAME$/'+relative(__dirname, filePath); +// filePaths.push(relativePath); +// } else if (stats.isDirectory()) { +// walkDirectory(filePath); +// } +// } +// } +// +// walkDirectory(dir); +// return filePaths; +//} + +//const __filename = fileURLToPath(import.meta.url); +//const __dirname = dirname(__filename); + +//const assetsDir = join(__dirname, 'resources/assets'); +//export const paths = getFilePaths(assetsDir); + + +//export const paths = [ +// 'Modules/$STUDLY_NAME$/resources/assets/sass/app.scss', +// 'Modules/$STUDLY_NAME$/resources/assets/js/app.js', +//]; diff --git a/be/tests/Feature/ExampleTest.php b/be/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..8364a84 --- /dev/null +++ b/be/tests/Feature/ExampleTest.php @@ -0,0 +1,19 @@ +get('/'); + + $response->assertStatus(200); + } +} diff --git a/be/tests/TestCase.php b/be/tests/TestCase.php new file mode 100644 index 0000000..fe1ffc2 --- /dev/null +++ b/be/tests/TestCase.php @@ -0,0 +1,10 @@ +assertTrue(true); + } +} diff --git a/be/vite-module-loader.js b/be/vite-module-loader.js new file mode 100644 index 0000000..56c35b9 --- /dev/null +++ b/be/vite-module-loader.js @@ -0,0 +1,51 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { pathToFileURL } from 'url'; + +async function collectModuleAssetsPaths(paths, modulesPath) { + modulesPath = path.join(__dirname, modulesPath); + + const moduleStatusesPath = path.join(__dirname, 'modules_statuses.json'); + + try { + // Read module_statuses.json + const moduleStatusesContent = await fs.readFile(moduleStatusesPath, 'utf-8'); + const moduleStatuses = JSON.parse(moduleStatusesContent); + + // Read module directories + const moduleDirectories = await fs.readdir(modulesPath); + + for (const moduleDir of moduleDirectories) { + if (moduleDir === '.DS_Store') { + // Skip .DS_Store directory + continue; + } + + // Check if the module is enabled (status is true) + if (moduleStatuses[moduleDir] === true) { + const viteConfigPath = path.join(modulesPath, moduleDir, 'vite.config.js'); + + try { + await fs.access(viteConfigPath); + // Convert to a file URL for Windows compatibility + const moduleConfigURL = pathToFileURL(viteConfigPath); + + // Import the module-specific Vite configuration + const moduleConfig = await import(moduleConfigURL.href); + + if (moduleConfig.paths && Array.isArray(moduleConfig.paths)) { + paths.push(...moduleConfig.paths); + } + } catch (error) { + // vite.config.js does not exist, skip this module + } + } + } + } catch (error) { + console.error(`Error reading module statuses or module configurations: ${error}`); + } + + return paths; +} + +export default collectModuleAssetsPaths; diff --git a/be/vite.config.js b/be/vite.config.js new file mode 100644 index 0000000..29fbfe9 --- /dev/null +++ b/be/vite.config.js @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + }), + tailwindcss(), + ], +}); diff --git a/docs/BORANG-MENJADI-ANGGOTA-2024.pdf b/docs/BORANG-MENJADI-ANGGOTA-2024.pdf new file mode 100644 index 0000000..7fac5ff Binary files /dev/null and b/docs/BORANG-MENJADI-ANGGOTA-2024.pdf differ diff --git a/docs/BORANG-PERMOHONAN-PEMBIAYAAN-ANGGOTA-new.pdf b/docs/BORANG-PERMOHONAN-PEMBIAYAAN-ANGGOTA-new.pdf new file mode 100644 index 0000000..652789b Binary files /dev/null and b/docs/BORANG-PERMOHONAN-PEMBIAYAAN-ANGGOTA-new.pdf differ diff --git a/docs/Flowchart-MyKOPKB.drawio b/docs/Flowchart-MyKOPKB.drawio new file mode 100644 index 0000000..60cf24c --- /dev/null +++ b/docs/Flowchart-MyKOPKB.drawio @@ -0,0 +1,653 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/Kertas Cadangan Kerja - Penggunaan sistem iKOOP.pdf b/docs/Kertas Cadangan Kerja - Penggunaan sistem iKOOP.pdf new file mode 100644 index 0000000..565bc20 Binary files /dev/null and b/docs/Kertas Cadangan Kerja - Penggunaan sistem iKOOP.pdf differ diff --git a/docs/SENARAI DAFTAR ANGGOTA 2026.xls b/docs/SENARAI DAFTAR ANGGOTA 2026.xls new file mode 100644 index 0000000..8011f86 Binary files /dev/null and b/docs/SENARAI DAFTAR ANGGOTA 2026.xls differ diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..55f92ce --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,150 @@ +To build a solid **e-commerce backend using Spring Boot**, you need to design it around core business domains, security, scalability, and integrations. + +Here’s a structured roadmap 👇 + +--- + +# 1️⃣ Core Functional Modules + +## 🧑‍💼 1. User & Authentication Module + +* User registration & login +* Role-based access (ADMIN, CUSTOMER, SELLER) +* JWT or OAuth2 authentication +* Password encryption (BCrypt) +* Email verification +* Forgot/reset password + +**Technologies** + +* Spring Security +* JWT +* OAuth2 (optional) +* BCryptPasswordEncoder + +--- + +# 2️⃣ Recommended Architecture + +### 🔹 Layered Architecture + +* Controller layer (REST APIs) +* Service layer (Business logic) +* Repository layer (Data access) +* DTO & Mapper layer (MapStruct recommended) + +### 🔹 Database + +* PostgreSQL / MySQL +* Redis (caching, cart) +* Elasticsearch (for search, optional) + +--- + +# 3️⃣ Important Technical Features + +## 🔐 Security + +* Spring Security +* JWT authentication +* CSRF protection +* CORS config +* Rate limiting +* Input validation + +--- + +## ⚡ Performance & Scalability + +* Caching (Redis) +* Pagination +* Indexing DB columns +* Async processing +* Connection pooling (HikariCP) + +--- + +## 📡 API Design + +* RESTful endpoints +* Proper HTTP status codes +* Exception handling (GlobalExceptionHandler) +* Swagger / OpenAPI documentation + +--- + +## 🧾 Logging & Monitoring + +* Logback +* ELK stack +* Actuator +* Prometheus & Grafana + +--- + +# 4️⃣ Advanced / Production-Level Features + +* Microservices architecture +* API Gateway +* Circuit breaker (Resilience4j) +* Kafka for order events +* Docker + Kubernetes +* CI/CD pipeline +* Cloud deployment (AWS/GCP/Azure) + +--- + +# 5️⃣ Suggested Database Entities + +Basic Entities: + +* User +* Role + +--- + +# 6️⃣ Tools & Dependencies + +Spring Boot Starters: + +* spring-boot-starter-web +* spring-boot-starter-data-jpa +* spring-boot-starter-security +* spring-boot-starter-validation +* spring-boot-starter-actuator + +Other: + +* Lombok +* MapStruct +* Flyway or Liquibase +* Redis +* Docker + +--- + +# 7️⃣ Typical Development Roadmap + +1. Setup project +2. Configure DB +3. Implement User & Security +8. Add caching +9. Add logging & monitoring +10. Deploy + + + ┌───────────────┐ + │ Keycloak │ + └──┬─────────┬──┘ + PKCE ──┘ └── OIDC code flow + (mobile) (BFF callback) + │ │ + ┌────▼───┐ ┌───▼────┐ + │ Mobile │ │ BFF │ ← Spring app, holds session + └────┬───┘ └───┬────┘ + │ Bearer │ Bearer (server-attached) + └──────┬──────┘ + ▼ + ┌─────────────┐ + │ Resource API│ ← validates JWT via JWKS + └─────────────┘ \ No newline at end of file diff --git a/docs/db_design.md b/docs/db_design.md new file mode 100644 index 0000000..9eb5988 --- /dev/null +++ b/docs/db_design.md @@ -0,0 +1,248 @@ +# 🧾 3. Table Design (Detailed) + +--- + +## `loan_applications` (MAIN TABLE) +This is the core record. + +```sql +id (PK) +loan_type_id (FK) +amount_requested +deduction_period +purpose + +status (SUBMITTED, UNDER_REVIEW, APPROVED, REJECTED) +created_at +updated_at +``` + +## `applicants` +One-to-one with loan application. + +```sql +id (PK) +loan_application_id (FK) + +name +ic_num +phone_num +member_id_num +birth_date +email + +employer_name +current_position +position_status + +address + +bank_account_number +bank_name + +basic_salary +total_income +deduction + +service_date +service_period +``` + +## `guarantors` +👉 One loan → **3 guarantors** (One-to-Many) + +```sql +id (PK) +loan_application_id (FK) + +name +ic_num +unit +address +phone_num +member_id_number + +basic_salary +allowance +total_income +``` + +## `loan_approvals` +One-to-one with loan application. + +```sql +id (PK) +loan_application_id (FK) + +level +role +approver_id + +decision (PENDING, APPROVED, REJECTED) +remarks + +approved_at +``` + +## `loan_evaluations` +```sql +id (PK) +loan_application_id (FK) + +calculated_by (FK users) + +total_loan +total_payment +period + +insurance_per_month + +deduction_start_date +deduction_end_date + +number_of_shares +total_fees +debt_balance + +payment_date + +created_at +``` + +## `documents` +```sql +id +loan_application_id (FK) + +file_name +file_url +file_type + +uploaded_by +uploaded_at +``` + +## `loan_status_history` +```sql +id (PK) +loan_application_id (FK) + +status (DRAFT, SUBMITTED, UNDER_REVIEW, APPROVED, REJECTED) + +changed_by (FK users) +changed_at (timestamp) + +remarks (optional) +``` + +## `repayments` +```sql +id (PK) +loan_application_id (FK) + +installment_number (1, 2, 3...) + +due_date +amount_due + +amount_paid +payment_date + +status (PENDING, PAID, LATE) + +created_at +``` + +--- + +# 🔗 4. Relationships Diagram (Simple) + +```id="c2x7qp" +loan_applications + │ + ├── applicants (1:1) + ├── guarantors (1:N) + └── loan_reviews (1:1) +``` + +--- + + +# 🧩 6. JPA Entity Mapping (Example) + +### LoanApplication + +```java +@Entity +public class LoanApplication { + @Id + @GeneratedValue + private Long id; + + private BigDecimal amountRequested; + private Integer deductionPeriod; + private String purpose; + + @Enumerated(EnumType.STRING) + private LoanStatus status; + + @OneToOne(mappedBy = "loanApplication", cascade = CascadeType.ALL) + private Applicant applicant; + + @OneToMany(mappedBy = "loanApplication", cascade = CascadeType.ALL) + private List guarantors; + + @OneToOne(mappedBy = "loanApplication", cascade = CascadeType.ALL) + private LoanReview review; +} +``` + +--- + +# ⚠️ 7. Important Constraints + +## 🔹 Enforce in Backend (NOT DB) + +* Exactly **3 guarantors** +* Valid IC format +* Salary > 0 +* Loan amount limits + +--- + +# 🔐 8. Audit & Compliance (Very Important) + +Add these fields to key tables: + +```sql +created_by +created_at +updated_by +updated_at +``` + +--- + +# 🚀 9. Future Enhancements + +You can easily extend this design with: + +* 📎 `documents` table (for uploads) +* 📊 `repayments` table +* 🧾 `audit_logs` +* 🔄 `loan_status_history` + +--- + +# 🏁 Final Architecture Mapping + +Your DB aligns perfectly with your system: + +```id="z7wq3f" +Frontend Form → LoanApplication → Applicant + Guarantors + ↓ + Officer Review + ↓ + Decision +``` + diff --git a/fe/.DS_Store b/fe/.DS_Store new file mode 100644 index 0000000..531073e Binary files /dev/null and b/fe/.DS_Store differ diff --git a/fe/.editorconfig b/fe/.editorconfig new file mode 100644 index 0000000..3b510aa --- /dev/null +++ b/fe/.editorconfig @@ -0,0 +1,8 @@ +[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] +charset = utf-8 +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true +end_of_line = lf +max_line_length = 100 diff --git a/fe/.env.development b/fe/.env.development new file mode 100644 index 0000000..921d0eb --- /dev/null +++ b/fe/.env.development @@ -0,0 +1,7 @@ +VITE_APP_NAME="MyKOPKB" +VITE_APP_VERSION="1.0" +VITE_API_BASE_URL=http://localhost +VITE_APP_URL=http://localhost +VITE_APP_PORT=5173 +VITE_APP_ENV=development +VITE_APP_DEBUG=true \ No newline at end of file diff --git a/fe/.gitignore b/fe/.gitignore new file mode 100644 index 0000000..7e4d949 --- /dev/null +++ b/fe/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +.env.production +dist/ \ No newline at end of file diff --git a/fe/.prettierrc.json b/fe/.prettierrc.json new file mode 100644 index 0000000..29a2402 --- /dev/null +++ b/fe/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://json.schemastore.org/prettierrc", + "semi": false, + "singleQuote": true, + "printWidth": 100 +} diff --git a/fe/README.md b/fe/README.md new file mode 100644 index 0000000..6ce3e21 --- /dev/null +++ b/fe/README.md @@ -0,0 +1,70 @@ +# midone-vue + +This template should help get you started developing with Vue 3 in Vite. + +## Recommended IDE Setup + +[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur). + +## Recommended Browser Setup + +- Chromium-based browsers (Chrome, Edge, Brave, etc.): + - [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd) + - [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters) +- Firefox: + - [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/) + - [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/) + +## Type Support for `.vue` Imports in TS + +TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types. + +## Customize configuration + +See [Vite Configuration Reference](https://vite.dev/config/). + +## Project Setup + +```sh +yarn +``` + +### Compile and Hot-Reload for Development + +```sh +yarn dev +``` + +### Type-Check, Compile and Minify for Production + +```sh +yarn build +``` + +### Run Unit Tests with [Vitest](https://vitest.dev/) + +```sh +yarn test:unit +``` + +### Run End-to-End Tests with [Cypress](https://www.cypress.io/) + +```sh +yarn test:e2e:dev +``` + +This runs the end-to-end tests against the Vite development server. +It is much faster than the production build. + +But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments): + +```sh +yarn build +yarn test:e2e +``` + +### Lint with [ESLint](https://eslint.org/) + +```sh +yarn lint +``` diff --git a/fe/cypress.config.ts b/fe/cypress.config.ts new file mode 100644 index 0000000..4a22885 --- /dev/null +++ b/fe/cypress.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'cypress' + +export default defineConfig({ + e2e: { + specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}', + baseUrl: 'http://localhost:4173', + }, +}) diff --git a/fe/env.d.ts b/fe/env.d.ts new file mode 100644 index 0000000..f9a4d99 --- /dev/null +++ b/fe/env.d.ts @@ -0,0 +1,15 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL?: string + readonly VITE_APP_NAME?: string + readonly VITE_APP_VERSION?: string + readonly VITE_APP_URL?: string + readonly VITE_APP_PORT?: string + readonly VITE_APP_ENV?: string + readonly VITE_APP_DEBUG?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/fe/eslint.config.ts b/fe/eslint.config.ts new file mode 100644 index 0000000..5f44e7c --- /dev/null +++ b/fe/eslint.config.ts @@ -0,0 +1,38 @@ +import { globalIgnores } from 'eslint/config' +import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' +import pluginVue from 'eslint-plugin-vue' +import pluginCypress from 'eslint-plugin-cypress' +import pluginVitest from '@vitest/eslint-plugin' +import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' + +// To allow more languages other than `ts` in `.vue` files, uncomment the following lines: +// import { configureVueProject } from '@vue/eslint-config-typescript' +// configureVueProject({ scriptLangs: ['ts', 'tsx'] }) +// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup + +export default defineConfigWithVueTs( + { + name: 'app/files-to-lint', + files: ['**/*.{vue,ts,mts,tsx}'], + }, + + globalIgnores(['**/dist/**', '**/dist-ssr/**', '**/coverage/**']), + + ...pluginVue.configs['flat/essential'], + vueTsConfigs.recommended, + + { + ...pluginCypress.configs.recommended, + files: [ + 'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}', + 'cypress/support/**/*.{js,ts,jsx,tsx}', + ], + }, + + { + ...pluginVitest.configs.recommended, + files: ['src/**/__tests__/*'], + }, + + skipFormatting, +) diff --git a/fe/index.html b/fe/index.html new file mode 100644 index 0000000..da3a3e6 --- /dev/null +++ b/fe/index.html @@ -0,0 +1,16 @@ + + + + + + + + MyKOPKB 1.0 + + + +
+ + + + \ No newline at end of file diff --git a/fe/package.json b/fe/package.json new file mode 100644 index 0000000..6d389fe --- /dev/null +++ b/fe/package.json @@ -0,0 +1,95 @@ +{ + "name": "@mykopkb/frontend", + "version": "1.0.0", + "private": true, + "type": "module", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "scripts": { + "dev": "vite", + "build": "run-p type-check \"build-only {@}\" --", + "preview": "vite preview", + "test:unit": "vitest", + "prepare": "cypress install", + "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'", + "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'", + "build-only": "vite build", + "type-check": "vue-tsc --build", + "lint": "eslint . --fix --cache", + "format": "prettier --write --experimental-cli src/" + }, + "dependencies": { + "@iconify/vue": "^5.0.1", + "@internationalized/date": "^3.11.0", + "@lucide/vue": "^1.17.0", + "@tailwindcss/vite": "^4.1.18", + "@tanstack/vue-table": "^8.21.3", + "@zag-js/accordion": "1.26.1", + "@zag-js/avatar": "1.26.1", + "@zag-js/carousel": "1.26.1", + "@zag-js/checkbox": "1.26.1", + "@zag-js/combobox": "1.26.1", + "@zag-js/date-picker": "1.26.1", + "@zag-js/dialog": "1.26.1", + "@zag-js/menu": "1.26.1", + "@zag-js/pagination": "1.26.1", + "@zag-js/popover": "1.26.1", + "@zag-js/presence": "1.26.1", + "@zag-js/progress": "1.26.1", + "@zag-js/radio-group": "1.26.1", + "@zag-js/scroll-area": "1.26.1", + "@zag-js/select": "1.26.1", + "@zag-js/slider": "1.26.1", + "@zag-js/switch": "1.26.1", + "@zag-js/tabs": "1.26.1", + "@zag-js/toast": "1.26.1", + "@zag-js/tooltip": "1.26.1", + "@zag-js/vue": "1.26.1", + "axios": "^1.16.1", + "chart.js": "^4.5.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "dayjs": "^1.11.21", + "file-saver": "^2.0.5", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.8", + "lodash": "^4.17.21", + "maplibre-gl": "^5.18.0", + "pinia": "^3.0.4", + "sweetalert2": "^11.26.25", + "tailwind-merge": "^3.5.0", + "tailwindcss": "^4.1.18", + "tw-animate-css": "^1.4.0", + "vue": "^3.5.26", + "vue-router": "^4.6.4", + "xlsx": "^0.18.5" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.3", + "@types/file-saver": "^2.0.7", + "@types/jsdom": "^27.0.0", + "@types/lodash": "^4.17.21", + "@types/node": "^24.10.4", + "@vitejs/plugin-vue": "^6.0.3", + "@vitest/eslint-plugin": "^1.6.4", + "@vue/eslint-config-prettier": "^10.2.0", + "@vue/eslint-config-typescript": "^14.6.0", + "@vue/test-utils": "^2.4.6", + "@vue/tsconfig": "^0.8.1", + "cypress": "^15.8.1", + "eslint": "^9.39.2", + "eslint-plugin-cypress": "^5.2.0", + "eslint-plugin-vue": "~10.6.2", + "jiti": "^2.6.1", + "jsdom": "^27.3.0", + "npm-run-all2": "^8.0.4", + "prettier": "3.7.4", + "start-server-and-test": "^2.1.3", + "typescript": "~5.9.3", + "vite": "^7.3.0", + "vite-plugin-vue-devtools": "^8.0.5", + "vitest": "^4.0.16", + "vue-tsc": "^3.2.1" + } +} \ No newline at end of file diff --git a/fe/pnpm-lock.yaml b/fe/pnpm-lock.yaml new file mode 100644 index 0000000..ef1bc00 --- /dev/null +++ b/fe/pnpm-lock.yaml @@ -0,0 +1,6852 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@iconify/vue': + specifier: ^5.0.1 + version: 5.0.1(vue@3.5.35(typescript@5.9.3)) + '@internationalized/date': + specifier: ^3.11.0 + version: 3.12.2 + '@lucide/vue': + specifier: ^1.17.0 + version: 1.17.0(vue@3.5.35(typescript@5.9.3)) + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.3.0(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) + '@tanstack/vue-table': + specifier: ^8.21.3 + version: 8.21.3(vue@3.5.35(typescript@5.9.3)) + '@zag-js/accordion': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/avatar': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/carousel': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/checkbox': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/combobox': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/date-picker': + specifier: 1.26.1 + version: 1.26.1(@internationalized/date@3.12.2) + '@zag-js/dialog': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/menu': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/pagination': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/popover': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/presence': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/progress': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/radio-group': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/scroll-area': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/select': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/slider': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/switch': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/tabs': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/toast': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/tooltip': + specifier: 1.26.1 + version: 1.26.1 + '@zag-js/vue': + specifier: 1.26.1 + version: 1.26.1(vue@3.5.35(typescript@5.9.3)) + axios: + specifier: ^1.16.1 + version: 1.16.1(debug@4.4.3) + chart.js: + specifier: ^4.5.0 + version: 4.5.1 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + dayjs: + specifier: ^1.11.21 + version: 1.11.21 + file-saver: + specifier: ^2.0.5 + version: 2.0.5 + jspdf: + specifier: ^4.2.1 + version: 4.2.1 + jspdf-autotable: + specifier: ^5.0.8 + version: 5.0.8(jspdf@4.2.1) + lodash: + specifier: ^4.17.21 + version: 4.18.1 + maplibre-gl: + specifier: ^5.18.0 + version: 5.24.0 + pinia: + specifier: ^3.0.4 + version: 3.0.4(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3)) + sweetalert2: + specifier: ^11.26.25 + version: 11.26.25 + tailwind-merge: + specifier: ^3.5.0 + version: 3.6.0 + tailwindcss: + specifier: ^4.1.18 + version: 4.3.0 + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + vue: + specifier: ^3.5.26 + version: 3.5.35(typescript@5.9.3) + vue-router: + specifier: ^4.6.4 + version: 4.6.4(vue@3.5.35(typescript@5.9.3)) + xlsx: + specifier: ^0.18.5 + version: 0.18.5 + devDependencies: + '@tsconfig/node24': + specifier: ^24.0.3 + version: 24.0.4 + '@types/file-saver': + specifier: ^2.0.7 + version: 2.0.7 + '@types/jsdom': + specifier: ^27.0.0 + version: 27.0.0 + '@types/lodash': + specifier: ^4.17.21 + version: 4.17.24 + '@types/node': + specifier: ^24.10.4 + version: 24.12.4 + '@vitejs/plugin-vue': + specifier: ^6.0.3 + version: 6.0.7(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3)) + '@vitest/eslint-plugin': + specifier: ^1.6.4 + version: 1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)(vitest@4.1.8(@types/node@24.12.4)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))) + '@vue/eslint-config-prettier': + specifier: ^10.2.0 + version: 10.2.0(eslint@9.39.4(jiti@2.7.0))(prettier@3.7.4) + '@vue/eslint-config-typescript': + specifier: ^14.6.0 + version: 14.7.0(eslint-plugin-vue@10.6.2(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.7.0))))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.10(@vue/compiler-dom@3.5.35)(@vue/server-renderer@3.5.35(vue@3.5.35(typescript@5.9.3)))(vue@3.5.35(typescript@5.9.3)) + '@vue/tsconfig': + specifier: ^0.8.1 + version: 0.8.1(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3)) + cypress: + specifier: ^15.8.1 + version: 15.16.0 + eslint: + specifier: ^9.39.2 + version: 9.39.4(jiti@2.7.0) + eslint-plugin-cypress: + specifier: ^5.2.0 + version: 5.4.0(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-vue: + specifier: ~10.6.2 + version: 10.6.2(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.7.0))) + jiti: + specifier: ^2.6.1 + version: 2.7.0 + jsdom: + specifier: ^27.3.0 + version: 27.4.0 + npm-run-all2: + specifier: ^8.0.4 + version: 8.0.4 + prettier: + specifier: 3.7.4 + version: 3.7.4 + start-server-and-test: + specifier: ^2.1.3 + version: 2.1.5 + typescript: + specifier: ~5.9.3 + version: 5.9.3 + vite: + specifier: ^7.3.0 + version: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + vite-plugin-vue-devtools: + specifier: ^8.0.5 + version: 8.1.2(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3)) + vitest: + specifier: ^4.0.16 + version: 4.1.8(@types/node@24.12.4)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) + vue-tsc: + specifier: ^3.2.1 + version: 3.3.3(typescript@5.9.3) + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.29.7': + resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.29.7': + resolution: {integrity: sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.1': + resolution: {integrity: sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.1': + resolution: {integrity: sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.4': + resolution: {integrity: sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@cypress/request@4.0.1': + resolution: {integrity: sha512-y20e+e6dFYkOUUJLVUZTsJRuTiXZaUQ32WD+R/ux/HBybbTx4ge7cNINcua0pU8+SNkKuRbOF12mBmzuzM8n5w==} + engines: {node: '>= 14.17.0'} + + '@cypress/xvfb@1.2.4': + resolution: {integrity: sha512-skbBzPggOVYCbnGgV+0dmBdW/s77ZkAOXIC1knS8NagwDjBrNC1LuXtQJeiN6l+m7lzmHtaoUw/ctJKdqkG57Q==} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@hapi/address@5.1.1': + resolution: {integrity: sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==} + engines: {node: '>=14.0.0'} + + '@hapi/formula@3.0.2': + resolution: {integrity: sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==} + + '@hapi/hoek@11.0.7': + resolution: {integrity: sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==} + + '@hapi/pinpoint@2.0.1': + resolution: {integrity: sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==} + + '@hapi/tlds@1.1.6': + resolution: {integrity: sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==} + engines: {node: '>=14.0.0'} + + '@hapi/topo@6.0.2': + resolution: {integrity: sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/vue@5.0.1': + resolution: {integrity: sha512-aumwwooJlFJ5H5qYWB6ZTAyM0C8hpfcSVLB9/a3qnH1GGvIJ+FEbpEs4s/HfErYe/M5qZeLjwmESR5fFm3lXEw==} + peerDependencies: + vue: '>=3.0.0' + + '@internationalized/date@3.12.2': + resolution: {integrity: sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@lucide/vue@1.17.0': + resolution: {integrity: sha512-6Q1ZHgr5FbmJzKWe5BxlNdjLj2lbmuH1zwDtVzUJofX0w9UREwKgq4F4jwKqFYyyIS4Rj3FiJvDi2k6djukmmw==} + peerDependencies: + vue: '>=3.0.1' + + '@mapbox/jsonlint-lines-primitives@2.0.2': + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} + engines: {node: '>= 0.6'} + + '@mapbox/point-geometry@1.1.0': + resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} + + '@mapbox/tiny-sdf@2.2.0': + resolution: {integrity: sha512-LVL4wgI9YAum5V+LNVQO6QgFBPw7/MIIY4XJPNsPDMrjEwcE+JfKk1LuIl8GnF197ejVdC9QdPaxrx5gfgdGXg==} + + '@mapbox/unitbezier@0.0.1': + resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} + + '@mapbox/vector-tile@2.0.5': + resolution: {integrity: sha512-pXj8m7KTsqZt+1jsE0xIpGvqTSbblfkuEJL/NJmNePMtEwxO8V3XMDo9WMSfDeqHvCtBI9Lmt4mGcGR10zecmw==} + + '@mapbox/whoots-js@3.1.0': + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} + engines: {node: '>=6.0.0'} + + '@maplibre/geojson-vt@5.0.4': + resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==} + + '@maplibre/geojson-vt@6.1.0': + resolution: {integrity: sha512-2eIY4gZxeKIVOZVNkAMb+5NgXhgsMQpOveTQAvnp53LYqHGJZDidk7Ew0Tged9PThidpbS+NFTh0g4zivhPDzQ==} + + '@maplibre/maplibre-gl-style-spec@24.8.5': + resolution: {integrity: sha512-EzEJmMt6thioRH7GI9LWS7ahXTcAhAPGWCe6oTP2Ps4YnsXOOAfeqx854lZaiDnwURfHmcCKV1mr6oo0i23x6w==} + hasBin: true + + '@maplibre/mlt@1.1.11': + resolution: {integrity: sha512-dKvjKdITw9d0y3ndGkSqLUEpWCizMtdq8NB06cHohH/JZ2sJoM7dClR9wzJLUWykjbw9RXDFmhjjNBnNW27mzw==} + + '@maplibre/vt-pbf@4.3.0': + resolution: {integrity: sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/rollup-android-arm-eabi@4.61.0': + resolution: {integrity: sha512-dnxczajOqt0gesZlN5pGQ1s1imQVrsmCw5G2Ci4oM+0WvNz3pyRnlWrT7McoZIb8VlFwCawdmbWRmxRn7HI+VQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.61.0': + resolution: {integrity: sha512-Bp3JpGP00Vu3f238ivRrjf7z3xSzVPXqCmaJYA9t2c+c8vKYvOzmXF7LkkeUalTEGd6cZcSWe+PFIP3Vy48fRg==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.61.0': + resolution: {integrity: sha512-zaYIpr670mUmmZ1tVzUFplbQbG7h3Gugx3L5FoqhsC2m/YnLlR1a7zVLmXNPy+iY1tFPEbNG+HHBXZGyId0G5w==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.61.0': + resolution: {integrity: sha512-+P49fvkv2dSoeevUW+lgZ/I2JHSsJCK1Lyjj7Cu6E4UHG4tS9XIefzIjo5qhgELjAclnen1rLzK2PMKJdo+Dyg==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.61.0': + resolution: {integrity: sha512-l3FAAOyKJXH2ea6KNFN+MMgC/rnE94YGLXs2ehYqDcCoHt1DpvgWX75BhUJxN38XojP7Ul+4H8PRn7EdyqSDrw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.61.0': + resolution: {integrity: sha512-VokPN3TSctKj65cyCNPaUh4vMFA8awxOot/0sp+4J7ZlNRKQEhXhawqPwajoi8H5ZFt61i0ugZJuTKXBjGJ17Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.61.0': + resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.61.0': + resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.61.0': + resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.61.0': + resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.61.0': + resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.61.0': + resolution: {integrity: sha512-jXaXFqKMehsOc+g8R6oo33RRC6w07G9jDBxAE5eAKX7mOcCbZloYIPNhfG9Wl+P9O9IWHFO4OJgPi1Ml2qkt7w==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + resolution: {integrity: sha512-OXNWVFocS2IA4+QplhTZZ2a+8hPZR7T8KuozsNmJKK8y7cp83StHvGksfHzPG3wczWTczyWHVQuqeiTUbjiyBg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + resolution: {integrity: sha512-AlAbNtBO637LxSldqV43z0FfXoGfl2TW1DgAg/bs7aQswFbDewz2SJm3BUhiGfbOVtW571xbc9p+REdxhyN/Eg==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.61.0': + resolution: {integrity: sha512-QRSrQXyJ1M4tjNXdR0/G/IgV6lzfQQJYBjlWIEYkY2Xs86DRl/iEpQ4blMDjJxSl7n19eDKKXMg0AmuBVYy8pQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.61.0': + resolution: {integrity: sha512-tkuFxhvKO/HlGd0VsINF6vHSYH8AF8W0TcNxKDK6JZmrehngFj78pToc8iemtnvwilDjs2G/qSzYFhe9U8q+fw==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + + '@tailwindcss/node@4.3.0': + resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + + '@tailwindcss/oxide-android-arm64@4.3.0': + resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.0': + resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.0': + resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + engines: {node: '>= 20'} + + '@tailwindcss/vite@4.3.0': + resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tanstack/vue-table@8.21.3': + resolution: {integrity: sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==} + engines: {node: '>=12'} + peerDependencies: + vue: '>=3.2' + + '@tsconfig/node24@24.0.4': + resolution: {integrity: sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/file-saver@2.0.7': + resolution: {integrity: sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/jsdom@27.0.0': + resolution: {integrity: sha512-NZyFl/PViwKzdEkQg96gtnB8wm+1ljhdDay9ahn4hgb+SfVtPCbm3TlmDUFXTA+MGN3CijicnMhG18SI5H3rFw==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/lodash@4.17.24': + resolution: {integrity: sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==} + + '@types/node@24.12.4': + resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} + + '@types/pako@2.0.4': + resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==} + + '@types/raf@3.4.3': + resolution: {integrity: sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==} + + '@types/sinonjs__fake-timers@8.1.1': + resolution: {integrity: sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==} + + '@types/sizzle@2.3.10': + resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} + + '@types/supercluster@7.1.3': + resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + + '@types/tmp@0.2.6': + resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==} + + '@types/tough-cookie@4.0.5': + resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@typescript-eslint/eslint-plugin@8.60.1': + resolution: {integrity: sha512-JQ4S5GB0tfjO8BuJ4fcX+HodkzJjYBV+7OJ+wLygaX7OGQ7FudyHL4NSCA6ob+w3Yn+5MkKIozOwQhXeM7opVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.60.1 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.60.1': + resolution: {integrity: sha512-A0M6ua6H252bVjPvvtSgl2QA4+ET9S5Mtkb2GDyTxIhH/C4qDItT7RQNO5PhMC6NXGYXOR9dIalcDDgBKT7oFA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.60.1': + resolution: {integrity: sha512-eXkTH2bxmXlqD1RnOPmLZ9ZM9D3VwSx04JOwBnP9RQ+yUA5a2Mu7SfW8uaV2Aon53NJzZlZYuX7tn91Izf+xaw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.60.1': + resolution: {integrity: sha512-gvI5OQoptnxQnchOirukCuQ55svJSTuD/4k5+pC267xyBtYry748R9/c3tYUzb/iE6RZfllRz2lVulLCHkTm4w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.60.1': + resolution: {integrity: sha512-nh8w4qAteiKuZu3pSSzG/yGKpw0OlkrKnzFmbVRenKaD4qc+7i1GrmZaLVkr8rk4uipiPGMOW4YsM6WmKZ5CvA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.60.1': + resolution: {integrity: sha512-sdwTrpjosW7ANQYJ39ZBF1ZyEMEGVB2UsikrserVM/30a/F1dTLnu9bGxEdosugyu5caigjLrR2qiD11asjI1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.60.1': + resolution: {integrity: sha512-4h0tY8ppCkdCzcrl2YM5M3my0xsE1Tf8om3owEu5oPWmXwkKRmk0j0LGDzYBGUcAlesEbxBhazqu/K4cu3Ug7w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.60.1': + resolution: {integrity: sha512-alpRkfG8hlVE5kdJW2GkfgDgXxold3e8e4l6EnmhRmRLbekgAPCCGDVD++sABy9FcgPFroq+uFcCSM1vR57Cew==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.60.1': + resolution: {integrity: sha512-h2MPBLoNtjc3qZWfY3Tl51yPorQ2McHn8pJfcMNTcIvrrZrr90Ykffit0yjrPFWQcRcUxzH20+6OcVdW4yHtUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.60.1': + resolution: {integrity: sha512-EbGRQg4FhrmwLodl+t3JNAnXHWVr9Vp+Zl1QBZVPY4ByfkzIT8cX3K6QWODHtkIZqqJVEWvhHSx3v5PDHsaQag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-vue@6.0.7': + resolution: {integrity: sha512-km+p+XdSz9Sxm5rqUbqcSfZYaAniKxWBj1KURl+Jr7UaPvvX7BmaWMdP69I5rrFDeQGyxAG7NXdc57vz+snhWg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + vue: ^3.2.25 + + '@vitest/eslint-plugin@1.6.19': + resolution: {integrity: sha512-zodmXRsVKFsuHxHJILuTFaaKsrsxm0YsiOX65clk+LpCW9JrVXaf6ERXr0caDs+NEk0S62Jyk0K7XYQ7gWXheA==} + engines: {node: '>=18'} + peerDependencies: + '@typescript-eslint/eslint-plugin': '*' + eslint: '>=8.57.0' + typescript: '>=5.0.0' + vitest: '*' + peerDependenciesMeta: + '@typescript-eslint/eslint-plugin': + optional: true + typescript: + optional: true + vitest: + optional: true + + '@vitest/expect@4.1.8': + resolution: {integrity: sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==} + + '@vitest/mocker@4.1.8': + resolution: {integrity: sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.8': + resolution: {integrity: sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==} + + '@vitest/runner@4.1.8': + resolution: {integrity: sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==} + + '@vitest/snapshot@4.1.8': + resolution: {integrity: sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==} + + '@vitest/spy@4.1.8': + resolution: {integrity: sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==} + + '@vitest/utils@4.1.8': + resolution: {integrity: sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==} + + '@volar/language-core@2.4.28': + resolution: {integrity: sha512-w4qhIJ8ZSitgLAkVay6AbcnC7gP3glYM3fYwKV3srj8m494E3xtrCv6E+bWviiK/8hs6e6t1ij1s2Endql7vzQ==} + + '@volar/source-map@2.4.28': + resolution: {integrity: sha512-yX2BDBqJkRXfKw8my8VarTyjv48QwxdJtvRgUpNE5erCsgEUdI2DsLbpa+rOQVAJYshY99szEcRDmyHbF10ggQ==} + + '@volar/typescript@2.4.28': + resolution: {integrity: sha512-Ja6yvWrbis2QtN4ClAKreeUZPVYMARDYZl9LMEv1iQ1QdepB6wn0jTRxA9MftYmYa4DQ4k/DaSZpFPUfxl8giw==} + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.35': + resolution: {integrity: sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==} + + '@vue/compiler-dom@3.5.35': + resolution: {integrity: sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==} + + '@vue/compiler-sfc@3.5.35': + resolution: {integrity: sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==} + + '@vue/compiler-ssr@3.5.35': + resolution: {integrity: sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.9': + resolution: {integrity: sha512-kIE8wvwlcZ6TJTbNeU2HQNtaxLx3a84aotTITUuL/4bzfPxzajGBOoqjMhwZJ8L9qFYDU/lAYMEEm11dnZOD6g==} + + '@vue/devtools-core@8.1.2': + resolution: {integrity: sha512-ZGGyaSBP4/+bN2Nd9ZHNYAVDRIzMw1rv2RyXWtyZlo6mQal+IDmTvKY4V+DjAEBhaXt30mHmsgYp1yXJ/2tIWg==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-kit@7.7.9': + resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} + + '@vue/devtools-kit@8.1.2': + resolution: {integrity: sha512-f75/upc+GCyjXErpgPGz4582ujS0L/adAltGy+tqXMGUJpgAcfGr6CxnnhpZY8BHuMYt6KpbF8uaFrrQG66rGQ==} + + '@vue/devtools-shared@7.7.9': + resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} + + '@vue/devtools-shared@8.1.2': + resolution: {integrity: sha512-X9RyVFYAdkBe4IUf5v48TxBF/6QPmF8CmWrDAjXzfUHrgQ/HGfTC1A6TqgXqZ03ye66l3AD51BAGD69IvKM9sw==} + + '@vue/eslint-config-prettier@10.2.0': + resolution: {integrity: sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==} + peerDependencies: + eslint: '>= 8.21.0' + prettier: '>= 3.0.0' + + '@vue/eslint-config-typescript@14.7.0': + resolution: {integrity: sha512-iegbMINVc+seZ/QxtzWiOBozctrHiF2WvGedruu2EbLujg9VuU0FQiNcN2z1ycuaoKKpF4m2qzB5HDEMKbxtIg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^9.10.0 || ^10.0.0 + eslint-plugin-vue: ^9.28.0 || ^10.0.0 + typescript: '>=4.8.4' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/language-core@3.3.3': + resolution: {integrity: sha512-X6p+7nfY7vVT6dQwUJ+v0Jfq/lwIfhL2jMi91dQ3ln4hnlGXlxsDu/FNkeyHYgvYtyQy18ZX76IZy7X4diDbiQ==} + + '@vue/reactivity@3.5.35': + resolution: {integrity: sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==} + + '@vue/runtime-core@3.5.35': + resolution: {integrity: sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==} + + '@vue/runtime-dom@3.5.35': + resolution: {integrity: sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==} + + '@vue/server-renderer@3.5.35': + resolution: {integrity: sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==} + peerDependencies: + vue: 3.5.35 + + '@vue/shared@3.5.35': + resolution: {integrity: sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==} + + '@vue/test-utils@2.4.10': + resolution: {integrity: sha512-SmoZ5EA1kYiAFs9NkYdiFFQF+cSnUwnvlYEbY+DogWQZUiqOm/Y29eSbc5T6yi75SgSF9863SBeXniIEoPajCA==} + peerDependencies: + '@vue/compiler-dom': 3.x + '@vue/server-renderer': 3.x + vue: 3.x + peerDependenciesMeta: + '@vue/server-renderer': + optional: true + + '@vue/tsconfig@0.8.1': + resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==} + peerDependencies: + typescript: 5.x + vue: ^3.4.0 + peerDependenciesMeta: + typescript: + optional: true + vue: + optional: true + + '@zag-js/accordion@1.26.1': + resolution: {integrity: sha512-Rqp5zPyWn7w1D2teZAlLytK7okRjfdU4qLuwO7SPdXgeqxr+bn7qP9Bxs4NU78nySA8ZbLZqPbmTA31m9Ya6lw==} + + '@zag-js/anatomy@1.26.1': + resolution: {integrity: sha512-1GVk5D/gFTvBd06w3MgDkSUGtUyU5n6XL8G6KsmuIh6dq16MgZ0TnDsUOkPVBQL3YEiHqlJfeCS6zkgcsu3q2g==} + + '@zag-js/aria-hidden@1.26.1': + resolution: {integrity: sha512-8zZQDODCfXuJ0zfsx6+WVaYh+ScQoFG2ib2FvKr9sxztoJAn1hiLYFKzzlA1aWOxTAh+dFQC/jJI+C1nWbhBkQ==} + + '@zag-js/avatar@1.26.1': + resolution: {integrity: sha512-s34+KSqv/frY3W4Ng9uuuqQbVlgpeWG8b0CtGcELUw9VSldDSOE2ZMIZ1inFLoby9vOOovttHSd/QdGseXkFqQ==} + + '@zag-js/carousel@1.26.1': + resolution: {integrity: sha512-hwxO1p+fsGpu+b8ufxdQAO2UhfP1jUugmBoYnaT3eQJ7Xl/E3MuJ3OMdxgeM9zbBRZuv7IWXVzfLQTNB1lz78g==} + + '@zag-js/checkbox@1.26.1': + resolution: {integrity: sha512-IESEO/WfQ6o45E8beCIOdg0u8tmgSF9gb2F0S1HNBSTmeTq5KSaz47Z8SDkci0g9cw2i+OZIX7BPZI4XpbT3tg==} + + '@zag-js/collection@1.26.1': + resolution: {integrity: sha512-b23L4urNhCCH89TRr5UIWZsCtV9eaBhgTPu91qO2CAgz0fbvmkZJ/E5yr7EI4KeNg0xExbiQrhinZQt2v0LjCA==} + + '@zag-js/combobox@1.26.1': + resolution: {integrity: sha512-VLTTLvzgurvYrsg7UEb11Ad0ZPxWp3ui9/+J3rzgrTMEuLWj8mPfWT0i0l3AG+ruyFlQv5aZNvA/pupk3UK1Tg==} + + '@zag-js/core@1.26.1': + resolution: {integrity: sha512-JesW6C1dlrG36Aa+yteL0v5nt4Zqa9n9coqDJUQ9L1AYWzjz6NTHubsA7ysJlTKwl411gSsssmH9Ey5sRxFEWQ==} + + '@zag-js/date-picker@1.26.1': + resolution: {integrity: sha512-dh105RY/SKXa0iPDmIH5qpXZ1W/Ls+AU0d2l5phcEsVItbgE+FBKYTSuC91w+LNv+MpGPH+PXvatt10tW6SYGA==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/date-utils@1.26.1': + resolution: {integrity: sha512-nMTSfIk0B7MfOIJ0EpFFrHHOGDEI/F0OCG9LVvkwlKZ3evKmgAT5Hw18h2FbQ8ovQ9gNbWoUGPPnrnZJtkzhNw==} + peerDependencies: + '@internationalized/date': '>=3.0.0' + + '@zag-js/dialog@1.26.1': + resolution: {integrity: sha512-kbNzTIMUkn19RcqDKF+3kH5cGPW3bY2FKiw6qtPi072SshHQtuAeE4g5K3wGFdSTJXHBhXYSK04ZR+ok00YbGA==} + + '@zag-js/dismissable@1.26.1': + resolution: {integrity: sha512-n7MdvInqfOh+UJ+VpeIovVU734vf8ekHhfhIugDt41xAzndZW8hdXcBsKnDFK0uY9OVk8cq5FRqpdzhoZuZSgw==} + + '@zag-js/dom-query@1.26.1': + resolution: {integrity: sha512-AcdNV+Zn+Mrz6jq6IpgBJmiooFCuGBzhbbjjU6Drh3TfNDL7q2YXinrZydoD4EGDaFOQ1h71iChTtCZSHw/yNw==} + + '@zag-js/focus-trap@1.26.1': + resolution: {integrity: sha512-88NvG1vxga/Umx54p9K6+nkrC5QLOA/hc5FV68eMKn83u1PCCH8U2+RV/vhMJ7IUTEwI0VTZLOmDfyfbkfomTQ==} + + '@zag-js/focus-visible@1.26.1': + resolution: {integrity: sha512-Hq57XwQWS0a5rjiZcgWhC8ca1e75VoqVLaSGn8XM29lv13423MPRtJ6b/zkEx5G4yZmddgA+wFh2/U2KPztcYA==} + + '@zag-js/interact-outside@1.26.1': + resolution: {integrity: sha512-Kxyg8AuJq4WssVbFyCtY14O611DU7UEiKgDwiNBXdQ5V+XXd4BBBwrA5QK9pk/qjgxZawKOalwjcYsyWhFbwLQ==} + + '@zag-js/live-region@1.26.1': + resolution: {integrity: sha512-Z3c2/ssr5ERIVr0sLchua0e/JrPlbIN3suLZh4oYYPcxpKRjhwZQbN66xa5jm0LfIlW/Z21Obrb8X91GFwXR6w==} + + '@zag-js/menu@1.26.1': + resolution: {integrity: sha512-ElFlY9yqea14XUkRXL1WA3t3SGDh+4jBMXZLIG8GY14GOzoTvruATBg1+Sbd5VVpggAbrUui33F/umWgwh6X4w==} + + '@zag-js/pagination@1.26.1': + resolution: {integrity: sha512-tInkqiMj1+VoFPSN80Y7lS1ABfxTA+xgyjMyx3Y09dgHNhiOKMOYva3eTGIJaEd6OskUayc3TFKqvfT5R43KlQ==} + + '@zag-js/popover@1.26.1': + resolution: {integrity: sha512-nwZfOa7W8NW7npZXqw898nGIBJ7/sk89jglO9Ealqgay5ypOSGBg4v3Afc188Q9c4jFLLqNLQ/LzUhox+h7oLA==} + + '@zag-js/popper@1.26.1': + resolution: {integrity: sha512-wY/YzdXT97gIw8wpPDyU+qf1nlnf6wYwVs3SGDCd6cbgU7iy2jo3Pn6bMJ2OrsFrLZqFVZp6lF2zQtBTNJMZtg==} + + '@zag-js/presence@1.26.1': + resolution: {integrity: sha512-qcbdmwVQdrc4nbHsGTTRBT9UI6lJTmT2ZVv+QQW1ZeegiIEEg/IQAuMHVaPcGGnS2Kec2OJAA174+Br8rZCKtQ==} + + '@zag-js/progress@1.26.1': + resolution: {integrity: sha512-NEmLImi7ZvsTQ1B+AAZARsl3IwvdS4+qv9Wpgf+RIjJVCa0CbE3N7XS+sHuI5X4nWAKbP6Kpc3VKPwZ/9dZ7jQ==} + + '@zag-js/radio-group@1.26.1': + resolution: {integrity: sha512-85BA4b4h6Dq3Qh9VhlRM49gJUk0NGnEqmd86LctNLXMTMeJQ9i40LMeRVlKJqeDDcXMPQDKsINCfATegq2mj2Q==} + + '@zag-js/rect-utils@1.26.1': + resolution: {integrity: sha512-ZvO4kyXXfSPrFmVKgHDTSlTagioU5KNE2VhN6tN/uJYVh2M1us2DBIWtZLGZeGSpdcCuHEpThUp1eES7ZVN8ng==} + + '@zag-js/remove-scroll@1.26.1': + resolution: {integrity: sha512-m1wVmykWfpvmp3Q3jLArCvojR8SM4/DTH6I5KIZAwgIp9QiI16K83ZTSS7TiVkhwt3SgH72F9oAaJT/HA9ZHnQ==} + + '@zag-js/scroll-area@1.26.1': + resolution: {integrity: sha512-+QzuY/zYzl0ZH5Xf7tLg2nTKt8h3B55lu8yvg7WavqT2nLTMcocK6Vr1r7b+9FpGyt3nMYuZ6iUUU1Rdkk2vwg==} + + '@zag-js/scroll-snap@1.26.1': + resolution: {integrity: sha512-fRRvXO0Yl9fMwrjN7zofVcFSQvHEKYRptq4W4lG2DXIKuUrmJwyTLKMREvLka6kRZ420hDfVlTj7PpSPp4OoGg==} + + '@zag-js/select@1.26.1': + resolution: {integrity: sha512-posQbGiEXiLHoUeD2mGsuPPd/cSKat99h78krivAEJqFDijPSmX9l8A0o6sXXxWHFje50gqWxSH4p25IKBGQkA==} + + '@zag-js/slider@1.26.1': + resolution: {integrity: sha512-iHFeugMk9pNZLoijEYeV49h7lLC+twxkpeiTc5KjFgKQB9n8a+H22fIVkl+B+dmIPcfr8tDpri1FfN4hh9kxpA==} + + '@zag-js/store@1.26.1': + resolution: {integrity: sha512-0473Z38EVnl6RcjXecevzfxYbzZWEcCqsx+Q3Eti+5LKvSnl05SomiF5dYDd8qPQ167iWAQWhBYSTvasfjKa4w==} + + '@zag-js/switch@1.26.1': + resolution: {integrity: sha512-OfOF2BYBtEeoI4RU0eYTFH3QAORNQBuVtDrsERJuWldLk9p1sukETXooTFxqaQuLRx98J3Y7duryjvTvNyA8nA==} + + '@zag-js/tabs@1.26.1': + resolution: {integrity: sha512-RI3Od0H6guYfflmCmpKO0hrh1FOjE/OduEEiD1XDn5foeD0uV/qhS8Wdh8R3ikgcaVjb5XrSOrCZXkuc732g8w==} + + '@zag-js/toast@1.26.1': + resolution: {integrity: sha512-TLcN9Bd17fI0069VOV7Ug1kGk5feUK38YKJys8/1cujG+Cu4d5iUif/GUJDrRjcI/P6dPVTzrZ9ag+pCzxkWOA==} + + '@zag-js/tooltip@1.26.1': + resolution: {integrity: sha512-ezBF4DZYo7E+AjVvpOoXnoPwhSnbS3y6Fp9BMvzQRSn5qElZbdQOEZSI9R6MZBTfEo6syXE68MYyCMzKFpkQFg==} + + '@zag-js/types@1.26.1': + resolution: {integrity: sha512-MXRn8x0af7ZVJ50cxCoJU7hLZ3FmPnD53UQnjkYSmOcwr1eE0Wyt7bjGUP1Zv+6hPMTlyZ18XR2rnUkKAtr7/w==} + + '@zag-js/utils@1.26.1': + resolution: {integrity: sha512-OeQia4UV/3osMdrhSa2NmhWvzlnlF1vxOBAdYP5R96ho0FpI15DvAaPQDvE94shzIMgzS4k0pqK5lZWK5FTmqw==} + + '@zag-js/vue@1.26.1': + resolution: {integrity: sha512-uUxMyROrRXYUri+WVN75TDJ5GWXGtZPTgdYXtOmYJBIxAoafPAsxnacwJCe6Lbj0uVX+D8nw7yKVIRJA4TJOvg==} + peerDependencies: + vue: '>=3.0.0' + + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + adler-32@1.3.1: + resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==} + engines: {node: '>=0.8'} + + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + alien-signals@3.2.1: + resolution: {integrity: sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==} + + ansi-escapes@7.3.0: + resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} + engines: {node: '>=18'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.3.1: + resolution: {integrity: sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==} + engines: {node: '>=14'} + + arch@2.2.0: + resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asn1@0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + + assert-plus@1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + aws-sign2@0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.10.33: + resolution: {integrity: sha512-bA6+tcSLpz2tIEdDXZPpPTIuxBcC4+w6SieaYyfigIa4h8GlFxbA17v22Vx3JUtuZQj9SgOsnbK+aTBzyDyEuw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bcrypt-pbkdf@1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + birpc@2.9.0: + resolution: {integrity: sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==} + + birpc@4.0.0: + resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==} + + blob-util@2.0.2: + resolution: {integrity: sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cachedir@2.4.0: + resolution: {integrity: sha512-9EtFOZR8g22CL7BWjJ9BUx1+A/djkofnyW3aOXZORNW2kxoUpx2h+uN2cOqwPmFhnpVmxg+KW2OjOSgChTEvsQ==} + engines: {node: '>=6'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} + + canvg@3.0.11: + resolution: {integrity: sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==} + engines: {node: '>=10.0.0'} + + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + + cfb@1.2.2: + resolution: {integrity: sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==} + engines: {node: '>=0.8'} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + check-more-types@2.24.0: + resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==} + engines: {node: '>= 0.8.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-table3@0.6.1: + resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==} + engines: {node: 10.* || >= 12.*} + + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + codepage@1.15.0: + resolution: {integrity: sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==} + engines: {node: '>=0.8'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + colors@1.4.0: + resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==} + engines: {node: '>=0.1.90'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + common-tags@1.8.2: + resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} + engines: {node: '>=4.0.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@4.0.5: + resolution: {integrity: sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==} + engines: {node: '>=18'} + + core-js@3.49.0: + resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + + core-util-is@1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + + crc-32@1.2.2: + resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} + engines: {node: '>=0.8'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + cypress@15.16.0: + resolution: {integrity: sha512-fy0M0c9xDLEp4v9y7LLKFeAQhIdDsobxDSKpD3JcZpqQefjy9TSzEyVV3HA0zu7hUi0bGHlSYlI7ASub8wgR9A==} + engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0} + hasBin: true + + dashdash@1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + + dayjs@1.11.21: + resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==} + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dompurify@3.4.7: + resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer@0.1.2: + resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + + earcut@3.0.2: + resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecc-jsbn@0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + + electron-to-chromium@1.5.364: + resolution: {integrity: sha512-G/dYE3+AYhyHwzTwg8UbnXf7zqMERYh7l2jJ3QujhFsH8agSYwtnGAR2aZ7f0AakIKJXd5En/Hre4igIUrdlYw==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.22.1: + resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + engines: {node: '>=10.13.0'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + environment@1.1.0: + resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==} + engines: {node: '>=18'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-cypress@5.4.0: + resolution: {integrity: sha512-XAQYuzMpLWJdFRQorPO3GDx4XHqI362qr1/XIp0N6SNTAa8lyzmpTA26qNRc99I53NnqX9l0SHwbHXX7TAKIkg==} + deprecated: 'deprecate: accidentally includes breaking changes from 6.0.0' + peerDependencies: + eslint: '>=9' + + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-vue@10.6.2: + resolution: {integrity: sha512-nA5yUs/B1KmKzvC42fyD0+l9Yd+LtEpVhWRbXuDj0e+ZURcTtyRbMDWUeJmTAh2wC6jC83raS63anNM2YT3NPw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 + '@typescript-eslint/parser': ^7.0.0 || ^8.0.0 + eslint: ^8.57.0 || ^9.0.0 + vue-eslint-parser: ^10.0.0 + peerDependenciesMeta: + '@stylistic/eslint-plugin': + optional: true + '@typescript-eslint/parser': + optional: true + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + event-stream@3.3.4: + resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} + + eventemitter2@6.4.7: + resolution: {integrity: sha512-tYUSVOGeQPKt/eC1ABfhHy5Xd96N3oIijJvN3O9+TsC28T5V9yX9oEfEK5faP0EFSNVOG97qtAS68GBrQB2hDg==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + execa@4.1.0: + resolution: {integrity: sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==} + engines: {node: '>=10'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + executable@4.1.1: + resolution: {integrity: sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==} + engines: {node: '>=4'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extsprintf@1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-png@6.4.0: + resolution: {integrity: sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==} + + fastq@1.20.1: + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-saver@2.0.5: + resolution: {integrity: sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + forever-agent@0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + frac@1.1.2: + resolution: {integrity: sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==} + engines: {node: '>=0.8'} + + from@0.1.7: + resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + getpass@0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + + gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.6.0: + resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasha@5.2.2: + resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} + engines: {node: '>=8'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http-signature@1.4.0: + resolution: {integrity: sha512-G5akfn7eKbpDN+8nPS/cb57YeA1jLTVxjpCj7tmm3QKPdyDy7T+qSC40e9ptydSWvkwjSXw1VbkpyEm39ukeAg==} + engines: {node: '>=0.10'} + + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + human-signals@1.1.1: + resolution: {integrity: sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==} + engines: {node: '>=8.12.0'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + + iobuffer@5.4.0: + resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-fullwidth-code-point@5.1.0: + resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==} + engines: {node: '>=18'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-in-ssh@1.0.0: + resolution: {integrity: sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==} + engines: {node: '>=20'} + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-typedarray@1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + is-what@5.5.0: + resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} + engines: {node: '>=18'} + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + isstream@0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + joi@18.2.1: + resolution: {integrity: sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==} + engines: {node: '>= 20'} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.8: + resolution: {integrity: sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} + hasBin: true + + jsbn@0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@4.0.0: + resolution: {integrity: sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==} + engines: {node: ^18.17.0 || >=20.5.0} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json-stringify-pretty-compact@4.0.0: + resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + jspdf-autotable@5.0.8: + resolution: {integrity: sha512-Hy05N86yBO7CXBrnSLOge7i1ZYpKH2DjQ94iybaP7vBhSInjvRBgDc99ngKzSbSO8Jc98ZCally8I6n0tj2RJQ==} + peerDependencies: + jspdf: ^2 || ^3 || ^4 + + jspdf@4.2.1: + resolution: {integrity: sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==} + + jsprim@2.0.2: + resolution: {integrity: sha512-gqXddjPqQ6G40VdnI6T6yObEC+pDNvyP95wdQhkWkg7crHH3km5qP1FsOXEkzEQwnz6gz5qGTn1c2Y52wP3OyQ==} + engines: {'0': node >=0.6.0} + + kdbush@4.1.0: + resolution: {integrity: sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + lazy-ass@1.6.0: + resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} + engines: {node: '> 0.8'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + listr2@9.0.5: + resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==} + engines: {node: '>=20.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + log-update@6.1.0: + resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} + engines: {node: '>=18'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + map-stream@0.1.0: + resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} + + maplibre-gl@5.24.0: + resolution: {integrity: sha512-ALyFxgtd5R+65UqZ/++lOqwWcC0SNho9c27fYSyLmG7AfnAul2o46F05aDJGPbFU57wos9dgcIySHs0Xe6ia3A==} + engines: {node: '>=16.14.0', npm: '>=8.1.0'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + memorystream@0.3.1: + resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} + engines: {node: '>= 0.10.0'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + murmurhash-js@1.0.0: + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.46: + resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} + engines: {node: '>=18'} + + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + + npm-normalize-package-bin@4.0.0: + resolution: {integrity: sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==} + engines: {node: ^18.17.0 || >=20.5.0} + + npm-run-all2@8.0.4: + resolution: {integrity: sha512-wdbB5My48XKp2ZfJUlhnLVihzeuA1hgBnqB2J9ahV77wLS+/YAJAlN8I+X3DIFIPZ3m5L7nplmlbhNiFDmXRDA==} + engines: {node: ^20.5.0 || >=22.0.0, npm: '>= 10'} + hasBin: true + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + open@11.0.0: + resolution: {integrity: sha512-smsWv2LzFjP03xmvFoJ331ss6h+jixfA4UUV/Bsiyuu4YJPfN+FIQGOIiv4w9/+MoHkfkJ22UIaQWRVFRfH6Vw==} + engines: {node: '>=20'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ospath@1.2.2: + resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + pako@2.1.0: + resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pause-stream@0.0.11: + resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} + + pbf@4.0.2: + resolution: {integrity: sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg==} + hasBin: true + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + performance-now@2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + pidtree@0.6.0: + resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} + engines: {node: '>=0.10'} + hasBin: true + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + pinia@3.0.4: + resolution: {integrity: sha512-l7pqLUFTI/+ESXn6k3nu30ZIzW5E2WZF/LaHJEpoq6ElcLD+wduZoB2kBN19du6K/4FDpPMazY2wJr+IndBtQw==} + peerDependencies: + typescript: '>=4.5.0' + vue: ^3.5.11 + peerDependenciesMeta: + typescript: + optional: true + + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} + + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + engines: {node: ^10 || ^12 || >=14} + + potpack@2.1.0: + resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} + + powershell-utils@0.1.0: + resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==} + engines: {node: '>=20'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + hasBin: true + + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + + protocol-buffers-schema@3.6.1: + resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==} + + proxy-compare@3.0.1: + resolution: {integrity: sha512-V9plBAt3qjMlS1+nC8771KNf6oJ12gExvaxnNzN/9yVRLdTv/lc+oJlnSzrdYDAvBfTStPCoiaCOTmTs0adv7Q==} + + proxy-from-env@1.0.0: + resolution: {integrity: sha512-F2JHgJQ1iqwnHDcQjVBsq3n/uoaFL+iPW/eAeL7kVxy/2RrWaN4WroKjjvbsoRtv0ftelNyC01bjRhn/bhcf4A==} + + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + ps-tree@1.2.0: + resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} + engines: {node: '>= 0.10'} + hasBin: true + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.2: + resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + engines: {node: '>=0.6'} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + + raf@3.4.1: + resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} + + read-package-json-fast@4.0.0: + resolution: {integrity: sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg==} + engines: {node: ^18.17.0 || >=20.5.0} + + regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + + request-progress@3.0.0: + resolution: {integrity: sha512-MnWzEHHaxHO2iWiQuHrUPBi/1WeBf5PkxQqNyNvLl9VAYSdXkP8tQ3pBSeCPD+yw0v0Aq1zosWLz0BdeXpWwZg==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rgbcolor@1.0.1: + resolution: {integrity: sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==} + engines: {node: '>= 0.8.15'} + + rollup@4.61.0: + resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.1: + resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shell-quote@1.8.4: + resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==} + engines: {node: '>= 0.4'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + slice-ansi@7.1.2: + resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==} + engines: {node: '>=18'} + + slice-ansi@8.0.0: + resolution: {integrity: sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==} + engines: {node: '>=20'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + split@0.3.3: + resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} + + ssf@0.11.2: + resolution: {integrity: sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==} + engines: {node: '>=0.8'} + + sshpk@1.18.0: + resolution: {integrity: sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==} + engines: {node: '>=0.10.0'} + hasBin: true + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + stackblur-canvas@2.7.0: + resolution: {integrity: sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==} + engines: {node: '>=0.1.14'} + + start-server-and-test@2.1.5: + resolution: {integrity: sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==} + engines: {node: '>=16'} + hasBin: true + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + stream-combiner@0.0.4: + resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.1: + resolution: {integrity: sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA==} + engines: {node: '>=20'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supercluster@8.0.1: + resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} + + superjson@2.2.6: + resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==} + engines: {node: '>=16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + svg-pathdata@6.0.3: + resolution: {integrity: sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==} + engines: {node: '>=12.0.0'} + + sweetalert2@11.26.25: + resolution: {integrity: sha512-+hunCOJdJ6FLj04T9YSLvvZXRjsvIkTeTKP2e4VF8CaBias961BTnWiSFAy7F/CM5eq3QK2Rraoc5Gzftslvkg==} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + systeminformation@5.31.7: + resolution: {integrity: sha512-/8NC53e5nP9nmhn42/ncdOkyJnOoue/Vy+tJOyUGd1Yv66G069wK4rrziwhrqDETgk78CudTQupw5z19S5uoZw==} + engines: {node: '>=8.0.0'} + os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] + hasBin: true + + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.3.0: + resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + + throttleit@1.0.1: + resolution: {integrity: sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyqueue@3.0.0: + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts-core@7.4.2: + resolution: {integrity: sha512-nwEyF4vl4RSJjwSjBUmOSxc3BFPoIFdlRthJ6e+5v9P3bHNsoD06UjuqMUspqp7vsEZ1beaHi1km+optiE17yA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + + tldts@7.4.2: + resolution: {integrity: sha512-kCwffuaH8ntKtygnWe1b4BJKWiCUH30n5KfoTr6IchcXOwR7chAOFJxFrH3vjANafUYrIA4a7SDL+nn7SiR4Sw==} + hasBin: true + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + tweetnacl@0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@0.8.1: + resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} + engines: {node: '>=8'} + + typescript-eslint@8.60.1: + resolution: {integrity: sha512-6m5hkkRAp8lKvhVpcprAIn5KkehQEh+47oHH2VGnExEh7dhNxXlg6GPAOIu6TxbVQxhebrJDvjl3020ooiWCMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unplugin-utils@0.3.1: + resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==} + engines: {node: '>=20.19.0'} + + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + + verror@1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + + vite-dev-rpc@2.0.0: + resolution: {integrity: sha512-yKwbTwdHKSD2k/aGqyWpPHepo45OQc8lH3/6IfT4ZqeKE26ooKvi4WIEKzqWav8v+9Is8u1k8q54hvOmqASazA==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 || ^8.0.0 + + vite-hot-client@2.2.0: + resolution: {integrity: sha512-76Zs9zrHbH7M7wqeyooGQKdX+yg0pQ0xuQ1PbFp4z5a0Lzn2e5IPFoCswnmqZ4GiwqB4Jo3WcDAMO9jARTJl8w==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 || ^8.0.0 + + vite-plugin-inspect@11.4.1: + resolution: {integrity: sha512-ShOFe2PURXGvRS5OrgmOLZOCwDTD7dEBVt0tMpFPKb9AsvqXKCRGM8QgKrUbRbJYFXScHvDPpGRd28rYidC0tA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 || ^8.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.1.2: + resolution: {integrity: sha512-gt5h1CNryR9Hy0tvhSbqY3j0F7aj0pGxBxWLa1lXSiZVkhdWDf0vbCOZyjh8ivFGE6FDHTGy3zkcZGlMZdVHig==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite-plugin-vue-inspector@6.0.0: + resolution: {integrity: sha512-OpyITJLgZNibxlrik1EmRtvXHDjLRxNPsWkGFTERZs2LgMEdG4W0WoFt5GIgp3a3jRou+eJR8U1zOBk/XQgEbw==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 + + vite@7.3.5: + resolution: {integrity: sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.8: + resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.8 + '@vitest/browser-preview': 4.1.8 + '@vitest/browser-webdriverio': 4.1.8 + '@vitest/coverage-istanbul': 4.1.8 + '@vitest/coverage-v8': 4.1.8 + '@vitest/ui': 4.1.8 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} + + vue-component-type-helpers@3.3.3: + resolution: {integrity: sha512-x4nsFpy5Pe8fqPzp/5vkTPeTTDBpAx4WVtV47Ejt0+2FQrq4pRRsJs7JmYRqMFzTu/LW+pCWEjQ3YVCkPV7f9g==} + + vue-eslint-parser@10.4.0: + resolution: {integrity: sha512-Vxi9pJdbN3ZnVGLODVtZ7y4Y2kzAAE2Cm0CZ3ZDRvydVYxZ6VrnBhLikBsRS+dpwj4Jv4UCv21PTEwF5rQ9WXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + + vue-router@4.6.4: + resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==} + peerDependencies: + vue: ^3.5.0 + + vue-tsc@3.3.3: + resolution: {integrity: sha512-SWUEG7YRUeDJHT7Xsuhf02elYX2gxPzzAII7OxDAh4KNOr4QHQ0Lls0YfnaO5GNd560CwVa2HTfdqmA5MqvRqQ==} + hasBin: true + peerDependencies: + typescript: '>=5.0.0' + + vue@3.5.35: + resolution: {integrity: sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + wait-on@9.0.4: + resolution: {integrity: sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==} + engines: {node: '>=20.0.0'} + hasBin: true + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wmf@1.0.2: + resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} + engines: {node: '>=0.8'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + word@0.3.0: + resolution: {integrity: sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==} + engines: {node: '>=0.8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.3.1: + resolution: {integrity: sha512-g/eziiSUNBSsdDJtCLB8bdYEUMj4jR7AGeUo96p/3dTafgjHhpF4RiCFPiRILwjQoDXx5MqkBr4fwWtR3Ky4Wg==} + engines: {node: '>=20'} + + xlsx@0.18.5: + resolution: {integrity: sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==} + engines: {node: '>=0.8'} + hasBin: true + + xml-name-validator@4.0.0: + resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} + engines: {node: '>=12'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yauzl@3.3.2: + resolution: {integrity: sha512-Md9ankxxN23wncAN8s7+Tn3Co52zLUPMtnrLAbVCnfG5d2tKBFfmygYSgXlqFgXObtzIgqkx7aNgDBpso9+4qA==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@acemir/cssom@0.9.31': {} + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + lru-cache: 11.5.1 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.1 + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3(supports-color@8.1.1) + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + + '@babel/runtime@7.29.7': {} + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.4(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@cypress/request@4.0.1': + dependencies: + aws-sign2: 0.7.0 + aws4: 1.13.2 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 4.0.5 + http-signature: 1.4.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + performance-now: 2.1.0 + qs: 6.15.2 + safe-buffer: 5.2.1 + tough-cookie: 5.1.2 + tunnel-agent: 0.6.0 + + '@cypress/xvfb@1.2.4(supports-color@8.1.1)': + dependencies: + debug: 3.2.7(supports-color@8.1.1) + lodash.once: 4.1.1 + transitivePeerDependencies: + - supports-color + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': + dependencies: + eslint: 9.39.4(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@8.1.1) + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.2.0 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@exodus/bytes@1.15.1': {} + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + + '@hapi/address@5.1.1': + dependencies: + '@hapi/hoek': 11.0.7 + + '@hapi/formula@3.0.2': {} + + '@hapi/hoek@11.0.7': {} + + '@hapi/pinpoint@2.0.1': {} + + '@hapi/tlds@1.1.6': {} + + '@hapi/topo@6.0.2': + dependencies: + '@hapi/hoek': 11.0.7 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/vue@5.0.1(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@iconify/types': 2.0.0 + vue: 3.5.35(typescript@5.9.3) + + '@internationalized/date@3.12.2': + dependencies: + '@swc/helpers': 0.5.23 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kurkle/color@0.3.4': {} + + '@lucide/vue@1.17.0(vue@3.5.35(typescript@5.9.3))': + dependencies: + vue: 3.5.35(typescript@5.9.3) + + '@mapbox/jsonlint-lines-primitives@2.0.2': {} + + '@mapbox/point-geometry@1.1.0': {} + + '@mapbox/tiny-sdf@2.2.0': {} + + '@mapbox/unitbezier@0.0.1': {} + + '@mapbox/vector-tile@2.0.5': + dependencies: + '@mapbox/point-geometry': 1.1.0 + '@types/geojson': 7946.0.16 + pbf: 4.0.2 + + '@mapbox/whoots-js@3.1.0': {} + + '@maplibre/geojson-vt@5.0.4': {} + + '@maplibre/geojson-vt@6.1.0': + dependencies: + kdbush: 4.1.0 + + '@maplibre/maplibre-gl-style-spec@24.8.5': + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/unitbezier': 0.0.1 + json-stringify-pretty-compact: 4.0.0 + minimist: 1.2.8 + quickselect: 3.0.0 + tinyqueue: 3.0.0 + + '@maplibre/mlt@1.1.11': + dependencies: + '@mapbox/point-geometry': 1.1.0 + + '@maplibre/vt-pbf@4.3.0': + dependencies: + '@mapbox/point-geometry': 1.1.0 + '@mapbox/vector-tile': 2.0.5 + '@maplibre/geojson-vt': 5.0.4 + '@types/geojson': 7946.0.16 + '@types/supercluster': 7.1.3 + pbf: 4.0.2 + supercluster: 8.0.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.20.1 + + '@one-ini/wasm@0.1.1': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.3.6': {} + + '@polka/url@1.0.0-next.29': {} + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/rollup-android-arm-eabi@4.61.0': + optional: true + + '@rollup/rollup-android-arm64@4.61.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.61.0': + optional: true + + '@rollup/rollup-darwin-x64@4.61.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.61.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.61.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.61.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.61.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.61.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.61.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.61.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.61.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.61.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.61.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.61.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.61.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.61.0': + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + + '@tailwindcss/node@4.3.0': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.22.1 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.0 + + '@tailwindcss/oxide-android-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.0': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.0': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + optional: true + + '@tailwindcss/oxide@4.3.0': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-arm64': 4.3.0 + '@tailwindcss/oxide-darwin-x64': 4.3.0 + '@tailwindcss/oxide-freebsd-x64': 4.3.0 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 + '@tailwindcss/oxide-linux-x64-musl': 4.3.0 + '@tailwindcss/oxide-wasm32-wasi': 4.3.0 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 + + '@tailwindcss/vite@4.3.0(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@tailwindcss/node': 4.3.0 + '@tailwindcss/oxide': 4.3.0 + tailwindcss: 4.3.0 + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + + '@tanstack/table-core@8.21.3': {} + + '@tanstack/vue-table@8.21.3(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@tanstack/table-core': 8.21.3 + vue: 3.5.35(typescript@5.9.3) + + '@tsconfig/node24@24.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.9': {} + + '@types/file-saver@2.0.7': {} + + '@types/geojson@7946.0.16': {} + + '@types/jsdom@27.0.0': + dependencies: + '@types/node': 24.12.4 + '@types/tough-cookie': 4.0.5 + parse5: 7.3.0 + + '@types/json-schema@7.0.15': {} + + '@types/lodash@4.17.24': {} + + '@types/node@24.12.4': + dependencies: + undici-types: 7.16.0 + + '@types/pako@2.0.4': {} + + '@types/raf@3.4.3': + optional: true + + '@types/sinonjs__fake-timers@8.1.1': {} + + '@types/sizzle@2.3.10': {} + + '@types/supercluster@7.1.3': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/tmp@0.2.6': {} + + '@types/tough-cookie@4.0.5': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/type-utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.1 + eslint: 9.39.4(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.60.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) + '@typescript-eslint/types': 8.60.1 + debug: 4.4.3(supports-color@8.1.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + + '@typescript-eslint/tsconfig-utils@8.60.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.4(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.60.1': {} + + '@typescript-eslint/typescript-estree@8.60.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.60.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3) + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/visitor-keys': 8.60.1 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 10.2.5 + semver: 7.8.1 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/types': 8.60.1 + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.60.1': + dependencies: + '@typescript-eslint/types': 8.60.1 + eslint-visitor-keys: 5.0.1 + + '@vitejs/plugin-vue@6.0.7(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@rolldown/pluginutils': 1.0.1 + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + vue: 3.5.35(typescript@5.9.3) + + '@vitest/eslint-plugin@1.6.19(@typescript-eslint/eslint-plugin@8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)(vitest@4.1.8(@types/node@24.12.4)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)))': + dependencies: + '@typescript-eslint/scope-manager': 8.60.1 + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + optionalDependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + typescript: 5.9.3 + vitest: 4.1.8(@types/node@24.12.4)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@4.1.8': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.8(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))': + dependencies: + '@vitest/spy': 4.1.8 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + + '@vitest/pretty-format@4.1.8': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.8': + dependencies: + '@vitest/utils': 4.1.8 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + '@vitest/utils': 4.1.8 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.8': {} + + '@vitest/utils@4.1.8': + dependencies: + '@vitest/pretty-format': 4.1.8 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + '@volar/language-core@2.4.28': + dependencies: + '@volar/source-map': 2.4.28 + + '@volar/source-map@2.4.28': {} + + '@volar/typescript@2.4.28': + dependencies: + '@volar/language-core': 2.4.28 + path-browserify: 1.0.1 + vscode-uri: 3.1.0 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.29.7)': + dependencies: + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.7) + '@vue/shared': 3.5.35 + optionalDependencies: + '@babel/core': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.29.7)': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/parser': 7.29.7 + '@vue/compiler-sfc': 3.5.35 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.35': + dependencies: + '@babel/parser': 7.29.7 + '@vue/shared': 3.5.35 + entities: 7.0.1 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.35': + dependencies: + '@vue/compiler-core': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/compiler-sfc@3.5.35': + dependencies: + '@babel/parser': 7.29.7 + '@vue/compiler-core': 3.5.35 + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + estree-walker: 2.0.2 + magic-string: 0.30.21 + postcss: 8.5.15 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.35': + dependencies: + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.9': + dependencies: + '@vue/devtools-kit': 7.7.9 + + '@vue/devtools-core@8.1.2(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@vue/devtools-kit': 8.1.2 + '@vue/devtools-shared': 8.1.2 + vue: 3.5.35(typescript@5.9.3) + + '@vue/devtools-kit@7.7.9': + dependencies: + '@vue/devtools-shared': 7.7.9 + birpc: 2.9.0 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.6 + + '@vue/devtools-kit@8.1.2': + dependencies: + '@vue/devtools-shared': 8.1.2 + birpc: 2.9.0 + hookable: 5.5.3 + perfect-debounce: 2.1.0 + + '@vue/devtools-shared@7.7.9': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@8.1.2': {} + + '@vue/eslint-config-prettier@10.2.0(eslint@9.39.4(jiti@2.7.0))(prettier@3.7.4)': + dependencies: + eslint: 9.39.4(jiti@2.7.0) + eslint-config-prettier: 10.1.8(eslint@9.39.4(jiti@2.7.0)) + eslint-plugin-prettier: 5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))(prettier@3.7.4) + prettier: 3.7.4 + transitivePeerDependencies: + - '@types/eslint' + + '@vue/eslint-config-typescript@14.7.0(eslint-plugin-vue@10.6.2(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.7.0))))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + eslint-plugin-vue: 10.6.2(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.7.0))) + fast-glob: 3.3.3 + typescript-eslint: 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.7.0)) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@vue/language-core@3.3.3': + dependencies: + '@volar/language-core': 2.4.28 + '@vue/compiler-dom': 3.5.35 + '@vue/shared': 3.5.35 + alien-signals: 3.2.1 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.4 + + '@vue/reactivity@3.5.35': + dependencies: + '@vue/shared': 3.5.35 + + '@vue/runtime-core@3.5.35': + dependencies: + '@vue/reactivity': 3.5.35 + '@vue/shared': 3.5.35 + + '@vue/runtime-dom@3.5.35': + dependencies: + '@vue/reactivity': 3.5.35 + '@vue/runtime-core': 3.5.35 + '@vue/shared': 3.5.35 + csstype: 3.2.3 + + '@vue/server-renderer@3.5.35(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@vue/compiler-ssr': 3.5.35 + '@vue/shared': 3.5.35 + vue: 3.5.35(typescript@5.9.3) + + '@vue/shared@3.5.35': {} + + '@vue/test-utils@2.4.10(@vue/compiler-dom@3.5.35)(@vue/server-renderer@3.5.35(vue@3.5.35(typescript@5.9.3)))(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@vue/compiler-dom': 3.5.35 + js-beautify: 1.15.4 + vue: 3.5.35(typescript@5.9.3) + vue-component-type-helpers: 3.3.3 + optionalDependencies: + '@vue/server-renderer': 3.5.35(vue@3.5.35(typescript@5.9.3)) + + '@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3))': + optionalDependencies: + typescript: 5.9.3 + vue: 3.5.35(typescript@5.9.3) + + '@zag-js/accordion@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/anatomy@1.26.1': {} + + '@zag-js/aria-hidden@1.26.1': + dependencies: + '@zag-js/dom-query': 1.26.1 + + '@zag-js/avatar@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/carousel@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/scroll-snap': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/checkbox@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-visible': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/collection@1.26.1': + dependencies: + '@zag-js/utils': 1.26.1 + + '@zag-js/combobox@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/aria-hidden': 1.26.1 + '@zag-js/collection': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/core@1.26.1': + dependencies: + '@zag-js/dom-query': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/date-picker@1.26.1(@internationalized/date@3.12.2)': + dependencies: + '@internationalized/date': 3.12.2 + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/date-utils': 1.26.1(@internationalized/date@3.12.2) + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/live-region': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/date-utils@1.26.1(@internationalized/date@3.12.2)': + dependencies: + '@internationalized/date': 3.12.2 + + '@zag-js/dialog@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/aria-hidden': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-trap': 1.26.1 + '@zag-js/remove-scroll': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/dismissable@1.26.1': + dependencies: + '@zag-js/dom-query': 1.26.1 + '@zag-js/interact-outside': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/dom-query@1.26.1': + dependencies: + '@zag-js/types': 1.26.1 + + '@zag-js/focus-trap@1.26.1': + dependencies: + '@zag-js/dom-query': 1.26.1 + + '@zag-js/focus-visible@1.26.1': + dependencies: + '@zag-js/dom-query': 1.26.1 + + '@zag-js/interact-outside@1.26.1': + dependencies: + '@zag-js/dom-query': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/live-region@1.26.1': {} + + '@zag-js/menu@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/rect-utils': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/pagination@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/popover@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/aria-hidden': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-trap': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/remove-scroll': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/popper@1.26.1': + dependencies: + '@floating-ui/dom': 1.7.4 + '@zag-js/dom-query': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/presence@1.26.1': + dependencies: + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + + '@zag-js/progress@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/radio-group@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-visible': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/rect-utils@1.26.1': {} + + '@zag-js/remove-scroll@1.26.1': + dependencies: + '@zag-js/dom-query': 1.26.1 + + '@zag-js/scroll-area@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/scroll-snap@1.26.1': + dependencies: + '@zag-js/dom-query': 1.26.1 + + '@zag-js/select@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/collection': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/slider@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/store@1.26.1': + dependencies: + proxy-compare: 3.0.1 + + '@zag-js/switch@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-visible': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/tabs@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/toast@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dismissable': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/tooltip@1.26.1': + dependencies: + '@zag-js/anatomy': 1.26.1 + '@zag-js/core': 1.26.1 + '@zag-js/dom-query': 1.26.1 + '@zag-js/focus-visible': 1.26.1 + '@zag-js/popper': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + + '@zag-js/types@1.26.1': + dependencies: + csstype: 3.1.3 + + '@zag-js/utils@1.26.1': {} + + '@zag-js/vue@1.26.1(vue@3.5.35(typescript@5.9.3))': + dependencies: + '@zag-js/core': 1.26.1 + '@zag-js/store': 1.26.1 + '@zag-js/types': 1.26.1 + '@zag-js/utils': 1.26.1 + vue: 3.5.35(typescript@5.9.3) + + abbrev@2.0.0: {} + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + adler-32@1.3.1: {} + + agent-base@6.0.2: + dependencies: + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + alien-signals@3.2.1: {} + + ansi-escapes@7.3.0: + dependencies: + environment: 1.1.0 + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + ansis@4.3.1: {} + + arch@2.2.0: {} + + arg@5.0.2: {} + + argparse@2.0.1: {} + + asn1@0.2.6: + dependencies: + safer-buffer: 2.1.2 + + assert-plus@1.0.0: {} + + assertion-error@2.0.1: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + aws-sign2@0.7.0: {} + + aws4@1.13.2: {} + + axios@1.16.1(debug@4.4.3): + dependencies: + follow-redirects: 1.16.0(debug@4.4.3) + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-arraybuffer@1.0.2: + optional: true + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.10.33: {} + + bcrypt-pbkdf@1.0.2: + dependencies: + tweetnacl: 0.14.5 + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + birpc@2.9.0: {} + + birpc@4.0.0: {} + + blob-util@2.0.2: {} + + bluebird@3.7.2: {} + + boolbase@1.0.0: {} + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.33 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.364 + node-releases: 2.0.46 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cachedir@2.4.0: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001793: {} + + canvg@3.0.11: + dependencies: + '@babel/runtime': 7.29.7 + '@types/raf': 3.4.3 + core-js: 3.49.0 + raf: 3.4.1 + regenerator-runtime: 0.13.11 + rgbcolor: 1.0.1 + stackblur-canvas: 2.7.0 + svg-pathdata: 6.0.3 + optional: true + + caseless@0.12.0: {} + + cfb@1.2.2: + dependencies: + adler-32: 1.3.1 + crc-32: 1.2.2 + + chai@6.2.2: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + check-more-types@2.24.0: {} + + ci-info@4.4.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-table3@0.6.1: + dependencies: + string-width: 4.2.3 + optionalDependencies: + colors: 1.4.0 + + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.1 + + clsx@2.1.1: {} + + codepage@1.15.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + colors@1.4.0: + optional: true + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@10.0.1: {} + + commander@6.2.1: {} + + common-tags@1.8.2: {} + + concat-map@0.0.1: {} + + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + + convert-source-map@2.0.0: {} + + copy-anything@4.0.5: + dependencies: + is-what: 5.5.0 + + core-js@3.49.0: + optional: true + + core-util-is@1.0.2: {} + + crc-32@1.2.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + optional: true + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + cssesc@3.0.0: {} + + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.4(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.1 + + csstype@3.1.3: {} + + csstype@3.2.3: {} + + cypress@15.16.0: + dependencies: + '@cypress/request': 4.0.1 + '@cypress/xvfb': 1.2.4(supports-color@8.1.1) + '@types/sinonjs__fake-timers': 8.1.1 + '@types/sizzle': 2.3.10 + '@types/tmp': 0.2.6 + arch: 2.2.0 + blob-util: 2.0.2 + bluebird: 3.7.2 + buffer: 5.7.1 + cachedir: 2.4.0 + chalk: 4.1.2 + ci-info: 4.4.0 + cli-table3: 0.6.1 + commander: 6.2.1 + common-tags: 1.8.2 + dayjs: 1.11.21 + debug: 4.4.3(supports-color@8.1.1) + eventemitter2: 6.4.7 + execa: 4.1.0 + executable: 4.1.1 + fs-extra: 9.1.0 + hasha: 5.2.2 + is-installed-globally: 0.4.0 + listr2: 9.0.5 + lodash: 4.18.1 + log-symbols: 4.1.0 + minimist: 1.2.8 + ospath: 1.2.2 + pretty-bytes: 5.6.0 + process: 0.11.10 + proxy-from-env: 1.0.0 + request-progress: 3.0.0 + supports-color: 8.1.1 + systeminformation: 5.31.7 + tmp: 0.2.7 + tree-kill: 1.2.2 + tslib: 1.14.1 + untildify: 4.0.0 + yauzl: 3.3.2 + + dashdash@1.14.1: + dependencies: + assert-plus: 1.0.0 + + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + + dayjs@1.11.21: {} + + debug@3.2.7(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + debug@4.4.3(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: {} + + dompurify@3.4.7: + optionalDependencies: + '@types/trusted-types': 2.0.7 + optional: true + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer@0.1.2: {} + + earcut@3.0.2: {} + + eastasianwidth@0.2.0: {} + + ecc-jsbn@0.1.2: + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.9 + semver: 7.8.1 + + electron-to-chromium@1.5.364: {} + + emoji-regex@10.6.0: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.22.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@6.0.1: {} + + entities@7.0.1: {} + + entities@8.0.0: {} + + environment@1.1.0: {} + + error-stack-parser-es@1.0.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)): + dependencies: + eslint: 9.39.4(jiti@2.7.0) + + eslint-plugin-cypress@5.4.0(eslint@9.39.4(jiti@2.7.0)): + dependencies: + eslint: 9.39.4(jiti@2.7.0) + globals: 17.6.0 + + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@9.39.4(jiti@2.7.0)))(eslint@9.39.4(jiti@2.7.0))(prettier@3.7.4): + dependencies: + eslint: 9.39.4(jiti@2.7.0) + prettier: 3.7.4 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + eslint-config-prettier: 10.1.8(eslint@9.39.4(jiti@2.7.0)) + + eslint-plugin-vue@10.6.2(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.7.0))): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + eslint: 9.39.4(jiti@2.7.0) + natural-compare: 1.4.0 + nth-check: 2.1.1 + postcss-selector-parser: 7.1.1 + semver: 7.8.1 + vue-eslint-parser: 10.4.0(eslint@9.39.4(jiti@2.7.0)) + xml-name-validator: 4.0.0 + optionalDependencies: + '@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.9 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + esutils@2.0.3: {} + + event-stream@3.3.4: + dependencies: + duplexer: 0.1.2 + from: 0.1.7 + map-stream: 0.1.0 + pause-stream: 0.0.11 + split: 0.3.3 + stream-combiner: 0.0.4 + through: 2.3.8 + + eventemitter2@6.4.7: {} + + eventemitter3@5.0.4: {} + + execa@4.1.0: + dependencies: + cross-spawn: 7.0.6 + get-stream: 5.2.0 + human-signals: 1.1.1 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + executable@4.1.1: + dependencies: + pify: 2.3.0 + + expect-type@1.3.0: {} + + extend@3.0.2: {} + + extsprintf@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-png@6.4.0: + dependencies: + '@types/pako': 2.0.4 + iobuffer: 5.4.0 + pako: 2.1.0 + + fastq@1.20.1: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + fflate@0.8.3: {} + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-saver@2.0.5: {} + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + follow-redirects@1.16.0(debug@4.4.3): + optionalDependencies: + debug: 4.4.3(supports-color@8.1.1) + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forever-agent@0.6.1: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + frac@1.1.2: {} + + from@0.1.7: {} + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-east-asian-width@1.6.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-stream@6.0.1: {} + + getpass@0.1.7: + dependencies: + assert-plus: 1.0.0 + + gl-matrix@3.4.4: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + + globals@14.0.0: {} + + globals@17.6.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasha@5.2.2: + dependencies: + is-stream: 2.0.1 + type-fest: 0.8.1 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + optional: true + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + http-signature@1.4.0: + dependencies: + assert-plus: 1.0.0 + jsprim: 2.0.2 + sshpk: 1.18.0 + + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3(supports-color@8.1.1) + transitivePeerDependencies: + - supports-color + + human-signals@1.1.1: {} + + human-signals@2.1.0: {} + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + ini@1.3.8: {} + + ini@2.0.0: {} + + iobuffer@5.4.0: {} + + is-docker@3.0.0: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-fullwidth-code-point@5.1.0: + dependencies: + get-east-asian-width: 1.6.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-in-ssh@1.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + + is-number@7.0.0: {} + + is-path-inside@3.0.3: {} + + is-potential-custom-element-name@1.0.1: {} + + is-stream@2.0.1: {} + + is-typedarray@1.0.0: {} + + is-unicode-supported@0.1.0: {} + + is-what@5.5.0: {} + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + isstream@0.1.2: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jiti@2.7.0: {} + + joi@18.2.1: + dependencies: + '@hapi/address': 5.1.1 + '@hapi/formula': 3.0.2 + '@hapi/hoek': 11.0.7 + '@hapi/pinpoint': 2.0.1 + '@hapi/tlds': 1.1.6 + '@hapi/topo': 6.0.2 + '@standard-schema/spec': 1.1.0 + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.8 + nopt: 7.2.1 + + js-cookie@3.0.8: {} + + js-tokens@4.0.0: {} + + js-yaml@4.2.0: + dependencies: + argparse: 2.0.1 + + jsbn@0.1.1: {} + + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.15.1 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.21.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@4.0.0: {} + + json-schema-traverse@0.4.1: {} + + json-schema@0.4.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json-stringify-pretty-compact@4.0.0: {} + + json-stringify-safe@5.0.1: {} + + json5@2.2.3: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jspdf-autotable@5.0.8(jspdf@4.2.1): + dependencies: + jspdf: 4.2.1 + + jspdf@4.2.1: + dependencies: + '@babel/runtime': 7.29.7 + fast-png: 6.4.0 + fflate: 0.8.3 + optionalDependencies: + canvg: 3.0.11 + core-js: 3.49.0 + dompurify: 3.4.7 + html2canvas: 1.4.1 + + jsprim@2.0.2: + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + + kdbush@4.1.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + lazy-ass@1.6.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + listr2@9.0.5: + dependencies: + cli-truncate: 5.2.0 + colorette: 2.0.20 + eventemitter3: 5.0.4 + log-update: 6.1.0 + rfdc: 1.4.1 + wrap-ansi: 9.0.2 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + log-update@6.1.0: + dependencies: + ansi-escapes: 7.3.0 + cli-cursor: 5.0.0 + slice-ansi: 7.1.2 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + lru-cache@10.4.3: {} + + lru-cache@11.5.1: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + map-stream@0.1.0: {} + + maplibre-gl@5.24.0: + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/point-geometry': 1.1.0 + '@mapbox/tiny-sdf': 2.2.0 + '@mapbox/unitbezier': 0.0.1 + '@mapbox/vector-tile': 2.0.5 + '@mapbox/whoots-js': 3.1.0 + '@maplibre/geojson-vt': 6.1.0 + '@maplibre/maplibre-gl-style-spec': 24.8.5 + '@maplibre/mlt': 1.1.11 + '@maplibre/vt-pbf': 4.3.0 + '@types/geojson': 7946.0.16 + earcut: 3.0.2 + gl-matrix: 3.4.4 + kdbush: 4.1.0 + murmurhash-js: 1.0.0 + pbf: 4.0.2 + potpack: 2.1.0 + quickselect: 3.0.0 + tinyqueue: 3.0.0 + + math-intrinsics@1.1.0: {} + + mdn-data@2.27.1: {} + + memorystream@0.3.1: {} + + merge-stream@2.0.0: {} + + merge2@1.4.1: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@2.1.0: {} + + mimic-function@5.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.1 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + mitt@3.0.1: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + murmurhash-js@1.0.0: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.46: {} + + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + + npm-normalize-package-bin@4.0.0: {} + + npm-run-all2@8.0.4: + dependencies: + ansi-styles: 6.2.3 + cross-spawn: 7.0.6 + memorystream: 0.3.1 + picomatch: 4.0.4 + pidtree: 0.6.0 + read-package-json-fast: 4.0.0 + shell-quote: 1.8.4 + which: 5.0.0 + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-inspect@1.13.4: {} + + obug@2.1.1: {} + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + open@11.0.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-in-ssh: 1.0.0 + is-inside-container: 1.0.0 + powershell-utils: 0.1.0 + wsl-utils: 0.3.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ospath@1.2.2: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + pako@2.1.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + path-browserify@1.0.1: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pause-stream@0.0.11: + dependencies: + through: 2.3.8 + + pbf@4.0.2: + dependencies: + resolve-protobuf-schema: 2.1.0 + + pend@1.2.0: {} + + perfect-debounce@1.0.0: {} + + perfect-debounce@2.1.0: {} + + performance-now@2.1.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + pidtree@0.6.0: {} + + pify@2.3.0: {} + + pinia@3.0.4(typescript@5.9.3)(vue@3.5.35(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 7.7.9 + vue: 3.5.35(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.15: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + potpack@2.1.0: {} + + powershell-utils@0.1.0: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.7.4: {} + + pretty-bytes@5.6.0: {} + + process@0.11.10: {} + + proto-list@1.2.4: {} + + protocol-buffers-schema@3.6.1: {} + + proxy-compare@3.0.1: {} + + proxy-from-env@1.0.0: {} + + proxy-from-env@2.1.0: {} + + ps-tree@1.2.0: + dependencies: + event-stream: 3.3.4 + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + punycode@2.3.1: {} + + qs@6.15.2: + dependencies: + side-channel: 1.1.0 + + queue-microtask@1.2.3: {} + + quickselect@3.0.0: {} + + raf@3.4.1: + dependencies: + performance-now: 2.1.0 + optional: true + + read-package-json-fast@4.0.0: + dependencies: + json-parse-even-better-errors: 4.0.0 + npm-normalize-package-bin: 4.0.0 + + regenerator-runtime@0.13.11: + optional: true + + request-progress@3.0.0: + dependencies: + throttleit: 1.0.1 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.1 + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rgbcolor@1.0.1: + optional: true + + rollup@4.61.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.61.0 + '@rollup/rollup-android-arm64': 4.61.0 + '@rollup/rollup-darwin-arm64': 4.61.0 + '@rollup/rollup-darwin-x64': 4.61.0 + '@rollup/rollup-freebsd-arm64': 4.61.0 + '@rollup/rollup-freebsd-x64': 4.61.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.61.0 + '@rollup/rollup-linux-arm-musleabihf': 4.61.0 + '@rollup/rollup-linux-arm64-gnu': 4.61.0 + '@rollup/rollup-linux-arm64-musl': 4.61.0 + '@rollup/rollup-linux-loong64-gnu': 4.61.0 + '@rollup/rollup-linux-loong64-musl': 4.61.0 + '@rollup/rollup-linux-ppc64-gnu': 4.61.0 + '@rollup/rollup-linux-ppc64-musl': 4.61.0 + '@rollup/rollup-linux-riscv64-gnu': 4.61.0 + '@rollup/rollup-linux-riscv64-musl': 4.61.0 + '@rollup/rollup-linux-s390x-gnu': 4.61.0 + '@rollup/rollup-linux-x64-gnu': 4.61.0 + '@rollup/rollup-linux-x64-musl': 4.61.0 + '@rollup/rollup-openbsd-x64': 4.61.0 + '@rollup/rollup-openharmony-arm64': 4.61.0 + '@rollup/rollup-win32-arm64-msvc': 4.61.0 + '@rollup/rollup-win32-ia32-msvc': 4.61.0 + '@rollup/rollup-win32-x64-gnu': 4.61.0 + '@rollup/rollup-win32-x64-msvc': 4.61.0 + fsevents: 2.3.3 + + run-applescript@7.1.0: {} + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + semver@6.3.1: {} + + semver@7.8.1: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shell-quote@1.8.4: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + siginfo@2.0.0: {} + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + slice-ansi@7.1.2: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + slice-ansi@8.0.0: + dependencies: + ansi-styles: 6.2.3 + is-fullwidth-code-point: 5.1.0 + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + split@0.3.3: + dependencies: + through: 2.3.8 + + ssf@0.11.2: + dependencies: + frac: 1.1.2 + + sshpk@1.18.0: + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + + stackback@0.0.2: {} + + stackblur-canvas@2.7.0: + optional: true + + start-server-and-test@2.1.5: + dependencies: + arg: 5.0.2 + bluebird: 3.7.2 + check-more-types: 2.24.0 + debug: 4.4.3(supports-color@8.1.1) + execa: 5.1.1 + lazy-ass: 1.6.0 + ps-tree: 1.2.0 + wait-on: 9.0.4(debug@4.4.3) + transitivePeerDependencies: + - supports-color + + std-env@4.1.0: {} + + stream-combiner@0.0.4: + dependencies: + duplexer: 0.1.2 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + string-width@8.2.1: + dependencies: + get-east-asian-width: 1.6.0 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + supercluster@8.0.1: + dependencies: + kdbush: 4.1.0 + + superjson@2.2.6: + dependencies: + copy-anything: 4.0.5 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + svg-pathdata@6.0.3: + optional: true + + sweetalert2@11.26.25: {} + + symbol-tree@3.2.4: {} + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + systeminformation@5.31.7: {} + + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.0: {} + + tapable@2.3.3: {} + + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + optional: true + + throttleit@1.0.1: {} + + through@2.3.8: {} + + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyqueue@3.0.0: {} + + tinyrainbow@3.1.0: {} + + tldts-core@6.1.86: {} + + tldts-core@7.4.2: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + + tldts@7.4.2: + dependencies: + tldts-core: 7.4.2 + + tmp@0.2.7: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + totalist@3.0.1: {} + + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tough-cookie@6.0.1: + dependencies: + tldts: 7.4.2 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tw-animate-css@1.4.0: {} + + tweetnacl@0.14.5: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@0.8.1: {} + + typescript-eslint@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.60.1(@typescript-eslint/parser@8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.60.1(eslint@9.39.4(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + universalify@2.0.1: {} + + unplugin-utils@0.3.1: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.4 + + untildify@4.0.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + optional: true + + verror@1.10.0: + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + + vite-dev-rpc@2.0.0(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + birpc: 4.0.0 + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + vite-hot-client: 2.2.0(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) + + vite-hot-client@2.2.0(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + + vite-plugin-inspect@11.4.1(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + ansis: 4.3.1 + error-stack-parser-es: 1.0.5 + obug: 2.1.1 + ohash: 2.0.11 + open: 11.0.0 + perfect-debounce: 2.1.0 + sirv: 3.0.2 + unplugin-utils: 0.3.1 + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + vite-dev-rpc: 2.0.0(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) + + vite-plugin-vue-devtools@8.1.2(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0))(vue@3.5.35(typescript@5.9.3)): + dependencies: + '@vue/devtools-core': 8.1.2(vue@3.5.35(typescript@5.9.3)) + '@vue/devtools-kit': 8.1.2 + '@vue/devtools-shared': 8.1.2 + sirv: 3.0.2 + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + vite-plugin-inspect: 11.4.1(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) + vite-plugin-vue-inspector: 6.0.0(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + + vite-plugin-vue-inspector@6.0.0(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.7) + '@vue/compiler-dom': 3.5.35 + kolorist: 1.8.0 + magic-string: 0.30.21 + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + transitivePeerDependencies: + - supports-color + + vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.15 + rollup: 4.61.0 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.12.4 + fsevents: 2.3.3 + jiti: 2.7.0 + lightningcss: 1.32.0 + + vitest@4.1.8(@types/node@24.12.4)(jsdom@27.4.0)(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)): + dependencies: + '@vitest/expect': 4.1.8 + '@vitest/mocker': 4.1.8(vite@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)) + '@vitest/pretty-format': 4.1.8 + '@vitest/runner': 4.1.8 + '@vitest/snapshot': 4.1.8 + '@vitest/spy': 4.1.8 + '@vitest/utils': 4.1.8 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.4 + jsdom: 27.4.0 + transitivePeerDependencies: + - msw + + vscode-uri@3.1.0: {} + + vue-component-type-helpers@3.3.3: {} + + vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.7.0)): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.39.4(jiti@2.7.0) + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + semver: 7.8.1 + transitivePeerDependencies: + - supports-color + + vue-router@4.6.4(vue@3.5.35(typescript@5.9.3)): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.35(typescript@5.9.3) + + vue-tsc@3.3.3(typescript@5.9.3): + dependencies: + '@volar/typescript': 2.4.28 + '@vue/language-core': 3.3.3 + typescript: 5.9.3 + + vue@3.5.35(typescript@5.9.3): + dependencies: + '@vue/compiler-dom': 3.5.35 + '@vue/compiler-sfc': 3.5.35 + '@vue/runtime-dom': 3.5.35 + '@vue/server-renderer': 3.5.35(vue@3.5.35(typescript@5.9.3)) + '@vue/shared': 3.5.35 + optionalDependencies: + typescript: 5.9.3 + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + wait-on@9.0.4(debug@4.4.3): + dependencies: + axios: 1.16.1(debug@4.4.3) + joi: 18.2.1 + lodash: 4.18.1 + minimist: 1.2.8 + rxjs: 7.8.2 + transitivePeerDependencies: + - debug + - supports-color + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@5.0.0: + dependencies: + isexe: 3.1.5 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wmf@1.0.2: {} + + word-wrap@1.2.5: {} + + word@0.3.0: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + wsl-utils@0.3.1: + dependencies: + is-wsl: 3.1.1 + powershell-utils: 0.1.0 + + xlsx@0.18.5: + dependencies: + adler-32: 1.3.1 + cfb: 1.2.2 + codepage: 1.15.0 + crc-32: 1.2.2 + ssf: 0.11.2 + wmf: 1.0.2 + word: 0.3.0 + + xml-name-validator@4.0.0: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yauzl@3.3.2: + dependencies: + pend: 1.2.0 + + yocto-queue@0.1.0: {} diff --git a/fe/public/favicon.ico b/fe/public/favicon.ico new file mode 100644 index 0000000..84fbd8a Binary files /dev/null and b/fe/public/favicon.ico differ diff --git a/fe/src/App.vue b/fe/src/App.vue new file mode 100644 index 0000000..7c2aa3f --- /dev/null +++ b/fe/src/App.vue @@ -0,0 +1,3 @@ + diff --git a/fe/src/assets/css/themes/enigma/side-menu.css b/fe/src/assets/css/themes/enigma/side-menu.css new file mode 100644 index 0000000..0b84c89 --- /dev/null +++ b/fe/src/assets/css/themes/enigma/side-menu.css @@ -0,0 +1,309 @@ +.enigma { + @media (min-width: theme(screens.xl)) { + .side-menu--collapsed:hover + .content.content--compact { + margin-left: 320px; + } + + .side-menu--collapsed:hover + .content.content--compact .content__scroll-area { + margin-left: -155px; + } + + .side-menu--collapsed:hover + .content.content--compact .top-bar { + margin-left: 320px; + } + + .side-menu--collapsed:hover + .content.content--compact .top-bar::before { + margin-left: -320px; + } + + .side-menu--collapsed:hover + .content.content--compact .top-bar::after { + margin-left: -320px; + } + } + + .side-menu { + @media (min-width: theme(screens.xl)) { + &.side-menu--collapsed { + &:hover { + .side-menu__group-label { + color: currentColor; + &:before { + @apply opacity-0; + } + } + .side-menu__link { + .side-menu__link__title { + color: currentColor; + } + .side-menu__link__badge { + opacity: 1; + } + .side-menu__link__chevron { + opacity: 1; + } + } + ul.scrollable { + > li { + > .side-menu__link { + .side-menu__link__icon { + margin-left: 0; + } + } + li { + .side-menu__link { + .side-menu__link__icon { + margin-left: 0; + } + } + } + } + } + } + .side-menu__group-label { + position: relative; + color: transparent; + transition-property: theme(transitionProperty.colors); + transition-duration: 100ms; + &:before { + content: '...'; + position: absolute; + text-align: center; + left: 0; + right: 0; + color: var(--color-background); + transition-property: opacity; + transition-duration: 100ms; + } + } + .side-menu__link { + .side-menu__link__title { + white-space: nowrap; + color: transparent; + transition-property: color; + transition-duration: theme(transitionDuration.300); + } + .side-menu__link__badge { + opacity: 0; + transition-property: opacity; + transition-duration: theme(transitionDuration.300); + } + .side-menu__link__chevron { + opacity: 0; + transition-property: opacity; + transition-duration: theme(transitionDuration.300); + } + .side-menu__link__icon { + transition-property: margin; + transition-duration: 100ms; + } + } + ul.scrollable { + > li { + > .side-menu__link { + .side-menu__link__icon { + margin-left: theme(spacing[2.5]); + } + } + li { + .side-menu__link { + .side-menu__link__icon { + margin-left: theme(spacing[1.5]); + } + } + } + } + } + } + ul.scrollable { + > li { + > .side-menu__link { + &.side-menu__link--active { + } + } + } + } + } + .side-menu__group-label { + white-space: nowrap; + font-size: theme(fontSize.xs); + opacity: theme(opacity.50); + margin-left: theme(spacing.2); + margin-top: theme(spacing.7); + margin-bottom: theme(spacing.3); + text-transform: uppercase; + } + .side-menu__link { + display: flex; + align-items: center; + margin-bottom: theme(spacing[1.5]); + padding: theme(spacing.4) theme(spacing.5); + &.side-menu__link--active { + font-weight: theme(fontWeight.medium); + .side-menu__link__icon { + } + .side-menu__link__title { + color: var(--color-primary); + } + .side-menu__link__chevron { + } + } + .side-menu__link__icon { + flex: none; + width: 1.15rem; + height: 1.15rem; + --color: color-mix(in oklch, var(--color-primary), transparent 30%); + } + .side-menu__link__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-left: theme(spacing.3); + color: color-mix(in oklch, var(--color-foreground), transparent 20%); + } + .side-menu__link__badge { + min-width: 1.15rem; + height: 1.2rem; + padding: 0 theme(spacing.2); + display: flex; + align-items: center; + justify-content: center; + font-size: theme(fontSize.xs); + margin-left: theme(spacing[3.5]); + font-weight: theme(fontWeight.medium); + color: color-mix(in oklch, var(--color-primary), transparent 20%); + background-color: color-mix(in oklch, var(--color-primary), transparent 85%); + border: 1px solid color-mix(in oklch, var(--color-primary), transparent 85%); + border-radius: theme(borderRadius.lg); + } + .side-menu__link__chevron { + margin-left: auto; + stroke-width: 1.2; + color: color-mix(in oklch, var(--color-foreground), transparent 20%); + } + } + ul.scrollable { + position: relative; + > li { + > .side-menu__link { + border-radius: theme(borderRadius.full); + &.side-menu__link--active { + background: color-mix(in oklch, var(--color-background), var(--color-foreground) 3%); + border: 1px solid color-mix(in oklch, var(--color-foreground), transparent 90%); + box-shadow: theme('boxShadow.sm'); + &:before { + content: ''; + width: 7.8rem; + height: 7.8rem; + opacity: 0.1; + background-color: color-mix( + in oklch, + var(--color-background), + var(--color-foreground) 90% + ); + z-index: -2; + position: absolute; + right: 0px; + margin-right: -11.7em; + mask-repeat: no-repeat; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 16.039 122.1'%3E%3Cpath id='Union_1' data-name='Union 1' d='M16.038,122H16v-2.213a95.8,95.8,0,0,0-2.886-20.735A94.894,94.894,0,0,0,5.331,78.618,39.039,39.039,0,0,1,0,61.051,39.035,39.035,0,0,1,5.331,43.484a94.9,94.9,0,0,0,7.783-20.435A95.747,95.747,0,0,0,16,2.314V0l.039,122v0Z'/%3E%3C/svg%3E"); + } + &:after { + content: ''; + width: 7.8rem; + height: 7.8rem; + background-color: color-mix( + in oklch, + var(--color-background), + var(--color-foreground) 2% + ); + z-index: -1; + position: absolute; + right: -1px; + margin-right: -11.7em; + mask-repeat: no-repeat; + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 16.039 122.1'%3E%3Cpath id='Union_1' data-name='Union 1' d='M16.038,122H16v-2.213a95.8,95.8,0,0,0-2.886-20.735A94.894,94.894,0,0,0,5.331,78.618,39.039,39.039,0,0,1,0,61.051,39.035,39.035,0,0,1,5.331,43.484a94.9,94.9,0,0,0,7.783-20.435A95.747,95.747,0,0,0,16,2.314V0l.039,122v0Z'/%3E%3C/svg%3E"); + } + } + &:hover:not(.side-menu__link--active) { + background: color-mix(in oklch, var(--color-background), transparent 50%); + } + } + ul { + padding: theme(spacing.1) 0; + background: color-mix(in oklch, var(--color-background), transparent 50%); + border-radius: theme(borderRadius.2xl); + border: 1px solid color-mix(in oklch, var(--color-foreground), transparent 90%); + margin-left: theme(spacing.1); + margin-right: theme(spacing.1); + margin-bottom: theme(spacing.2); + .side-menu__link { + margin-bottom: 0; + } + ul { + margin: 0px -1px; + } + } + } + } + } +} + +.dark { + .enigma { + .side-menu { + .side-menu__group-label { + opacity: theme(opacity.30); + } + .side-menu__link { + .side-menu__link__icon { + --color: --alpha(var(--color-foreground) / 70%); + } + .side-menu__link__title { + color: --alpha(var(--color-foreground) / 70%); + } + .side-menu__link__badge { + --color-primary: --alpha(var(--color-foreground) / 70%); + } + &.side-menu__link--active { + .side-menu__link__title { + color: var(--color-foreground); + } + } + } + ul.scrollable { + > li { + > .side-menu__link { + &.side-menu__link--active { + background: color-mix(in oklch, var(--color-background), white 14%); + &:before { + background-color: var(--color-foreground); + } + &:after { + background-color: color-mix(in oklch, var(--color-foreground), black 84%); + } + } + &:hover:not(.side-menu__link--active) { + background: color-mix(in oklch, var(--color-background), white 14%); + } + } + ul { + background: color-mix(in oklch, var(--color-background), white 10%); + } + } + } + } + } +} + +[data-theme='default']:not(.dark) .enigma .top-bar::before { + background-color: transparent; + background-image: var(--color-primary-gradient); + border-color: color-mix(in oklch, var(--color-blue-600), transparent 40%); +} + +[data-theme='default']:not(.dark) .enigma .top-bar::after { + background-color: transparent; + background-image: var(--color-primary-gradient); + opacity: 0.3; + border-color: transparent; +} diff --git a/fe/src/assets/css/themes/enigma/top-menu.css b/fe/src/assets/css/themes/enigma/top-menu.css new file mode 100644 index 0000000..aa8eb32 --- /dev/null +++ b/fe/src/assets/css/themes/enigma/top-menu.css @@ -0,0 +1,262 @@ +.enigma { + .top-menu { + @media (min-width: theme(screens.xl)) { + @apply border-0; + display: flex; + z-index: 20; + height: theme(spacing.14); + gap: theme(spacing.6); + padding: 0 theme(spacing.14); + margin-bottom: -1px; + + &>li { + height: 100%; + position: relative; + + &>.top-menu__link { + height: 100%; + display: flex; + align-items: center; + gap: theme(spacing.3); + position: relative; + padding: 0 theme(spacing.7); + border-radius: theme(borderRadius.full); + + &.top-menu__link--active { + background: color-mix(in oklch, + var(--color-background), + var(--color-foreground) 3%); + border: 1px solid color-mix(in oklch, + var(--color-foreground), + transparent 90%); + box-shadow: theme("boxShadow.sm"); + + &:before { + content: ""; + width: 7.8rem; + height: 7.8rem; + opacity: 0.1; + background-color: color-mix(in oklch, + var(--color-background), + var(--color-foreground) 90%); + z-index: -2; + position: absolute; + bottom: 0px; + left: 0px; + right: 0px; + margin-left: auto; + margin-right: auto; + margin-bottom: -10.1em; + mask-repeat: no-repeat; + transform: rotate(90deg); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 16.039 122.1'%3E%3Cpath id='Union_1' data-name='Union 1' d='M16.038,122H16v-2.213a95.8,95.8,0,0,0-2.886-20.735A94.894,94.894,0,0,0,5.331,78.618,39.039,39.039,0,0,1,0,61.051,39.035,39.035,0,0,1,5.331,43.484a94.9,94.9,0,0,0,7.783-20.435A95.747,95.747,0,0,0,16,2.314V0l.039,122v0Z'/%3E%3C/svg%3E"); + pointer-events: none; + } + + &:after { + content: ""; + width: 7.8rem; + height: 7.8rem; + background-color: color-mix(in oklch, + var(--color-background), + var(--color-foreground) 2%); + z-index: -1; + position: absolute; + bottom: -1px; + left: 0px; + right: 0px; + margin-left: auto; + margin-right: auto; + margin-bottom: -10.1em; + mask-repeat: no-repeat; + transform: rotate(90deg); + mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%' viewBox='0 0 16.039 122.1'%3E%3Cpath id='Union_1' data-name='Union 1' d='M16.038,122H16v-2.213a95.8,95.8,0,0,0-2.886-20.735A94.894,94.894,0,0,0,5.331,78.618,39.039,39.039,0,0,1,0,61.051,39.035,39.035,0,0,1,5.331,43.484a94.9,94.9,0,0,0,7.783-20.435A95.747,95.747,0,0,0,16,2.314V0l.039,122v0Z'/%3E%3C/svg%3E"); + pointer-events: none; + } + + .top-menu__link__title { + font-weight: theme(fontWeight.medium); + } + + .top-menu__link__icon { + --color: var(--color-primary); + } + } + + .top-menu__link__icon { + &>svg { + --color: var(--color-primary); + } + } + + .top-menu__link__title { + display: flex; + align-items: center; + gap: theme(spacing.3); + } + + .top-menu__link__chevron { + margin-left: auto; + } + } + + &:hover>ul { + display: block; + } + + ul { + display: none; + width: theme(spacing.72); + position: absolute; + left: 50%; + transform: translateX(-50%); + margin-top: calc(theme(spacing.20) * -1); + background: var(--color-background); + border: 1px solid --alpha(var(--color-foreground) / 10%); + border-radius: theme(borderRadius.2xl); + padding: theme(spacing.3) theme(spacing.2); + box-shadow: theme(boxShadow.lg); + z-index: 2; + + .top-menu__link { + height: theme(spacing.12); + display: flex; + align-items: center; + gap: theme(spacing.3); + padding: 0 theme(spacing.5); + position: relative; + + .top-menu__link__title { + width: 100%; + display: flex; + align-items: center; + gap: theme(spacing.3); + } + + .top-menu__link__chevron { + margin-left: auto; + } + } + + li { + position: relative; + + &:hover>ul { + display: block; + } + } + + ul { + display: none; + margin-top: 0; + left: 100%; + top: 0; + transform: translateX(0); + } + } + } + } + + @media (max-width: theme(screens.xl)) { + padding: theme(spacing.5) theme(spacing.4); + + >li { + z-index: 10; + position: relative; + + ul { + padding: theme(spacing.1) 0; + background: color-mix(in oklch, + var(--color-background), + transparent 90%); + border-radius: theme(borderRadius.2xl); + border: 1px solid color-mix(in oklch, + var(--color-background), + transparent 87%); + margin-left: theme(spacing.2); + margin-right: theme(spacing.2); + margin-bottom: theme(spacing.2); + + .top-menu__link { + margin-bottom: 0; + } + + ul { + margin: 0px -1px; + } + } + + .top-menu__link { + display: flex; + align-items: center; + margin-bottom: theme(spacing[1.5]); + padding: theme(spacing.4) theme(spacing.5); + + &.top-menu__link--active { + font-weight: theme(fontWeight.medium); + + .top-menu__link__title { + color: var(--color-background); + } + } + + .top-menu__link__icon>svg { + width: 1.15rem; + height: 1.15rem; + --color: var(--color-background); + } + + .top-menu__link__title { + width: 100%; + display: flex; + align-items: center; + margin-left: theme(spacing.3); + color: --alpha(var(--color-background) / 80%); + + .top-menu__link__chevron { + margin-left: auto; + stroke-width: 1.2; + color: color-mix(in oklch, + var(--color-background), + transparent 20%); + } + } + } + } + } + } +} + +.dark { + .enigma { + .top-menu { + @media (min-width: theme(screens.xl)) { + >li { + >.top-menu__link { + &.top-menu__link--active { + background: color-mix(in oklch, + var(--color-background), + white 14%); + + &:before { + background-color: var(--color-foreground); + } + + &:after { + background-color: color-mix(in oklch, + var(--color-foreground), + black 85%); + } + } + } + } + } + + @media (max-width: theme(screens.xl)) { + >li { + --color-background: var(--color-foreground); + } + } + } + } +} \ No newline at end of file diff --git a/fe/src/assets/images/Logo-RAJD.png b/fe/src/assets/images/Logo-RAJD.png new file mode 100644 index 0000000..37e434c Binary files /dev/null and b/fe/src/assets/images/Logo-RAJD.png differ diff --git a/fe/src/assets/images/accent.svg b/fe/src/assets/images/accent.svg new file mode 100644 index 0000000..9569b68 --- /dev/null +++ b/fe/src/assets/images/accent.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/src/assets/images/bg-pattern.svg b/fe/src/assets/images/bg-pattern.svg new file mode 100644 index 0000000..e6a7c20 --- /dev/null +++ b/fe/src/assets/images/bg-pattern.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/src/assets/images/error-illustration.svg b/fe/src/assets/images/error-illustration.svg new file mode 100644 index 0000000..19f09e9 --- /dev/null +++ b/fe/src/assets/images/error-illustration.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/src/assets/images/fakers/food-beverage-1.jpg b/fe/src/assets/images/fakers/food-beverage-1.jpg new file mode 100755 index 0000000..547586c Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-1.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-10.jpg b/fe/src/assets/images/fakers/food-beverage-10.jpg new file mode 100755 index 0000000..b860fcc Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-10.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-11.jpg b/fe/src/assets/images/fakers/food-beverage-11.jpg new file mode 100755 index 0000000..8136d3b Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-11.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-12.jpg b/fe/src/assets/images/fakers/food-beverage-12.jpg new file mode 100755 index 0000000..dc18308 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-12.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-13.jpg b/fe/src/assets/images/fakers/food-beverage-13.jpg new file mode 100755 index 0000000..2daf94d Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-13.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-14.jpg b/fe/src/assets/images/fakers/food-beverage-14.jpg new file mode 100755 index 0000000..53ab2f9 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-14.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-15.jpg b/fe/src/assets/images/fakers/food-beverage-15.jpg new file mode 100755 index 0000000..a78064a Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-15.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-16.jpg b/fe/src/assets/images/fakers/food-beverage-16.jpg new file mode 100755 index 0000000..39282a9 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-16.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-17.jpg b/fe/src/assets/images/fakers/food-beverage-17.jpg new file mode 100755 index 0000000..8ad5e92 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-17.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-18.jpg b/fe/src/assets/images/fakers/food-beverage-18.jpg new file mode 100755 index 0000000..70f6ba7 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-18.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-19.jpg b/fe/src/assets/images/fakers/food-beverage-19.jpg new file mode 100755 index 0000000..b76b335 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-19.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-2.jpg b/fe/src/assets/images/fakers/food-beverage-2.jpg new file mode 100755 index 0000000..c49f8d1 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-2.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-3.jpg b/fe/src/assets/images/fakers/food-beverage-3.jpg new file mode 100755 index 0000000..9d027e1 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-3.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-4.jpg b/fe/src/assets/images/fakers/food-beverage-4.jpg new file mode 100755 index 0000000..94579c6 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-4.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-5.jpg b/fe/src/assets/images/fakers/food-beverage-5.jpg new file mode 100755 index 0000000..ddf354f Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-5.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-6.jpg b/fe/src/assets/images/fakers/food-beverage-6.jpg new file mode 100755 index 0000000..efe41d6 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-6.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-7.jpg b/fe/src/assets/images/fakers/food-beverage-7.jpg new file mode 100755 index 0000000..7ce9373 Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-7.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-8.jpg b/fe/src/assets/images/fakers/food-beverage-8.jpg new file mode 100755 index 0000000..70ce8da Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-8.jpg differ diff --git a/fe/src/assets/images/fakers/food-beverage-9.jpg b/fe/src/assets/images/fakers/food-beverage-9.jpg new file mode 100755 index 0000000..ff4e94b Binary files /dev/null and b/fe/src/assets/images/fakers/food-beverage-9.jpg differ diff --git a/fe/src/assets/images/fakers/preview-1.jpg b/fe/src/assets/images/fakers/preview-1.jpg new file mode 100755 index 0000000..e303cc2 Binary files /dev/null and b/fe/src/assets/images/fakers/preview-1.jpg differ diff --git a/fe/src/assets/images/fakers/preview-10.jpg b/fe/src/assets/images/fakers/preview-10.jpg new file mode 100755 index 0000000..c0b6e3f Binary files /dev/null and b/fe/src/assets/images/fakers/preview-10.jpg differ diff --git a/fe/src/assets/images/fakers/preview-11.jpg b/fe/src/assets/images/fakers/preview-11.jpg new file mode 100755 index 0000000..3f2ef43 Binary files /dev/null and b/fe/src/assets/images/fakers/preview-11.jpg differ diff --git a/fe/src/assets/images/fakers/preview-12.jpg b/fe/src/assets/images/fakers/preview-12.jpg new file mode 100755 index 0000000..e88dcf2 Binary files /dev/null and b/fe/src/assets/images/fakers/preview-12.jpg differ diff --git a/fe/src/assets/images/fakers/preview-13.jpg b/fe/src/assets/images/fakers/preview-13.jpg new file mode 100755 index 0000000..348cbfe Binary files /dev/null and b/fe/src/assets/images/fakers/preview-13.jpg differ diff --git a/fe/src/assets/images/fakers/preview-14.jpg b/fe/src/assets/images/fakers/preview-14.jpg new file mode 100755 index 0000000..da2cf26 Binary files /dev/null and b/fe/src/assets/images/fakers/preview-14.jpg differ diff --git a/fe/src/assets/images/fakers/preview-15.jpg b/fe/src/assets/images/fakers/preview-15.jpg new file mode 100755 index 0000000..61008f8 Binary files /dev/null and b/fe/src/assets/images/fakers/preview-15.jpg differ diff --git a/fe/src/assets/images/fakers/preview-2.jpg b/fe/src/assets/images/fakers/preview-2.jpg new file mode 100755 index 0000000..b709fe7 Binary files /dev/null and b/fe/src/assets/images/fakers/preview-2.jpg differ diff --git a/fe/src/assets/images/fakers/preview-3.jpg b/fe/src/assets/images/fakers/preview-3.jpg new file mode 100755 index 0000000..3c7c5fa Binary files /dev/null and b/fe/src/assets/images/fakers/preview-3.jpg differ diff --git a/fe/src/assets/images/fakers/preview-4.jpg b/fe/src/assets/images/fakers/preview-4.jpg new file mode 100755 index 0000000..ddd80c8 Binary files /dev/null and b/fe/src/assets/images/fakers/preview-4.jpg differ diff --git a/fe/src/assets/images/fakers/preview-5.jpg b/fe/src/assets/images/fakers/preview-5.jpg new file mode 100755 index 0000000..736a6bd Binary files /dev/null and b/fe/src/assets/images/fakers/preview-5.jpg differ diff --git a/fe/src/assets/images/fakers/preview-6.jpg b/fe/src/assets/images/fakers/preview-6.jpg new file mode 100755 index 0000000..d696a88 Binary files /dev/null and b/fe/src/assets/images/fakers/preview-6.jpg differ diff --git a/fe/src/assets/images/fakers/preview-7.jpg b/fe/src/assets/images/fakers/preview-7.jpg new file mode 100755 index 0000000..d43746a Binary files /dev/null and b/fe/src/assets/images/fakers/preview-7.jpg differ diff --git a/fe/src/assets/images/fakers/preview-8.jpg b/fe/src/assets/images/fakers/preview-8.jpg new file mode 100755 index 0000000..9bd6b3a Binary files /dev/null and b/fe/src/assets/images/fakers/preview-8.jpg differ diff --git a/fe/src/assets/images/fakers/preview-9.jpg b/fe/src/assets/images/fakers/preview-9.jpg new file mode 100755 index 0000000..2df7b9c Binary files /dev/null and b/fe/src/assets/images/fakers/preview-9.jpg differ diff --git a/fe/src/assets/images/fakers/profile-1.jpg b/fe/src/assets/images/fakers/profile-1.jpg new file mode 100755 index 0000000..766ead5 Binary files /dev/null and b/fe/src/assets/images/fakers/profile-1.jpg differ diff --git a/fe/src/assets/images/fakers/profile-10.jpg b/fe/src/assets/images/fakers/profile-10.jpg new file mode 100755 index 0000000..c8cfd39 Binary files /dev/null and b/fe/src/assets/images/fakers/profile-10.jpg differ diff --git a/fe/src/assets/images/fakers/profile-11.jpg b/fe/src/assets/images/fakers/profile-11.jpg new file mode 100755 index 0000000..345e72e Binary files /dev/null and b/fe/src/assets/images/fakers/profile-11.jpg differ diff --git a/fe/src/assets/images/fakers/profile-12.jpg b/fe/src/assets/images/fakers/profile-12.jpg new file mode 100755 index 0000000..0f1549e Binary files /dev/null and b/fe/src/assets/images/fakers/profile-12.jpg differ diff --git a/fe/src/assets/images/fakers/profile-13.jpg b/fe/src/assets/images/fakers/profile-13.jpg new file mode 100755 index 0000000..825cfc2 Binary files /dev/null and b/fe/src/assets/images/fakers/profile-13.jpg differ diff --git a/fe/src/assets/images/fakers/profile-14.jpg b/fe/src/assets/images/fakers/profile-14.jpg new file mode 100755 index 0000000..c398c6a Binary files /dev/null and b/fe/src/assets/images/fakers/profile-14.jpg differ diff --git a/fe/src/assets/images/fakers/profile-15.jpg b/fe/src/assets/images/fakers/profile-15.jpg new file mode 100755 index 0000000..0a8a04e Binary files /dev/null and b/fe/src/assets/images/fakers/profile-15.jpg differ diff --git a/fe/src/assets/images/fakers/profile-2.jpg b/fe/src/assets/images/fakers/profile-2.jpg new file mode 100755 index 0000000..262fdd3 Binary files /dev/null and b/fe/src/assets/images/fakers/profile-2.jpg differ diff --git a/fe/src/assets/images/fakers/profile-3.jpg b/fe/src/assets/images/fakers/profile-3.jpg new file mode 100755 index 0000000..bc46fce Binary files /dev/null and b/fe/src/assets/images/fakers/profile-3.jpg differ diff --git a/fe/src/assets/images/fakers/profile-4.jpg b/fe/src/assets/images/fakers/profile-4.jpg new file mode 100755 index 0000000..ffa5e24 Binary files /dev/null and b/fe/src/assets/images/fakers/profile-4.jpg differ diff --git a/fe/src/assets/images/fakers/profile-5.jpg b/fe/src/assets/images/fakers/profile-5.jpg new file mode 100755 index 0000000..953420d Binary files /dev/null and b/fe/src/assets/images/fakers/profile-5.jpg differ diff --git a/fe/src/assets/images/fakers/profile-6.jpg b/fe/src/assets/images/fakers/profile-6.jpg new file mode 100755 index 0000000..ebf71d9 Binary files /dev/null and b/fe/src/assets/images/fakers/profile-6.jpg differ diff --git a/fe/src/assets/images/fakers/profile-7.jpg b/fe/src/assets/images/fakers/profile-7.jpg new file mode 100755 index 0000000..b7f8cea Binary files /dev/null and b/fe/src/assets/images/fakers/profile-7.jpg differ diff --git a/fe/src/assets/images/fakers/profile-8.jpg b/fe/src/assets/images/fakers/profile-8.jpg new file mode 100755 index 0000000..6caf684 Binary files /dev/null and b/fe/src/assets/images/fakers/profile-8.jpg differ diff --git a/fe/src/assets/images/fakers/profile-9.jpg b/fe/src/assets/images/fakers/profile-9.jpg new file mode 100755 index 0000000..c5104d3 Binary files /dev/null and b/fe/src/assets/images/fakers/profile-9.jpg differ diff --git a/fe/src/assets/images/illustration.svg b/fe/src/assets/images/illustration.svg new file mode 100644 index 0000000..a0ae933 --- /dev/null +++ b/fe/src/assets/images/illustration.svg @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/src/assets/images/logo-kopkb.svg b/fe/src/assets/images/logo-kopkb.svg new file mode 100644 index 0000000..ef88ccb --- /dev/null +++ b/fe/src/assets/images/logo-kopkb.svg @@ -0,0 +1,418 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/src/assets/images/logo.svg b/fe/src/assets/images/logo.svg new file mode 100644 index 0000000..28c5228 --- /dev/null +++ b/fe/src/assets/images/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/fe/src/assets/images/noise.png b/fe/src/assets/images/noise.png new file mode 100644 index 0000000..37afd82 Binary files /dev/null and b/fe/src/assets/images/noise.png differ diff --git a/fe/src/assets/images/phone-illustration.svg b/fe/src/assets/images/phone-illustration.svg new file mode 100644 index 0000000..279c65a --- /dev/null +++ b/fe/src/assets/images/phone-illustration.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/fe/src/assets/images/placeholders/200x200.jpg b/fe/src/assets/images/placeholders/200x200.jpg new file mode 100755 index 0000000..4d40faa Binary files /dev/null and b/fe/src/assets/images/placeholders/200x200.jpg differ diff --git a/fe/src/assets/images/placeholders/800x800.jpg b/fe/src/assets/images/placeholders/800x800.jpg new file mode 100755 index 0000000..560c7f6 Binary files /dev/null and b/fe/src/assets/images/placeholders/800x800.jpg differ diff --git a/fe/src/assets/images/woman-illustration.svg b/fe/src/assets/images/woman-illustration.svg new file mode 100644 index 0000000..f023ddf --- /dev/null +++ b/fe/src/assets/images/woman-illustration.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fe/src/components/account-dropdown/AccountDropdown.vue b/fe/src/components/account-dropdown/AccountDropdown.vue new file mode 100644 index 0000000..859874c --- /dev/null +++ b/fe/src/components/account-dropdown/AccountDropdown.vue @@ -0,0 +1,122 @@ + + + diff --git a/fe/src/components/account-dropdown/AccountTrigger.vue b/fe/src/components/account-dropdown/AccountTrigger.vue new file mode 100644 index 0000000..c9532f2 --- /dev/null +++ b/fe/src/components/account-dropdown/AccountTrigger.vue @@ -0,0 +1,66 @@ + + + diff --git a/fe/src/components/account-dropdown/index.ts b/fe/src/components/account-dropdown/index.ts new file mode 100644 index 0000000..735218a --- /dev/null +++ b/fe/src/components/account-dropdown/index.ts @@ -0,0 +1,2 @@ +export { default as AccountDropdown } from './AccountDropdown.vue' +export { default as AccountTrigger } from './AccountTrigger.vue' diff --git a/fe/src/components/chart-presets/Bar1.vue b/fe/src/components/chart-presets/Bar1.vue new file mode 100644 index 0000000..90f11b3 --- /dev/null +++ b/fe/src/components/chart-presets/Bar1.vue @@ -0,0 +1,99 @@ + + + diff --git a/fe/src/components/chart-presets/Bar2.vue b/fe/src/components/chart-presets/Bar2.vue new file mode 100644 index 0000000..cc296da --- /dev/null +++ b/fe/src/components/chart-presets/Bar2.vue @@ -0,0 +1,85 @@ + + + diff --git a/fe/src/components/chart-presets/Donut1.vue b/fe/src/components/chart-presets/Donut1.vue new file mode 100644 index 0000000..02ac1ab --- /dev/null +++ b/fe/src/components/chart-presets/Donut1.vue @@ -0,0 +1,43 @@ + + + diff --git a/fe/src/components/chart-presets/Donut2.vue b/fe/src/components/chart-presets/Donut2.vue new file mode 100644 index 0000000..e883f56 --- /dev/null +++ b/fe/src/components/chart-presets/Donut2.vue @@ -0,0 +1,43 @@ + + + diff --git a/fe/src/components/chart-presets/Line1.vue b/fe/src/components/chart-presets/Line1.vue new file mode 100644 index 0000000..35956a6 --- /dev/null +++ b/fe/src/components/chart-presets/Line1.vue @@ -0,0 +1,92 @@ + + + diff --git a/fe/src/components/chart-presets/Line2.vue b/fe/src/components/chart-presets/Line2.vue new file mode 100644 index 0000000..5cdb4f1 --- /dev/null +++ b/fe/src/components/chart-presets/Line2.vue @@ -0,0 +1,72 @@ + + + diff --git a/fe/src/components/chart-presets/Line3.vue b/fe/src/components/chart-presets/Line3.vue new file mode 100644 index 0000000..36630d5 --- /dev/null +++ b/fe/src/components/chart-presets/Line3.vue @@ -0,0 +1,82 @@ + + + diff --git a/fe/src/components/chart-presets/Pie1.vue b/fe/src/components/chart-presets/Pie1.vue new file mode 100644 index 0000000..714f941 --- /dev/null +++ b/fe/src/components/chart-presets/Pie1.vue @@ -0,0 +1,42 @@ + + + diff --git a/fe/src/components/chart-presets/index.ts b/fe/src/components/chart-presets/index.ts new file mode 100644 index 0000000..63b68f3 --- /dev/null +++ b/fe/src/components/chart-presets/index.ts @@ -0,0 +1,8 @@ +export { default as Line1 } from './Line1.vue' //report-line-chart +export { default as Line2 } from './Line2.vue' //simple-line-chart-1 +export { default as Line3 } from './Line3.vue' //simple-line-chart-3, simple-line-chart-4 +export { default as Pie1 } from './Pie1.vue' //report-pie-chart +export { default as Donut1 } from './Donut1.vue' //report-donut-chart +export { default as Donut2 } from './Donut2.vue' //report-donut-chart-1 +export { default as Bar1 } from './Bar1.vue' //report-bar-chart +export { default as Bar2 } from './Bar2.vue' //report-bar-chart-1 diff --git a/fe/src/components/daily-notes/DailyNotes.vue b/fe/src/components/daily-notes/DailyNotes.vue new file mode 100644 index 0000000..0b5be3e --- /dev/null +++ b/fe/src/components/daily-notes/DailyNotes.vue @@ -0,0 +1,132 @@ + + + diff --git a/fe/src/components/daily-notes/index.ts b/fe/src/components/daily-notes/index.ts new file mode 100644 index 0000000..ec217b6 --- /dev/null +++ b/fe/src/components/daily-notes/index.ts @@ -0,0 +1 @@ +export { default as DailyNotes } from './DailyNotes.vue' diff --git a/fe/src/components/docs/ApiButton.vue b/fe/src/components/docs/ApiButton.vue new file mode 100644 index 0000000..7633727 --- /dev/null +++ b/fe/src/components/docs/ApiButton.vue @@ -0,0 +1,9 @@ + diff --git a/fe/src/components/docs/InstallPackage.vue b/fe/src/components/docs/InstallPackage.vue new file mode 100644 index 0000000..d680ead --- /dev/null +++ b/fe/src/components/docs/InstallPackage.vue @@ -0,0 +1,101 @@ + + + diff --git a/fe/src/components/docs/Menu.vue b/fe/src/components/docs/Menu.vue new file mode 100644 index 0000000..70870cf --- /dev/null +++ b/fe/src/components/docs/Menu.vue @@ -0,0 +1,8 @@ + diff --git a/fe/src/components/docs/Preview.vue b/fe/src/components/docs/Preview.vue new file mode 100644 index 0000000..4ab05a5 --- /dev/null +++ b/fe/src/components/docs/Preview.vue @@ -0,0 +1,42 @@ + + + diff --git a/fe/src/components/docs/PreviewCode.vue b/fe/src/components/docs/PreviewCode.vue new file mode 100644 index 0000000..ec3bd0d --- /dev/null +++ b/fe/src/components/docs/PreviewCode.vue @@ -0,0 +1,88 @@ + + + diff --git a/fe/src/components/docs/SectionContent.vue b/fe/src/components/docs/SectionContent.vue new file mode 100644 index 0000000..af1d956 --- /dev/null +++ b/fe/src/components/docs/SectionContent.vue @@ -0,0 +1,13 @@ + + + diff --git a/fe/src/components/docs/SectionTitle.vue b/fe/src/components/docs/SectionTitle.vue new file mode 100644 index 0000000..636a005 --- /dev/null +++ b/fe/src/components/docs/SectionTitle.vue @@ -0,0 +1,5 @@ + diff --git a/fe/src/components/docs/Subtitle.vue b/fe/src/components/docs/Subtitle.vue new file mode 100644 index 0000000..db126de --- /dev/null +++ b/fe/src/components/docs/Subtitle.vue @@ -0,0 +1,5 @@ + diff --git a/fe/src/components/docs/Title.vue b/fe/src/components/docs/Title.vue new file mode 100644 index 0000000..7c0a60b --- /dev/null +++ b/fe/src/components/docs/Title.vue @@ -0,0 +1,5 @@ + diff --git a/fe/src/components/docs/Wrapper.vue b/fe/src/components/docs/Wrapper.vue new file mode 100644 index 0000000..cc3f2ed --- /dev/null +++ b/fe/src/components/docs/Wrapper.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/docs/index.ts b/fe/src/components/docs/index.ts new file mode 100644 index 0000000..d3e7dfe --- /dev/null +++ b/fe/src/components/docs/index.ts @@ -0,0 +1,10 @@ +export { default as Wrapper } from "./Wrapper.vue"; +export { default as Menu } from "./Menu.vue"; +export { default as Title } from "./Title.vue"; +export { default as Subtitle } from "./Subtitle.vue"; +export { default as Preview } from "./Preview.vue"; +export { default as SectionTitle } from "./SectionTitle.vue"; +export { default as SectionContent } from "./SectionContent.vue"; +export { default as InstallPackage } from "./InstallPackage.vue"; +export { default as PreviewCode } from "./PreviewCode.vue"; +export { default as ApiButton } from "./ApiButton.vue"; diff --git a/fe/src/components/notification-dropdown/NotificationDropdown.vue b/fe/src/components/notification-dropdown/NotificationDropdown.vue new file mode 100644 index 0000000..da6e3d6 --- /dev/null +++ b/fe/src/components/notification-dropdown/NotificationDropdown.vue @@ -0,0 +1,55 @@ + + + diff --git a/fe/src/components/notification-dropdown/index.ts b/fe/src/components/notification-dropdown/index.ts new file mode 100644 index 0000000..17b5536 --- /dev/null +++ b/fe/src/components/notification-dropdown/index.ts @@ -0,0 +1 @@ +export { default as NotificationDropdown } from './NotificationDropdown.vue' diff --git a/fe/src/components/official-stores/OfficialStores.vue b/fe/src/components/official-stores/OfficialStores.vue new file mode 100644 index 0000000..d98bc95 --- /dev/null +++ b/fe/src/components/official-stores/OfficialStores.vue @@ -0,0 +1,151 @@ + + + diff --git a/fe/src/components/official-stores/index.ts b/fe/src/components/official-stores/index.ts new file mode 100644 index 0000000..91bb4d7 --- /dev/null +++ b/fe/src/components/official-stores/index.ts @@ -0,0 +1 @@ +export { default as OfficialStores } from './OfficialStores.vue' diff --git a/fe/src/components/quick-search-dialog/QuickSearchDialog.vue b/fe/src/components/quick-search-dialog/QuickSearchDialog.vue new file mode 100644 index 0000000..15e56bb --- /dev/null +++ b/fe/src/components/quick-search-dialog/QuickSearchDialog.vue @@ -0,0 +1,142 @@ + + + diff --git a/fe/src/components/quick-search-dialog/index.ts b/fe/src/components/quick-search-dialog/index.ts new file mode 100644 index 0000000..7a201c7 --- /dev/null +++ b/fe/src/components/quick-search-dialog/index.ts @@ -0,0 +1 @@ +export { default as QuickSearchDialog } from './QuickSearchDialog.vue' diff --git a/fe/src/components/recent-activities/RecentActivities.vue b/fe/src/components/recent-activities/RecentActivities.vue new file mode 100644 index 0000000..e68e89b --- /dev/null +++ b/fe/src/components/recent-activities/RecentActivities.vue @@ -0,0 +1,191 @@ + + + diff --git a/fe/src/components/recent-activities/index.ts b/fe/src/components/recent-activities/index.ts new file mode 100644 index 0000000..ba48953 --- /dev/null +++ b/fe/src/components/recent-activities/index.ts @@ -0,0 +1 @@ +export { default as RecentActivities } from './RecentActivities.vue' diff --git a/fe/src/components/schedules/Schedules.vue b/fe/src/components/schedules/Schedules.vue new file mode 100644 index 0000000..d015de6 --- /dev/null +++ b/fe/src/components/schedules/Schedules.vue @@ -0,0 +1,167 @@ + + + diff --git a/fe/src/components/schedules/index.ts b/fe/src/components/schedules/index.ts new file mode 100644 index 0000000..ceb8c37 --- /dev/null +++ b/fe/src/components/schedules/index.ts @@ -0,0 +1 @@ +export { default as Schedules } from './Schedules.vue' diff --git a/fe/src/components/side-menu/SideMenu.vue b/fe/src/components/side-menu/SideMenu.vue new file mode 100644 index 0000000..df4aed4 --- /dev/null +++ b/fe/src/components/side-menu/SideMenu.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/fe/src/components/side-menu/index.ts b/fe/src/components/side-menu/index.ts new file mode 100644 index 0000000..c249b31 --- /dev/null +++ b/fe/src/components/side-menu/index.ts @@ -0,0 +1 @@ +export { default as SideMenu } from './SideMenu.vue' diff --git a/fe/src/components/theme-switcher/ThemeSwitcher.vue b/fe/src/components/theme-switcher/ThemeSwitcher.vue new file mode 100644 index 0000000..aabd2e6 --- /dev/null +++ b/fe/src/components/theme-switcher/ThemeSwitcher.vue @@ -0,0 +1,131 @@ + + + diff --git a/fe/src/components/theme-switcher/index.ts b/fe/src/components/theme-switcher/index.ts new file mode 100644 index 0000000..8ad3e96 --- /dev/null +++ b/fe/src/components/theme-switcher/index.ts @@ -0,0 +1 @@ +export { default as ThemeSwitcher } from './ThemeSwitcher.vue' diff --git a/fe/src/components/top-menu/TopMenu.vue b/fe/src/components/top-menu/TopMenu.vue new file mode 100644 index 0000000..04a0172 --- /dev/null +++ b/fe/src/components/top-menu/TopMenu.vue @@ -0,0 +1,166 @@ + + + diff --git a/fe/src/components/top-menu/index.ts b/fe/src/components/top-menu/index.ts new file mode 100644 index 0000000..d4a4b17 --- /dev/null +++ b/fe/src/components/top-menu/index.ts @@ -0,0 +1,3 @@ +import TopMenu from "./TopMenu.vue"; + +export { TopMenu }; diff --git a/fe/src/components/transactions/Transactions.vue b/fe/src/components/transactions/Transactions.vue new file mode 100644 index 0000000..c0957b0 --- /dev/null +++ b/fe/src/components/transactions/Transactions.vue @@ -0,0 +1,109 @@ + + + diff --git a/fe/src/components/transactions/index.ts b/fe/src/components/transactions/index.ts new file mode 100644 index 0000000..1ec6838 --- /dev/null +++ b/fe/src/components/transactions/index.ts @@ -0,0 +1 @@ +export { default as Transactions } from './Transactions.vue' diff --git a/fe/src/components/ui/accordion/AccordionContent.vue b/fe/src/components/ui/accordion/AccordionContent.vue new file mode 100644 index 0000000..40a71af --- /dev/null +++ b/fe/src/components/ui/accordion/AccordionContent.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/accordion/AccordionItem.vue b/fe/src/components/ui/accordion/AccordionItem.vue new file mode 100644 index 0000000..f5759fa --- /dev/null +++ b/fe/src/components/ui/accordion/AccordionItem.vue @@ -0,0 +1,39 @@ + + + diff --git a/fe/src/components/ui/accordion/AccordionRoot.vue b/fe/src/components/ui/accordion/AccordionRoot.vue new file mode 100644 index 0000000..8bf948e --- /dev/null +++ b/fe/src/components/ui/accordion/AccordionRoot.vue @@ -0,0 +1,44 @@ + + + diff --git a/fe/src/components/ui/accordion/AccordionTrigger.vue b/fe/src/components/ui/accordion/AccordionTrigger.vue new file mode 100644 index 0000000..46d7292 --- /dev/null +++ b/fe/src/components/ui/accordion/AccordionTrigger.vue @@ -0,0 +1,35 @@ + + + diff --git a/fe/src/components/ui/accordion/index.ts b/fe/src/components/ui/accordion/index.ts new file mode 100644 index 0000000..85d9eb2 --- /dev/null +++ b/fe/src/components/ui/accordion/index.ts @@ -0,0 +1,4 @@ +export { default as AccordionRoot } from "./AccordionRoot.vue"; +export { default as AccordionItem } from "./AccordionItem.vue"; +export { default as AccordionTrigger } from "./AccordionTrigger.vue"; +export { default as AccordionContent } from "./AccordionContent.vue"; diff --git a/fe/src/components/ui/alert/AlertCloseTrigger.vue b/fe/src/components/ui/alert/AlertCloseTrigger.vue new file mode 100644 index 0000000..7f9ef68 --- /dev/null +++ b/fe/src/components/ui/alert/AlertCloseTrigger.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/alert/AlertDescription.vue b/fe/src/components/ui/alert/AlertDescription.vue new file mode 100644 index 0000000..e84cdcc --- /dev/null +++ b/fe/src/components/ui/alert/AlertDescription.vue @@ -0,0 +1,23 @@ + + + diff --git a/fe/src/components/ui/alert/AlertRoot.vue b/fe/src/components/ui/alert/AlertRoot.vue new file mode 100644 index 0000000..86ca297 --- /dev/null +++ b/fe/src/components/ui/alert/AlertRoot.vue @@ -0,0 +1,36 @@ + + + diff --git a/fe/src/components/ui/alert/AlertTitle.vue b/fe/src/components/ui/alert/AlertTitle.vue new file mode 100644 index 0000000..3891b27 --- /dev/null +++ b/fe/src/components/ui/alert/AlertTitle.vue @@ -0,0 +1,23 @@ + + + diff --git a/fe/src/components/ui/alert/index.ts b/fe/src/components/ui/alert/index.ts new file mode 100644 index 0000000..d1619d8 --- /dev/null +++ b/fe/src/components/ui/alert/index.ts @@ -0,0 +1,4 @@ +export { default as AlertRoot } from "./AlertRoot.vue"; +export { default as AlertTitle } from "./AlertTitle.vue"; +export { default as AlertDescription } from "./AlertDescription.vue"; +export { default as AlertCloseTrigger } from "./AlertCloseTrigger.vue"; diff --git a/fe/src/components/ui/avatar/AvatarFallback.vue b/fe/src/components/ui/avatar/AvatarFallback.vue new file mode 100644 index 0000000..92fe74b --- /dev/null +++ b/fe/src/components/ui/avatar/AvatarFallback.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/avatar/AvatarImage.vue b/fe/src/components/ui/avatar/AvatarImage.vue new file mode 100644 index 0000000..6de2fc0 --- /dev/null +++ b/fe/src/components/ui/avatar/AvatarImage.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/avatar/AvatarRoot.vue b/fe/src/components/ui/avatar/AvatarRoot.vue new file mode 100644 index 0000000..1a38d5d --- /dev/null +++ b/fe/src/components/ui/avatar/AvatarRoot.vue @@ -0,0 +1,50 @@ + + + diff --git a/fe/src/components/ui/avatar/index.ts b/fe/src/components/ui/avatar/index.ts new file mode 100644 index 0000000..ece62de --- /dev/null +++ b/fe/src/components/ui/avatar/index.ts @@ -0,0 +1,3 @@ +export { default as AvatarRoot } from "./AvatarRoot.vue"; +export { default as AvatarFallback } from "./AvatarFallback.vue"; +export { default as AvatarImage } from "./AvatarImage.vue"; diff --git a/fe/src/components/ui/badge/Badge.vue b/fe/src/components/ui/badge/Badge.vue new file mode 100644 index 0000000..d0d5c8d --- /dev/null +++ b/fe/src/components/ui/badge/Badge.vue @@ -0,0 +1,39 @@ + + + diff --git a/fe/src/components/ui/badge/index.ts b/fe/src/components/ui/badge/index.ts new file mode 100644 index 0000000..0934d6b --- /dev/null +++ b/fe/src/components/ui/badge/index.ts @@ -0,0 +1 @@ +export { default as Badge } from "./Badge.vue"; diff --git a/fe/src/components/ui/box/box.vue b/fe/src/components/ui/box/box.vue new file mode 100644 index 0000000..d076bb0 --- /dev/null +++ b/fe/src/components/ui/box/box.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/box/index.ts b/fe/src/components/ui/box/index.ts new file mode 100644 index 0000000..99af555 --- /dev/null +++ b/fe/src/components/ui/box/index.ts @@ -0,0 +1 @@ +export { default as Box } from "./Box.vue"; diff --git a/fe/src/components/ui/breadcrumb/Breadcrumb.vue b/fe/src/components/ui/breadcrumb/Breadcrumb.vue new file mode 100644 index 0000000..aed4e6f --- /dev/null +++ b/fe/src/components/ui/breadcrumb/Breadcrumb.vue @@ -0,0 +1,61 @@ + + + diff --git a/fe/src/components/ui/breadcrumb/BreadcrumbItem.vue b/fe/src/components/ui/breadcrumb/BreadcrumbItem.vue new file mode 100644 index 0000000..54408ee --- /dev/null +++ b/fe/src/components/ui/breadcrumb/BreadcrumbItem.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/breadcrumb/BreadcrumbLink.vue b/fe/src/components/ui/breadcrumb/BreadcrumbLink.vue new file mode 100644 index 0000000..da9e41b --- /dev/null +++ b/fe/src/components/ui/breadcrumb/BreadcrumbLink.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/breadcrumb/BreadcrumbList.vue b/fe/src/components/ui/breadcrumb/BreadcrumbList.vue new file mode 100644 index 0000000..a8e1611 --- /dev/null +++ b/fe/src/components/ui/breadcrumb/BreadcrumbList.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/breadcrumb/index.ts b/fe/src/components/ui/breadcrumb/index.ts new file mode 100644 index 0000000..34d8639 --- /dev/null +++ b/fe/src/components/ui/breadcrumb/index.ts @@ -0,0 +1,4 @@ +export { default as Breadcrumb } from "./Breadcrumb.vue"; +export { default as BreadcrumbItem } from "./BreadcrumbItem.vue"; +export { default as BreadcrumbLink } from "./BreadcrumbLink.vue"; +export { default as BreadcrumbList } from "./BreadcrumbList.vue"; diff --git a/fe/src/components/ui/button/Button.vue b/fe/src/components/ui/button/Button.vue new file mode 100644 index 0000000..b5a06b5 --- /dev/null +++ b/fe/src/components/ui/button/Button.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/button/index.ts b/fe/src/components/ui/button/index.ts new file mode 100644 index 0000000..28c00c1 --- /dev/null +++ b/fe/src/components/ui/button/index.ts @@ -0,0 +1 @@ +export { default as Button } from "./Button.vue"; diff --git a/fe/src/components/ui/carousel/CarouselControl.vue b/fe/src/components/ui/carousel/CarouselControl.vue new file mode 100644 index 0000000..edd610c --- /dev/null +++ b/fe/src/components/ui/carousel/CarouselControl.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/carousel/CarouselIndicator.vue b/fe/src/components/ui/carousel/CarouselIndicator.vue new file mode 100644 index 0000000..9d798d4 --- /dev/null +++ b/fe/src/components/ui/carousel/CarouselIndicator.vue @@ -0,0 +1,25 @@ + + + diff --git a/fe/src/components/ui/carousel/CarouselIndicatorGroup.vue b/fe/src/components/ui/carousel/CarouselIndicatorGroup.vue new file mode 100644 index 0000000..32f2dfe --- /dev/null +++ b/fe/src/components/ui/carousel/CarouselIndicatorGroup.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/carousel/CarouselItem.vue b/fe/src/components/ui/carousel/CarouselItem.vue new file mode 100644 index 0000000..2c1ee75 --- /dev/null +++ b/fe/src/components/ui/carousel/CarouselItem.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/carousel/CarouselItemGroup.vue b/fe/src/components/ui/carousel/CarouselItemGroup.vue new file mode 100644 index 0000000..07a3626 --- /dev/null +++ b/fe/src/components/ui/carousel/CarouselItemGroup.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/carousel/CarouselNextTrigger.vue b/fe/src/components/ui/carousel/CarouselNextTrigger.vue new file mode 100644 index 0000000..161e92a --- /dev/null +++ b/fe/src/components/ui/carousel/CarouselNextTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/carousel/CarouselPrevTrigger.vue b/fe/src/components/ui/carousel/CarouselPrevTrigger.vue new file mode 100644 index 0000000..937d2a0 --- /dev/null +++ b/fe/src/components/ui/carousel/CarouselPrevTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/carousel/CarouselRoot.vue b/fe/src/components/ui/carousel/CarouselRoot.vue new file mode 100644 index 0000000..e29f7b6 --- /dev/null +++ b/fe/src/components/ui/carousel/CarouselRoot.vue @@ -0,0 +1,41 @@ + + + diff --git a/fe/src/components/ui/carousel/index.ts b/fe/src/components/ui/carousel/index.ts new file mode 100644 index 0000000..49898ba --- /dev/null +++ b/fe/src/components/ui/carousel/index.ts @@ -0,0 +1,8 @@ +export { default as CarouselRoot } from "./CarouselRoot.vue"; +export { default as CarouselControl } from "./CarouselControl.vue"; +export { default as CarouselPrevTrigger } from "./CarouselPrevTrigger.vue"; +export { default as CarouselNextTrigger } from "./CarouselNextTrigger.vue"; +export { default as CarouselIndicatorGroup } from "./CarouselIndicatorGroup.vue"; +export { default as CarouselIndicator } from "./CarouselIndicator.vue"; +export { default as CarouselItemGroup } from "./CarouselItemGroup.vue"; +export { default as CarouselItem } from "./CarouselItem.vue"; diff --git a/fe/src/components/ui/chart/Chart.vue b/fe/src/components/ui/chart/Chart.vue new file mode 100644 index 0000000..068f0b2 --- /dev/null +++ b/fe/src/components/ui/chart/Chart.vue @@ -0,0 +1,36 @@ + + + diff --git a/fe/src/components/ui/chart/index.ts b/fe/src/components/ui/chart/index.ts new file mode 100644 index 0000000..aae8060 --- /dev/null +++ b/fe/src/components/ui/chart/index.ts @@ -0,0 +1,2 @@ +export { default as Chart } from "./Chart.vue"; +export { getColor } from "./utils.ts"; diff --git a/fe/src/components/ui/chart/utils.ts b/fe/src/components/ui/chart/utils.ts new file mode 100644 index 0000000..8c063fa --- /dev/null +++ b/fe/src/components/ui/chart/utils.ts @@ -0,0 +1,11 @@ +export function getColor(name: string, opacity = 1) { + const color = getComputedStyle(document.documentElement) + .getPropertyValue(name) + .trim(); + if (opacity < 1) { + return `color-mix(in oklch, ${color} ${opacity * 100}%, transparent ${ + 100 - opacity * 100 + }%)`; + } + return color; +} diff --git a/fe/src/components/ui/checkbox/CheckboxControl.vue b/fe/src/components/ui/checkbox/CheckboxControl.vue new file mode 100644 index 0000000..07ca539 --- /dev/null +++ b/fe/src/components/ui/checkbox/CheckboxControl.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/checkbox/CheckboxHiddenInput.vue b/fe/src/components/ui/checkbox/CheckboxHiddenInput.vue new file mode 100644 index 0000000..eb70fd8 --- /dev/null +++ b/fe/src/components/ui/checkbox/CheckboxHiddenInput.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/checkbox/CheckboxIndicator.vue b/fe/src/components/ui/checkbox/CheckboxIndicator.vue new file mode 100644 index 0000000..887376c --- /dev/null +++ b/fe/src/components/ui/checkbox/CheckboxIndicator.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/checkbox/CheckboxLabel.vue b/fe/src/components/ui/checkbox/CheckboxLabel.vue new file mode 100644 index 0000000..852425f --- /dev/null +++ b/fe/src/components/ui/checkbox/CheckboxLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/checkbox/CheckboxRoot.vue b/fe/src/components/ui/checkbox/CheckboxRoot.vue new file mode 100644 index 0000000..ec63e53 --- /dev/null +++ b/fe/src/components/ui/checkbox/CheckboxRoot.vue @@ -0,0 +1,35 @@ + + + diff --git a/fe/src/components/ui/checkbox/index.ts b/fe/src/components/ui/checkbox/index.ts new file mode 100644 index 0000000..10741ed --- /dev/null +++ b/fe/src/components/ui/checkbox/index.ts @@ -0,0 +1,5 @@ +export { default as CheckboxRoot } from "./CheckboxRoot.vue"; +export { default as CheckboxLabel } from "./CheckboxLabel.vue"; +export { default as CheckboxControl } from "./CheckboxControl.vue"; +export { default as CheckboxIndicator } from "./CheckboxIndicator.vue"; +export { default as CheckboxHiddenInput } from "./CheckboxHiddenInput.vue"; diff --git a/fe/src/components/ui/combobox/ComboboxClearTrigger.vue b/fe/src/components/ui/combobox/ComboboxClearTrigger.vue new file mode 100644 index 0000000..1168b42 --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxClearTrigger.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxContent.vue b/fe/src/components/ui/combobox/ComboboxContent.vue new file mode 100644 index 0000000..836f6b2 --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxContent.vue @@ -0,0 +1,35 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxControl.vue b/fe/src/components/ui/combobox/ComboboxControl.vue new file mode 100644 index 0000000..67d8883 --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxControl.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxInput.vue b/fe/src/components/ui/combobox/ComboboxInput.vue new file mode 100644 index 0000000..e05a4d0 --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxInput.vue @@ -0,0 +1,17 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxItem.vue b/fe/src/components/ui/combobox/ComboboxItem.vue new file mode 100644 index 0000000..7fd022b --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxItem.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxItemGroup.vue b/fe/src/components/ui/combobox/ComboboxItemGroup.vue new file mode 100644 index 0000000..dba8bce --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxItemGroup.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxItemGroupLabel.vue b/fe/src/components/ui/combobox/ComboboxItemGroupLabel.vue new file mode 100644 index 0000000..15b56cf --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxItemGroupLabel.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxItemIndicator.vue b/fe/src/components/ui/combobox/ComboboxItemIndicator.vue new file mode 100644 index 0000000..9691fc3 --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxItemIndicator.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxItemText.vue b/fe/src/components/ui/combobox/ComboboxItemText.vue new file mode 100644 index 0000000..bb949ef --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxItemText.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxLabel.vue b/fe/src/components/ui/combobox/ComboboxLabel.vue new file mode 100644 index 0000000..4afa7a5 --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxPositioner.vue b/fe/src/components/ui/combobox/ComboboxPositioner.vue new file mode 100644 index 0000000..c61539c --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxPositioner.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxRoot.vue b/fe/src/components/ui/combobox/ComboboxRoot.vue new file mode 100644 index 0000000..b0b8fb3 --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxRoot.vue @@ -0,0 +1,47 @@ + + + diff --git a/fe/src/components/ui/combobox/ComboboxTrigger.vue b/fe/src/components/ui/combobox/ComboboxTrigger.vue new file mode 100644 index 0000000..9a729c7 --- /dev/null +++ b/fe/src/components/ui/combobox/ComboboxTrigger.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/combobox/index.ts b/fe/src/components/ui/combobox/index.ts new file mode 100644 index 0000000..ad0e146 --- /dev/null +++ b/fe/src/components/ui/combobox/index.ts @@ -0,0 +1,13 @@ +export { default as ComboboxRoot } from "./ComboboxRoot.vue"; +export { default as ComboboxLabel } from "./ComboboxLabel.vue"; +export { default as ComboboxControl } from "./ComboboxControl.vue"; +export { default as ComboboxInput } from "./ComboboxInput.vue"; +export { default as ComboboxTrigger } from "./ComboboxTrigger.vue"; +export { default as ComboboxClearTrigger } from "./ComboboxClearTrigger.vue"; +export { default as ComboboxPositioner } from "./ComboboxPositioner.vue"; +export { default as ComboboxContent } from "./ComboboxContent.vue"; +export { default as ComboboxItemGroup } from "./ComboboxItemGroup.vue"; +export { default as ComboboxItemGroupLabel } from "./ComboboxItemGroupLabel.vue"; +export { default as ComboboxItem } from "./ComboboxItem.vue"; +export { default as ComboboxItemText } from "./ComboboxItemText.vue"; +export { default as ComboboxItemIndicator } from "./ComboboxItemIndicator.vue"; diff --git a/fe/src/components/ui/datepicker/DatePickerClearTrigger.vue b/fe/src/components/ui/datepicker/DatePickerClearTrigger.vue new file mode 100644 index 0000000..dbf8c39 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerClearTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerContent.vue b/fe/src/components/ui/datepicker/DatePickerContent.vue new file mode 100644 index 0000000..0ffba93 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerContent.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerContext.vue b/fe/src/components/ui/datepicker/DatePickerContext.vue new file mode 100644 index 0000000..a2d113d --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerContext.vue @@ -0,0 +1,10 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerControl.vue b/fe/src/components/ui/datepicker/DatePickerControl.vue new file mode 100644 index 0000000..ae17782 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerControl.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerInput.vue b/fe/src/components/ui/datepicker/DatePickerInput.vue new file mode 100644 index 0000000..d560f73 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerInput.vue @@ -0,0 +1,26 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerLabel.vue b/fe/src/components/ui/datepicker/DatePickerLabel.vue new file mode 100644 index 0000000..a04f4dc --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerMonthSelect.vue b/fe/src/components/ui/datepicker/DatePickerMonthSelect.vue new file mode 100644 index 0000000..1b1ffb3 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerMonthSelect.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerNextTrigger.vue b/fe/src/components/ui/datepicker/DatePickerNextTrigger.vue new file mode 100644 index 0000000..8f758fc --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerNextTrigger.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerPositioner.vue b/fe/src/components/ui/datepicker/DatePickerPositioner.vue new file mode 100644 index 0000000..f166e69 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerPositioner.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerPresetTrigger.vue b/fe/src/components/ui/datepicker/DatePickerPresetTrigger.vue new file mode 100644 index 0000000..e9e5f95 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerPresetTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerPrevTrigger.vue b/fe/src/components/ui/datepicker/DatePickerPrevTrigger.vue new file mode 100644 index 0000000..299fe73 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerPrevTrigger.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerRangeText.vue b/fe/src/components/ui/datepicker/DatePickerRangeText.vue new file mode 100644 index 0000000..9cf24e8 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerRangeText.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerRoot.vue b/fe/src/components/ui/datepicker/DatePickerRoot.vue new file mode 100644 index 0000000..21f0caf --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerRoot.vue @@ -0,0 +1,40 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerTable.vue b/fe/src/components/ui/datepicker/DatePickerTable.vue new file mode 100644 index 0000000..fe7cb65 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerTable.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerTableBody.vue b/fe/src/components/ui/datepicker/DatePickerTableBody.vue new file mode 100644 index 0000000..b23b3cd --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerTableBody.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerTableCell.vue b/fe/src/components/ui/datepicker/DatePickerTableCell.vue new file mode 100644 index 0000000..2ac4d20 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerTableCell.vue @@ -0,0 +1,45 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerTableCellTrigger.vue b/fe/src/components/ui/datepicker/DatePickerTableCellTrigger.vue new file mode 100644 index 0000000..a0a2887 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerTableCellTrigger.vue @@ -0,0 +1,44 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerTableHead.vue b/fe/src/components/ui/datepicker/DatePickerTableHead.vue new file mode 100644 index 0000000..3536df3 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerTableHead.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerTableHeader.vue b/fe/src/components/ui/datepicker/DatePickerTableHeader.vue new file mode 100644 index 0000000..d98f7c6 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerTableHeader.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerTableRow.vue b/fe/src/components/ui/datepicker/DatePickerTableRow.vue new file mode 100644 index 0000000..cf77514 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerTableRow.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerTrigger.vue b/fe/src/components/ui/datepicker/DatePickerTrigger.vue new file mode 100644 index 0000000..cb3fda1 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerView.vue b/fe/src/components/ui/datepicker/DatePickerView.vue new file mode 100644 index 0000000..9b8c829 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerView.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerViewControl.vue b/fe/src/components/ui/datepicker/DatePickerViewControl.vue new file mode 100644 index 0000000..02fd0c0 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerViewControl.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerViewTrigger.vue b/fe/src/components/ui/datepicker/DatePickerViewTrigger.vue new file mode 100644 index 0000000..5e99f62 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerViewTrigger.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/datepicker/DatePickerYearSelect.vue b/fe/src/components/ui/datepicker/DatePickerYearSelect.vue new file mode 100644 index 0000000..ab182c7 --- /dev/null +++ b/fe/src/components/ui/datepicker/DatePickerYearSelect.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/datepicker/index.ts b/fe/src/components/ui/datepicker/index.ts new file mode 100644 index 0000000..38d8257 --- /dev/null +++ b/fe/src/components/ui/datepicker/index.ts @@ -0,0 +1,25 @@ +export { default as DatePickerRoot } from "./DatePickerRoot.vue"; +export { default as DatePickerLabel } from "./DatePickerLabel.vue"; +export { default as DatePickerControl } from "./DatePickerControl.vue"; +export { default as DatePickerInput } from "./DatePickerInput.vue"; +export { default as DatePickerTrigger } from "./DatePickerTrigger.vue"; +export { default as DatePickerClearTrigger } from "./DatePickerClearTrigger.vue"; +export { default as DatePickerPositioner } from "./DatePickerPositioner.vue"; +export { default as DatePickerContent } from "./DatePickerContent.vue"; +export { default as DatePickerYearSelect } from "./DatePickerYearSelect.vue"; +export { default as DatePickerMonthSelect } from "./DatePickerMonthSelect.vue"; +export { default as DatePickerView } from "./DatePickerView.vue"; +export { default as DatePickerViewControl } from "./DatePickerViewControl.vue"; +export { default as DatePickerPresetTrigger } from "./DatePickerPresetTrigger.vue"; +export { default as DatePickerPrevTrigger } from "./DatePickerPrevTrigger.vue"; +export { default as DatePickerViewTrigger } from "./DatePickerViewTrigger.vue"; +export { default as DatePickerNextTrigger } from "./DatePickerNextTrigger.vue"; +export { default as DatePickerRangeText } from "./DatePickerRangeText.vue"; +export { default as DatePickerTable } from "./DatePickerTable.vue"; +export { default as DatePickerTableHead } from "./DatePickerTableHead.vue"; +export { default as DatePickerTableRow } from "./DatePickerTableRow.vue"; +export { default as DatePickerTableHeader } from "./DatePickerTableHeader.vue"; +export { default as DatePickerTableBody } from "./DatePickerTableBody.vue"; +export { default as DatePickerTableCell } from "./DatePickerTableCell.vue"; +export { default as DatePickerTableCellTrigger } from "./DatePickerTableCellTrigger.vue"; +export { default as DatePickerContext } from "./DatePickerContext.vue"; diff --git a/fe/src/components/ui/dialog/DialogBackdrop.vue b/fe/src/components/ui/dialog/DialogBackdrop.vue new file mode 100644 index 0000000..2dce0b1 --- /dev/null +++ b/fe/src/components/ui/dialog/DialogBackdrop.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/dialog/DialogCloseTrigger.vue b/fe/src/components/ui/dialog/DialogCloseTrigger.vue new file mode 100644 index 0000000..25f221a --- /dev/null +++ b/fe/src/components/ui/dialog/DialogCloseTrigger.vue @@ -0,0 +1,44 @@ + + + diff --git a/fe/src/components/ui/dialog/DialogContent.vue b/fe/src/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..f004126 --- /dev/null +++ b/fe/src/components/ui/dialog/DialogContent.vue @@ -0,0 +1,38 @@ + + + diff --git a/fe/src/components/ui/dialog/DialogDescription.vue b/fe/src/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..0bc61dc --- /dev/null +++ b/fe/src/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/dialog/DialogPositioner.vue b/fe/src/components/ui/dialog/DialogPositioner.vue new file mode 100644 index 0000000..945bd42 --- /dev/null +++ b/fe/src/components/ui/dialog/DialogPositioner.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/dialog/DialogRoot.vue b/fe/src/components/ui/dialog/DialogRoot.vue new file mode 100644 index 0000000..d97f151 --- /dev/null +++ b/fe/src/components/ui/dialog/DialogRoot.vue @@ -0,0 +1,36 @@ + + + diff --git a/fe/src/components/ui/dialog/DialogTitle.vue b/fe/src/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..5436553 --- /dev/null +++ b/fe/src/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/dialog/DialogTrigger.vue b/fe/src/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..29b3169 --- /dev/null +++ b/fe/src/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/dialog/index.ts b/fe/src/components/ui/dialog/index.ts new file mode 100644 index 0000000..2599b5b --- /dev/null +++ b/fe/src/components/ui/dialog/index.ts @@ -0,0 +1,8 @@ +export { default as DialogRoot } from "./DialogRoot.vue"; +export { default as DialogTrigger } from "./DialogTrigger.vue"; +export { default as DialogBackdrop } from "./DialogBackdrop.vue"; +export { default as DialogPositioner } from "./DialogPositioner.vue"; +export { default as DialogContent } from "./DialogContent.vue"; +export { default as DialogTitle } from "./DialogTitle.vue"; +export { default as DialogDescription } from "./DialogDescription.vue"; +export { default as DialogCloseTrigger } from "./DialogCloseTrigger.vue"; diff --git a/fe/src/components/ui/field/Field.vue b/fe/src/components/ui/field/Field.vue new file mode 100644 index 0000000..d74ec9b --- /dev/null +++ b/fe/src/components/ui/field/Field.vue @@ -0,0 +1,24 @@ + + + diff --git a/fe/src/components/ui/field/FieldContent.vue b/fe/src/components/ui/field/FieldContent.vue new file mode 100644 index 0000000..a51efeb --- /dev/null +++ b/fe/src/components/ui/field/FieldContent.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/field/FieldDescription.vue b/fe/src/components/ui/field/FieldDescription.vue new file mode 100644 index 0000000..bdb0934 --- /dev/null +++ b/fe/src/components/ui/field/FieldDescription.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/field/FieldError.vue b/fe/src/components/ui/field/FieldError.vue new file mode 100644 index 0000000..db0f4d4 --- /dev/null +++ b/fe/src/components/ui/field/FieldError.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/field/FieldGroup.vue b/fe/src/components/ui/field/FieldGroup.vue new file mode 100644 index 0000000..a03cd06 --- /dev/null +++ b/fe/src/components/ui/field/FieldGroup.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/field/FieldLabel.vue b/fe/src/components/ui/field/FieldLabel.vue new file mode 100644 index 0000000..3ae5b56 --- /dev/null +++ b/fe/src/components/ui/field/FieldLabel.vue @@ -0,0 +1,15 @@ + + + diff --git a/fe/src/components/ui/field/FieldLegend.vue b/fe/src/components/ui/field/FieldLegend.vue new file mode 100644 index 0000000..52cc0b9 --- /dev/null +++ b/fe/src/components/ui/field/FieldLegend.vue @@ -0,0 +1,19 @@ + + + diff --git a/fe/src/components/ui/field/FieldSeparator.vue b/fe/src/components/ui/field/FieldSeparator.vue new file mode 100644 index 0000000..8c0ceae --- /dev/null +++ b/fe/src/components/ui/field/FieldSeparator.vue @@ -0,0 +1,19 @@ + + + diff --git a/fe/src/components/ui/field/FieldSet.vue b/fe/src/components/ui/field/FieldSet.vue new file mode 100644 index 0000000..07d94be --- /dev/null +++ b/fe/src/components/ui/field/FieldSet.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/field/FieldTitle.vue b/fe/src/components/ui/field/FieldTitle.vue new file mode 100644 index 0000000..934cc95 --- /dev/null +++ b/fe/src/components/ui/field/FieldTitle.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/field/index.ts b/fe/src/components/ui/field/index.ts new file mode 100644 index 0000000..909f2d6 --- /dev/null +++ b/fe/src/components/ui/field/index.ts @@ -0,0 +1,10 @@ +export { default as Field } from "./Field.vue"; +export { default as FieldContent } from "./FieldContent.vue"; +export { default as FieldDescription } from "./FieldDescription.vue"; +export { default as FieldError } from "./FieldError.vue"; +export { default as FieldGroup } from "./FieldGroup.vue"; +export { default as FieldLabel } from "./FieldLabel.vue"; +export { default as FieldLegend } from "./FieldLegend.vue"; +export { default as FieldSeparator } from "./FieldSeparator.vue"; +export { default as FieldSet } from "./FieldSet.vue"; +export { default as FieldTitle } from "./FieldTitle.vue"; diff --git a/fe/src/components/ui/file-icon/FileIcon.vue b/fe/src/components/ui/file-icon/FileIcon.vue new file mode 100644 index 0000000..6b14b59 --- /dev/null +++ b/fe/src/components/ui/file-icon/FileIcon.vue @@ -0,0 +1,37 @@ + + + diff --git a/fe/src/components/ui/file-icon/index.ts b/fe/src/components/ui/file-icon/index.ts new file mode 100644 index 0000000..98b6760 --- /dev/null +++ b/fe/src/components/ui/file-icon/index.ts @@ -0,0 +1 @@ +export { default as FileIcon } from "./FileIcon.vue"; diff --git a/fe/src/components/ui/frame/Frame.vue b/fe/src/components/ui/frame/Frame.vue new file mode 100644 index 0000000..662e944 --- /dev/null +++ b/fe/src/components/ui/frame/Frame.vue @@ -0,0 +1,53 @@ + + + diff --git a/fe/src/components/ui/frame/index.ts b/fe/src/components/ui/frame/index.ts new file mode 100644 index 0000000..414d1e6 --- /dev/null +++ b/fe/src/components/ui/frame/index.ts @@ -0,0 +1 @@ +export { default as Frame } from "./Frame.vue"; diff --git a/fe/src/components/ui/input/Input.vue b/fe/src/components/ui/input/Input.vue new file mode 100644 index 0000000..b8a8ca3 --- /dev/null +++ b/fe/src/components/ui/input/Input.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/input/index.ts b/fe/src/components/ui/input/index.ts new file mode 100644 index 0000000..110f046 --- /dev/null +++ b/fe/src/components/ui/input/index.ts @@ -0,0 +1 @@ +export { default as Input } from "./Input.vue"; diff --git a/fe/src/components/ui/label/Label.vue b/fe/src/components/ui/label/Label.vue new file mode 100644 index 0000000..4c2138d --- /dev/null +++ b/fe/src/components/ui/label/Label.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/label/index.ts b/fe/src/components/ui/label/index.ts new file mode 100644 index 0000000..38eaa35 --- /dev/null +++ b/fe/src/components/ui/label/index.ts @@ -0,0 +1 @@ +export { default as Label } from "./Label.vue"; diff --git a/fe/src/components/ui/lucide/Lucide.vue b/fe/src/components/ui/lucide/Lucide.vue new file mode 100644 index 0000000..1aa3974 --- /dev/null +++ b/fe/src/components/ui/lucide/Lucide.vue @@ -0,0 +1,22 @@ + + + diff --git a/fe/src/components/ui/lucide/index.ts b/fe/src/components/ui/lucide/index.ts new file mode 100644 index 0000000..a2d4653 --- /dev/null +++ b/fe/src/components/ui/lucide/index.ts @@ -0,0 +1 @@ +export { default as Lucide, type Icon } from './Lucide.vue' diff --git a/fe/src/components/ui/map/Map.vue b/fe/src/components/ui/map/Map.vue new file mode 100644 index 0000000..6ea0da3 --- /dev/null +++ b/fe/src/components/ui/map/Map.vue @@ -0,0 +1,224 @@ + + + diff --git a/fe/src/components/ui/map/index.ts b/fe/src/components/ui/map/index.ts new file mode 100644 index 0000000..8aefb13 --- /dev/null +++ b/fe/src/components/ui/map/index.ts @@ -0,0 +1 @@ +export { default as Map } from "./Map.vue"; diff --git a/fe/src/components/ui/menu/MenuCheckboxItem.vue b/fe/src/components/ui/menu/MenuCheckboxItem.vue new file mode 100644 index 0000000..9598658 --- /dev/null +++ b/fe/src/components/ui/menu/MenuCheckboxItem.vue @@ -0,0 +1,41 @@ + + + diff --git a/fe/src/components/ui/menu/MenuContent.vue b/fe/src/components/ui/menu/MenuContent.vue new file mode 100644 index 0000000..2b04673 --- /dev/null +++ b/fe/src/components/ui/menu/MenuContent.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/menu/MenuIndicator.vue b/fe/src/components/ui/menu/MenuIndicator.vue new file mode 100644 index 0000000..d4874e4 --- /dev/null +++ b/fe/src/components/ui/menu/MenuIndicator.vue @@ -0,0 +1,21 @@ + + + diff --git a/fe/src/components/ui/menu/MenuItem.vue b/fe/src/components/ui/menu/MenuItem.vue new file mode 100644 index 0000000..bbdbcf1 --- /dev/null +++ b/fe/src/components/ui/menu/MenuItem.vue @@ -0,0 +1,34 @@ + + + diff --git a/fe/src/components/ui/menu/MenuItemGroupLabel.vue b/fe/src/components/ui/menu/MenuItemGroupLabel.vue new file mode 100644 index 0000000..bd586c4 --- /dev/null +++ b/fe/src/components/ui/menu/MenuItemGroupLabel.vue @@ -0,0 +1,23 @@ + + + diff --git a/fe/src/components/ui/menu/MenuPositioner.vue b/fe/src/components/ui/menu/MenuPositioner.vue new file mode 100644 index 0000000..243228e --- /dev/null +++ b/fe/src/components/ui/menu/MenuPositioner.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/menu/MenuRadioItem.vue b/fe/src/components/ui/menu/MenuRadioItem.vue new file mode 100644 index 0000000..295f4a3 --- /dev/null +++ b/fe/src/components/ui/menu/MenuRadioItem.vue @@ -0,0 +1,43 @@ + + + diff --git a/fe/src/components/ui/menu/MenuRadioItemGroup.vue b/fe/src/components/ui/menu/MenuRadioItemGroup.vue new file mode 100644 index 0000000..cf43fad --- /dev/null +++ b/fe/src/components/ui/menu/MenuRadioItemGroup.vue @@ -0,0 +1,26 @@ + + + diff --git a/fe/src/components/ui/menu/MenuRoot.vue b/fe/src/components/ui/menu/MenuRoot.vue new file mode 100644 index 0000000..47df4c8 --- /dev/null +++ b/fe/src/components/ui/menu/MenuRoot.vue @@ -0,0 +1,36 @@ + + + diff --git a/fe/src/components/ui/menu/MenuSeparator.vue b/fe/src/components/ui/menu/MenuSeparator.vue new file mode 100644 index 0000000..5428aa7 --- /dev/null +++ b/fe/src/components/ui/menu/MenuSeparator.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/menu/MenuTrigger.vue b/fe/src/components/ui/menu/MenuTrigger.vue new file mode 100644 index 0000000..0d047eb --- /dev/null +++ b/fe/src/components/ui/menu/MenuTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/menu/MenuTriggerItem.vue b/fe/src/components/ui/menu/MenuTriggerItem.vue new file mode 100644 index 0000000..0001452 --- /dev/null +++ b/fe/src/components/ui/menu/MenuTriggerItem.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/menu/index.ts b/fe/src/components/ui/menu/index.ts new file mode 100644 index 0000000..33285b1 --- /dev/null +++ b/fe/src/components/ui/menu/index.ts @@ -0,0 +1,12 @@ +export { default as MenuRoot } from "./MenuRoot.vue"; +export { default as MenuTrigger } from "./MenuTrigger.vue"; +export { default as MenuIndicator } from "./MenuIndicator.vue"; +export { default as MenuPositioner } from "./MenuPositioner.vue"; +export { default as MenuContent } from "./MenuContent.vue"; +export { default as MenuItem } from "./MenuItem.vue"; +export { default as MenuTriggerItem } from "./MenuTriggerItem.vue"; +export { default as MenuCheckboxItem } from "./MenuCheckboxItem.vue"; +export { default as MenuRadioItemGroup } from "./MenuRadioItemGroup.vue"; +export { default as MenuItemGroupLabel } from "./MenuItemGroupLabel.vue"; +export { default as MenuRadioItem } from "./MenuRadioItem.vue"; +export { default as MenuSeparator } from "./MenuSeparator.vue"; diff --git a/fe/src/components/ui/native-select/NativeSelect.vue b/fe/src/components/ui/native-select/NativeSelect.vue new file mode 100644 index 0000000..449fbd2 --- /dev/null +++ b/fe/src/components/ui/native-select/NativeSelect.vue @@ -0,0 +1,15 @@ + + + diff --git a/fe/src/components/ui/native-select/NativeSelectOption.vue b/fe/src/components/ui/native-select/NativeSelectOption.vue new file mode 100644 index 0000000..1d79dfe --- /dev/null +++ b/fe/src/components/ui/native-select/NativeSelectOption.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/native-select/NativeSelectOptionGroup.vue b/fe/src/components/ui/native-select/NativeSelectOptionGroup.vue new file mode 100644 index 0000000..a365051 --- /dev/null +++ b/fe/src/components/ui/native-select/NativeSelectOptionGroup.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/native-select/index.ts b/fe/src/components/ui/native-select/index.ts new file mode 100644 index 0000000..b4b5fef --- /dev/null +++ b/fe/src/components/ui/native-select/index.ts @@ -0,0 +1,3 @@ +export { default as NativeSelect } from "./NativeSelect.vue"; +export { default as NativeSelectOption } from "./NativeSelectOption.vue"; +export { default as NativeSelectOptionGroup } from "./NativeSelectOptionGroup.vue"; diff --git a/fe/src/components/ui/pagination/PaginationContext.vue b/fe/src/components/ui/pagination/PaginationContext.vue new file mode 100644 index 0000000..e63be73 --- /dev/null +++ b/fe/src/components/ui/pagination/PaginationContext.vue @@ -0,0 +1,10 @@ + + + diff --git a/fe/src/components/ui/pagination/PaginationEllipsis.vue b/fe/src/components/ui/pagination/PaginationEllipsis.vue new file mode 100644 index 0000000..30beb0e --- /dev/null +++ b/fe/src/components/ui/pagination/PaginationEllipsis.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/pagination/PaginationItem.vue b/fe/src/components/ui/pagination/PaginationItem.vue new file mode 100644 index 0000000..fc8771f --- /dev/null +++ b/fe/src/components/ui/pagination/PaginationItem.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/pagination/PaginationNextTrigger.vue b/fe/src/components/ui/pagination/PaginationNextTrigger.vue new file mode 100644 index 0000000..541f454 --- /dev/null +++ b/fe/src/components/ui/pagination/PaginationNextTrigger.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/pagination/PaginationPrevTrigger.vue b/fe/src/components/ui/pagination/PaginationPrevTrigger.vue new file mode 100644 index 0000000..8762871 --- /dev/null +++ b/fe/src/components/ui/pagination/PaginationPrevTrigger.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/pagination/PaginationRoot.vue b/fe/src/components/ui/pagination/PaginationRoot.vue new file mode 100644 index 0000000..4e01884 --- /dev/null +++ b/fe/src/components/ui/pagination/PaginationRoot.vue @@ -0,0 +1,38 @@ + + + diff --git a/fe/src/components/ui/pagination/index.ts b/fe/src/components/ui/pagination/index.ts new file mode 100644 index 0000000..45612c8 --- /dev/null +++ b/fe/src/components/ui/pagination/index.ts @@ -0,0 +1,6 @@ +export { default as PaginationContext } from "./PaginationContext.vue"; +export { default as PaginationEllipsis } from "./PaginationEllipsis.vue"; +export { default as PaginationItem } from "./PaginationItem.vue"; +export { default as PaginationNextTrigger } from "./PaginationNextTrigger.vue"; +export { default as PaginationPrevTrigger } from "./PaginationPrevTrigger.vue"; +export { default as PaginationRoot } from "./PaginationRoot.vue"; diff --git a/fe/src/components/ui/password-input/PasswordInput.vue b/fe/src/components/ui/password-input/PasswordInput.vue new file mode 100644 index 0000000..1dcef63 --- /dev/null +++ b/fe/src/components/ui/password-input/PasswordInput.vue @@ -0,0 +1,57 @@ + + + diff --git a/fe/src/components/ui/password-input/index.ts b/fe/src/components/ui/password-input/index.ts new file mode 100644 index 0000000..3e7aa3f --- /dev/null +++ b/fe/src/components/ui/password-input/index.ts @@ -0,0 +1 @@ +export { default as PasswordInput } from './PasswordInput.vue' diff --git a/fe/src/components/ui/popover/PopoverArrow.vue b/fe/src/components/ui/popover/PopoverArrow.vue new file mode 100644 index 0000000..129e0fb --- /dev/null +++ b/fe/src/components/ui/popover/PopoverArrow.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/popover/PopoverArrowTip.vue b/fe/src/components/ui/popover/PopoverArrowTip.vue new file mode 100644 index 0000000..18fca0e --- /dev/null +++ b/fe/src/components/ui/popover/PopoverArrowTip.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/popover/PopoverContent.vue b/fe/src/components/ui/popover/PopoverContent.vue new file mode 100644 index 0000000..b4ded8f --- /dev/null +++ b/fe/src/components/ui/popover/PopoverContent.vue @@ -0,0 +1,34 @@ + + + diff --git a/fe/src/components/ui/popover/PopoverDescription.vue b/fe/src/components/ui/popover/PopoverDescription.vue new file mode 100644 index 0000000..96f547d --- /dev/null +++ b/fe/src/components/ui/popover/PopoverDescription.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/popover/PopoverIndicator.vue b/fe/src/components/ui/popover/PopoverIndicator.vue new file mode 100644 index 0000000..bfe0ea6 --- /dev/null +++ b/fe/src/components/ui/popover/PopoverIndicator.vue @@ -0,0 +1,26 @@ + + + diff --git a/fe/src/components/ui/popover/PopoverPositioner.vue b/fe/src/components/ui/popover/PopoverPositioner.vue new file mode 100644 index 0000000..b66cec1 --- /dev/null +++ b/fe/src/components/ui/popover/PopoverPositioner.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/popover/PopoverRoot.vue b/fe/src/components/ui/popover/PopoverRoot.vue new file mode 100644 index 0000000..83bb860 --- /dev/null +++ b/fe/src/components/ui/popover/PopoverRoot.vue @@ -0,0 +1,36 @@ + + + diff --git a/fe/src/components/ui/popover/PopoverTitle.vue b/fe/src/components/ui/popover/PopoverTitle.vue new file mode 100644 index 0000000..84a4ace --- /dev/null +++ b/fe/src/components/ui/popover/PopoverTitle.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/popover/PopoverTrigger.vue b/fe/src/components/ui/popover/PopoverTrigger.vue new file mode 100644 index 0000000..7c3af4f --- /dev/null +++ b/fe/src/components/ui/popover/PopoverTrigger.vue @@ -0,0 +1,30 @@ + + + diff --git a/fe/src/components/ui/popover/index.ts b/fe/src/components/ui/popover/index.ts new file mode 100644 index 0000000..5bdeecf --- /dev/null +++ b/fe/src/components/ui/popover/index.ts @@ -0,0 +1,9 @@ +export { default as PopoverArrow } from "./PopoverArrow.vue"; +export { default as PopoverArrowTip } from "./PopoverArrowTip.vue"; +export { default as PopoverContent } from "./PopoverContent.vue"; +export { default as PopoverDescription } from "./PopoverDescription.vue"; +export { default as PopoverIndicator } from "./PopoverIndicator.vue"; +export { default as PopoverPositioner } from "./PopoverPositioner.vue"; +export { default as PopoverRoot } from "./PopoverRoot.vue"; +export { default as PopoverTitle } from "./PopoverTitle.vue"; +export { default as PopoverTrigger } from "./PopoverTrigger.vue"; diff --git a/fe/src/components/ui/presence/Presence.vue b/fe/src/components/ui/presence/Presence.vue new file mode 100644 index 0000000..018190f --- /dev/null +++ b/fe/src/components/ui/presence/Presence.vue @@ -0,0 +1,44 @@ + + + diff --git a/fe/src/components/ui/presence/index.ts b/fe/src/components/ui/presence/index.ts new file mode 100644 index 0000000..fa686f8 --- /dev/null +++ b/fe/src/components/ui/presence/index.ts @@ -0,0 +1 @@ +export { default as Presence } from "./Presence.vue"; diff --git a/fe/src/components/ui/progress-circular/ProgressCircle.vue b/fe/src/components/ui/progress-circular/ProgressCircle.vue new file mode 100644 index 0000000..148f538 --- /dev/null +++ b/fe/src/components/ui/progress-circular/ProgressCircle.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/progress-circular/ProgressCircleRange.vue b/fe/src/components/ui/progress-circular/ProgressCircleRange.vue new file mode 100644 index 0000000..f824a93 --- /dev/null +++ b/fe/src/components/ui/progress-circular/ProgressCircleRange.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/progress-circular/ProgressCircleTrack.vue b/fe/src/components/ui/progress-circular/ProgressCircleTrack.vue new file mode 100644 index 0000000..e21812f --- /dev/null +++ b/fe/src/components/ui/progress-circular/ProgressCircleTrack.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/progress-circular/ProgressLabel.vue b/fe/src/components/ui/progress-circular/ProgressLabel.vue new file mode 100644 index 0000000..34d27b5 --- /dev/null +++ b/fe/src/components/ui/progress-circular/ProgressLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/progress-circular/ProgressRoot.vue b/fe/src/components/ui/progress-circular/ProgressRoot.vue new file mode 100644 index 0000000..fceab70 --- /dev/null +++ b/fe/src/components/ui/progress-circular/ProgressRoot.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/progress-circular/ProgressValueText.vue b/fe/src/components/ui/progress-circular/ProgressValueText.vue new file mode 100644 index 0000000..c0ed330 --- /dev/null +++ b/fe/src/components/ui/progress-circular/ProgressValueText.vue @@ -0,0 +1,23 @@ + + + diff --git a/fe/src/components/ui/progress-circular/index.ts b/fe/src/components/ui/progress-circular/index.ts new file mode 100644 index 0000000..5c951ad --- /dev/null +++ b/fe/src/components/ui/progress-circular/index.ts @@ -0,0 +1,6 @@ +export { default as ProgressCircle } from "./ProgressCircle.vue"; +export { default as ProgressCircleRange } from "./ProgressCircleRange.vue"; +export { default as ProgressCircleTrack } from "./ProgressCircleTrack.vue"; +export { default as ProgressLabel } from "./ProgressLabel.vue"; +export { default as ProgressRoot } from "./ProgressRoot.vue"; +export { default as ProgressValueText } from "./ProgressValueText.vue"; diff --git a/fe/src/components/ui/progress-linear/ProgressLabel.vue b/fe/src/components/ui/progress-linear/ProgressLabel.vue new file mode 100644 index 0000000..279ffc0 --- /dev/null +++ b/fe/src/components/ui/progress-linear/ProgressLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/progress-linear/ProgressRange.vue b/fe/src/components/ui/progress-linear/ProgressRange.vue new file mode 100644 index 0000000..c9c6314 --- /dev/null +++ b/fe/src/components/ui/progress-linear/ProgressRange.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/progress-linear/ProgressRoot.vue b/fe/src/components/ui/progress-linear/ProgressRoot.vue new file mode 100644 index 0000000..f005c65 --- /dev/null +++ b/fe/src/components/ui/progress-linear/ProgressRoot.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/progress-linear/ProgressTrack.vue b/fe/src/components/ui/progress-linear/ProgressTrack.vue new file mode 100644 index 0000000..edb15bd --- /dev/null +++ b/fe/src/components/ui/progress-linear/ProgressTrack.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/progress-linear/ProgressValueText.vue b/fe/src/components/ui/progress-linear/ProgressValueText.vue new file mode 100644 index 0000000..4f5319d --- /dev/null +++ b/fe/src/components/ui/progress-linear/ProgressValueText.vue @@ -0,0 +1,23 @@ + + + diff --git a/fe/src/components/ui/progress-linear/index.ts b/fe/src/components/ui/progress-linear/index.ts new file mode 100644 index 0000000..32eec20 --- /dev/null +++ b/fe/src/components/ui/progress-linear/index.ts @@ -0,0 +1,5 @@ +export { default as ProgressLabel } from "./ProgressLabel.vue"; +export { default as ProgressRange } from "./ProgressRange.vue"; +export { default as ProgressRoot } from "./ProgressRoot.vue"; +export { default as ProgressTrack } from "./ProgressTrack.vue"; +export { default as ProgressValueText } from "./ProgressValueText.vue"; diff --git a/fe/src/components/ui/radio-group/RadioGroupIndicator.vue b/fe/src/components/ui/radio-group/RadioGroupIndicator.vue new file mode 100644 index 0000000..c008874 --- /dev/null +++ b/fe/src/components/ui/radio-group/RadioGroupIndicator.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/radio-group/RadioGroupItem.vue b/fe/src/components/ui/radio-group/RadioGroupItem.vue new file mode 100644 index 0000000..7780b01 --- /dev/null +++ b/fe/src/components/ui/radio-group/RadioGroupItem.vue @@ -0,0 +1,33 @@ + + + diff --git a/fe/src/components/ui/radio-group/RadioGroupItemControl.vue b/fe/src/components/ui/radio-group/RadioGroupItemControl.vue new file mode 100644 index 0000000..c2c89f1 --- /dev/null +++ b/fe/src/components/ui/radio-group/RadioGroupItemControl.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/radio-group/RadioGroupItemHiddenInput.vue b/fe/src/components/ui/radio-group/RadioGroupItemHiddenInput.vue new file mode 100644 index 0000000..118cf69 --- /dev/null +++ b/fe/src/components/ui/radio-group/RadioGroupItemHiddenInput.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/radio-group/RadioGroupItemText.vue b/fe/src/components/ui/radio-group/RadioGroupItemText.vue new file mode 100644 index 0000000..3a23b1b --- /dev/null +++ b/fe/src/components/ui/radio-group/RadioGroupItemText.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/radio-group/RadioGroupLabel.vue b/fe/src/components/ui/radio-group/RadioGroupLabel.vue new file mode 100644 index 0000000..74ca31c --- /dev/null +++ b/fe/src/components/ui/radio-group/RadioGroupLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/radio-group/RadioGroupRoot.vue b/fe/src/components/ui/radio-group/RadioGroupRoot.vue new file mode 100644 index 0000000..2801467 --- /dev/null +++ b/fe/src/components/ui/radio-group/RadioGroupRoot.vue @@ -0,0 +1,37 @@ + + + diff --git a/fe/src/components/ui/radio-group/index.ts b/fe/src/components/ui/radio-group/index.ts new file mode 100644 index 0000000..d6deb3c --- /dev/null +++ b/fe/src/components/ui/radio-group/index.ts @@ -0,0 +1,7 @@ +export { default as RadioGroupRoot } from "./RadioGroupRoot.vue"; +export { default as RadioGroupLabel } from "./RadioGroupLabel.vue"; +export { default as RadioGroupIndicator } from "./RadioGroupIndicator.vue"; +export { default as RadioGroupItem } from "./RadioGroupItem.vue"; +export { default as RadioGroupItemText } from "./RadioGroupItemText.vue"; +export { default as RadioGroupItemControl } from "./RadioGroupItemControl.vue"; +export { default as RadioGroupItemHiddenInput } from "./RadioGroupItemHiddenInput.vue"; diff --git a/fe/src/components/ui/scroll-area/ScrollAreaContent.vue b/fe/src/components/ui/scroll-area/ScrollAreaContent.vue new file mode 100644 index 0000000..27b74b3 --- /dev/null +++ b/fe/src/components/ui/scroll-area/ScrollAreaContent.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/scroll-area/ScrollAreaCorner.vue b/fe/src/components/ui/scroll-area/ScrollAreaCorner.vue new file mode 100644 index 0000000..de52649 --- /dev/null +++ b/fe/src/components/ui/scroll-area/ScrollAreaCorner.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/scroll-area/ScrollAreaRoot.vue b/fe/src/components/ui/scroll-area/ScrollAreaRoot.vue new file mode 100644 index 0000000..7f57fa1 --- /dev/null +++ b/fe/src/components/ui/scroll-area/ScrollAreaRoot.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/scroll-area/ScrollAreaScrollbar.vue b/fe/src/components/ui/scroll-area/ScrollAreaScrollbar.vue new file mode 100644 index 0000000..e2ba072 --- /dev/null +++ b/fe/src/components/ui/scroll-area/ScrollAreaScrollbar.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/scroll-area/ScrollAreaThumb.vue b/fe/src/components/ui/scroll-area/ScrollAreaThumb.vue new file mode 100644 index 0000000..8de60a1 --- /dev/null +++ b/fe/src/components/ui/scroll-area/ScrollAreaThumb.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/scroll-area/ScrollAreaViewport.vue b/fe/src/components/ui/scroll-area/ScrollAreaViewport.vue new file mode 100644 index 0000000..2623b98 --- /dev/null +++ b/fe/src/components/ui/scroll-area/ScrollAreaViewport.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/scroll-area/index.ts b/fe/src/components/ui/scroll-area/index.ts new file mode 100644 index 0000000..ec972b4 --- /dev/null +++ b/fe/src/components/ui/scroll-area/index.ts @@ -0,0 +1,7 @@ +export { default as ScrollAreaRoot } from "./ScrollAreaRoot.vue"; +export { default as ScrollAreaViewport } from "./ScrollAreaViewport.vue"; +export { default as ScrollAreaContent } from "./ScrollAreaContent.vue"; +export { default as ScrollAreaScrollbar } from "./ScrollAreaScrollbar.vue"; +export { default as ScrollAreaThumb } from "./ScrollAreaThumb.vue"; +export { default as ScrollAreaCorner } from "./ScrollAreaCorner.vue"; + diff --git a/fe/src/components/ui/select/SelectClearTrigger.vue b/fe/src/components/ui/select/SelectClearTrigger.vue new file mode 100644 index 0000000..3b5212e --- /dev/null +++ b/fe/src/components/ui/select/SelectClearTrigger.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/select/SelectContent.vue b/fe/src/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..b0866df --- /dev/null +++ b/fe/src/components/ui/select/SelectContent.vue @@ -0,0 +1,35 @@ + + + diff --git a/fe/src/components/ui/select/SelectControl.vue b/fe/src/components/ui/select/SelectControl.vue new file mode 100644 index 0000000..5080d27 --- /dev/null +++ b/fe/src/components/ui/select/SelectControl.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/select/SelectHiddenSelect.vue b/fe/src/components/ui/select/SelectHiddenSelect.vue new file mode 100644 index 0000000..d9c0ca5 --- /dev/null +++ b/fe/src/components/ui/select/SelectHiddenSelect.vue @@ -0,0 +1,16 @@ + + + diff --git a/fe/src/components/ui/select/SelectIndicator.vue b/fe/src/components/ui/select/SelectIndicator.vue new file mode 100644 index 0000000..46b2b65 --- /dev/null +++ b/fe/src/components/ui/select/SelectIndicator.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/select/SelectItem.vue b/fe/src/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..912d1a6 --- /dev/null +++ b/fe/src/components/ui/select/SelectItem.vue @@ -0,0 +1,33 @@ + + + diff --git a/fe/src/components/ui/select/SelectItemGroup.vue b/fe/src/components/ui/select/SelectItemGroup.vue new file mode 100644 index 0000000..a5a1e57 --- /dev/null +++ b/fe/src/components/ui/select/SelectItemGroup.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/select/SelectItemGroupLabel.vue b/fe/src/components/ui/select/SelectItemGroupLabel.vue new file mode 100644 index 0000000..cab17e3 --- /dev/null +++ b/fe/src/components/ui/select/SelectItemGroupLabel.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/select/SelectItemIndicator.vue b/fe/src/components/ui/select/SelectItemIndicator.vue new file mode 100644 index 0000000..a0b123c --- /dev/null +++ b/fe/src/components/ui/select/SelectItemIndicator.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/select/SelectItemText.vue b/fe/src/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..6b821e5 --- /dev/null +++ b/fe/src/components/ui/select/SelectItemText.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/select/SelectLabel.vue b/fe/src/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..865b5ce --- /dev/null +++ b/fe/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/select/SelectPositioner.vue b/fe/src/components/ui/select/SelectPositioner.vue new file mode 100644 index 0000000..c01b96b --- /dev/null +++ b/fe/src/components/ui/select/SelectPositioner.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/select/SelectRoot.vue b/fe/src/components/ui/select/SelectRoot.vue new file mode 100644 index 0000000..6f53b9a --- /dev/null +++ b/fe/src/components/ui/select/SelectRoot.vue @@ -0,0 +1,41 @@ + + + diff --git a/fe/src/components/ui/select/SelectTrigger.vue b/fe/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..69f4017 --- /dev/null +++ b/fe/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/select/SelectValueText.vue b/fe/src/components/ui/select/SelectValueText.vue new file mode 100644 index 0000000..7356d93 --- /dev/null +++ b/fe/src/components/ui/select/SelectValueText.vue @@ -0,0 +1,26 @@ + + + diff --git a/fe/src/components/ui/select/index.ts b/fe/src/components/ui/select/index.ts new file mode 100644 index 0000000..42cd444 --- /dev/null +++ b/fe/src/components/ui/select/index.ts @@ -0,0 +1,15 @@ +export { default as SelectClearTrigger } from "./SelectClearTrigger.vue"; +export { default as SelectContent } from "./SelectContent.vue"; +export { default as SelectControl } from "./SelectControl.vue"; +export { default as SelectHiddenSelect } from "./SelectHiddenSelect.vue"; +export { default as SelectIndicator } from "./SelectIndicator.vue"; +export { default as SelectItem } from "./SelectItem.vue"; +export { default as SelectItemGroup } from "./SelectItemGroup.vue"; +export { default as SelectItemGroupLabel } from "./SelectItemGroupLabel.vue"; +export { default as SelectItemIndicator } from "./SelectItemIndicator.vue"; +export { default as SelectItemText } from "./SelectItemText.vue"; +export { default as SelectLabel } from "./SelectLabel.vue"; +export { default as SelectPositioner } from "./SelectPositioner.vue"; +export { default as SelectRoot } from "./SelectRoot.vue"; +export { default as SelectTrigger } from "./SelectTrigger.vue"; +export { default as SelectValueText } from "./SelectValueText.vue"; diff --git a/fe/src/components/ui/separator/Separator.vue b/fe/src/components/ui/separator/Separator.vue new file mode 100644 index 0000000..f1dadc5 --- /dev/null +++ b/fe/src/components/ui/separator/Separator.vue @@ -0,0 +1,21 @@ + + + diff --git a/fe/src/components/ui/separator/index.ts b/fe/src/components/ui/separator/index.ts new file mode 100644 index 0000000..aae7f1a --- /dev/null +++ b/fe/src/components/ui/separator/index.ts @@ -0,0 +1 @@ +export { default as Separator } from "./Separator.vue"; diff --git a/fe/src/components/ui/sheet/SheetBackdrop.vue b/fe/src/components/ui/sheet/SheetBackdrop.vue new file mode 100644 index 0000000..1e84cf7 --- /dev/null +++ b/fe/src/components/ui/sheet/SheetBackdrop.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/sheet/SheetCloseTrigger.vue b/fe/src/components/ui/sheet/SheetCloseTrigger.vue new file mode 100644 index 0000000..fb8d404 --- /dev/null +++ b/fe/src/components/ui/sheet/SheetCloseTrigger.vue @@ -0,0 +1,44 @@ + + + diff --git a/fe/src/components/ui/sheet/SheetContent.vue b/fe/src/components/ui/sheet/SheetContent.vue new file mode 100644 index 0000000..9eb9c01 --- /dev/null +++ b/fe/src/components/ui/sheet/SheetContent.vue @@ -0,0 +1,40 @@ + + + diff --git a/fe/src/components/ui/sheet/SheetDescription.vue b/fe/src/components/ui/sheet/SheetDescription.vue new file mode 100644 index 0000000..362e013 --- /dev/null +++ b/fe/src/components/ui/sheet/SheetDescription.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/sheet/SheetPositioner.vue b/fe/src/components/ui/sheet/SheetPositioner.vue new file mode 100644 index 0000000..9abde8b --- /dev/null +++ b/fe/src/components/ui/sheet/SheetPositioner.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/sheet/SheetRoot.vue b/fe/src/components/ui/sheet/SheetRoot.vue new file mode 100644 index 0000000..e3d5894 --- /dev/null +++ b/fe/src/components/ui/sheet/SheetRoot.vue @@ -0,0 +1,36 @@ + + + diff --git a/fe/src/components/ui/sheet/SheetTitle.vue b/fe/src/components/ui/sheet/SheetTitle.vue new file mode 100644 index 0000000..5a2e13a --- /dev/null +++ b/fe/src/components/ui/sheet/SheetTitle.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/sheet/SheetTrigger.vue b/fe/src/components/ui/sheet/SheetTrigger.vue new file mode 100644 index 0000000..634db69 --- /dev/null +++ b/fe/src/components/ui/sheet/SheetTrigger.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/sheet/index.ts b/fe/src/components/ui/sheet/index.ts new file mode 100644 index 0000000..08a9d59 --- /dev/null +++ b/fe/src/components/ui/sheet/index.ts @@ -0,0 +1,8 @@ +export { default as SheetRoot } from "./SheetRoot.vue"; +export { default as SheetTrigger } from "./SheetTrigger.vue"; +export { default as SheetBackdrop } from "./SheetBackdrop.vue"; +export { default as SheetPositioner } from "./SheetPositioner.vue"; +export { default as SheetContent } from "./SheetContent.vue"; +export { default as SheetTitle } from "./SheetTitle.vue"; +export { default as SheetDescription } from "./SheetDescription.vue"; +export { default as SheetCloseTrigger } from "./SheetCloseTrigger.vue"; diff --git a/fe/src/components/ui/slider/SliderControl.vue b/fe/src/components/ui/slider/SliderControl.vue new file mode 100644 index 0000000..cf5a95a --- /dev/null +++ b/fe/src/components/ui/slider/SliderControl.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/slider/SliderHiddenInput.vue b/fe/src/components/ui/slider/SliderHiddenInput.vue new file mode 100644 index 0000000..296650e --- /dev/null +++ b/fe/src/components/ui/slider/SliderHiddenInput.vue @@ -0,0 +1,18 @@ + + + diff --git a/fe/src/components/ui/slider/SliderLabel.vue b/fe/src/components/ui/slider/SliderLabel.vue new file mode 100644 index 0000000..f314b9a --- /dev/null +++ b/fe/src/components/ui/slider/SliderLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/slider/SliderMarker.vue b/fe/src/components/ui/slider/SliderMarker.vue new file mode 100644 index 0000000..0d8fc9a --- /dev/null +++ b/fe/src/components/ui/slider/SliderMarker.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/slider/SliderMarkerGroup.vue b/fe/src/components/ui/slider/SliderMarkerGroup.vue new file mode 100644 index 0000000..53dcca0 --- /dev/null +++ b/fe/src/components/ui/slider/SliderMarkerGroup.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/slider/SliderRange.vue b/fe/src/components/ui/slider/SliderRange.vue new file mode 100644 index 0000000..93f63e2 --- /dev/null +++ b/fe/src/components/ui/slider/SliderRange.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/slider/SliderRoot.vue b/fe/src/components/ui/slider/SliderRoot.vue new file mode 100644 index 0000000..4e9c79d --- /dev/null +++ b/fe/src/components/ui/slider/SliderRoot.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/slider/SliderThumb.vue b/fe/src/components/ui/slider/SliderThumb.vue new file mode 100644 index 0000000..1f52346 --- /dev/null +++ b/fe/src/components/ui/slider/SliderThumb.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/slider/SliderTrack.vue b/fe/src/components/ui/slider/SliderTrack.vue new file mode 100644 index 0000000..b033aa3 --- /dev/null +++ b/fe/src/components/ui/slider/SliderTrack.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/slider/SliderValueText.vue b/fe/src/components/ui/slider/SliderValueText.vue new file mode 100644 index 0000000..2617a0f --- /dev/null +++ b/fe/src/components/ui/slider/SliderValueText.vue @@ -0,0 +1,25 @@ + + + diff --git a/fe/src/components/ui/slider/index.ts b/fe/src/components/ui/slider/index.ts new file mode 100644 index 0000000..f41c7f4 --- /dev/null +++ b/fe/src/components/ui/slider/index.ts @@ -0,0 +1,10 @@ +export { default as SliderRoot } from "./SliderRoot.vue"; +export { default as SliderLabel } from "./SliderLabel.vue"; +export { default as SliderValueText } from "./SliderValueText.vue"; +export { default as SliderControl } from "./SliderControl.vue"; +export { default as SliderTrack } from "./SliderTrack.vue"; +export { default as SliderRange } from "./SliderRange.vue"; +export { default as SliderThumb } from "./SliderThumb.vue"; +export { default as SliderHiddenInput } from "./SliderHiddenInput.vue"; +export { default as SliderMarkerGroup } from "./SliderMarkerGroup.vue"; +export { default as SliderMarker } from "./SliderMarker.vue"; diff --git a/fe/src/components/ui/slot/index.ts b/fe/src/components/ui/slot/index.ts new file mode 100644 index 0000000..90871a5 --- /dev/null +++ b/fe/src/components/ui/slot/index.ts @@ -0,0 +1,59 @@ +import { + defineComponent, + h, + cloneVNode, + Fragment, + isVNode, + type VNode, + type PropType, +} from "vue"; +import { calculateSlot, flattenItems, type AnyProps } from "./slot"; + +/* ------------------------------------------------------------------------------------------------- + * Vue Implementation (Component Shell) + * -----------------------------------------------------------------------------------------------*/ + +export const Slot = defineComponent({ + name: "Slot", + inheritAttrs: false, + props: { + children: { + type: [Object, Array] as PropType, + }, + }, + setup(props, { attrs, slots }) { + return () => { + const raw = props.children ?? slots.default?.(); + + const isValidVNode = (item: any): item is VNode => isVNode(item) && typeof item.type !== "symbol"; + + // Use generic flatten logic with Vue-specific adapter + // Aligned with React's flatten(children) logic + const items = flattenItems( + raw as any, + (item) => isVNode(item) && item.type === Fragment, + (item) => (isVNode(item) && Array.isArray(item.children) ? (item.children as VNode[]) : []) + ).filter(isValidVNode); + + // Use our vanilla logic to determine the transform + const result = calculateSlot({ + props: attrs as AnyProps, + items, + isValid: isValidVNode, + getProps: (item) => (item.props as AnyProps) || {}, + getChildren: (item) => item.children, + }); + + // If it's a wrapper, we render a real div + if (result.type === "wrapper") { + return h("div", result.props, result.children as any); + } + + // If it's slotted, we clone the target VNode with merged props + const target = result.target; + return cloneVNode(target, result.props, false); + }; + }, +}); + +export { Slot as Root }; diff --git a/fe/src/components/ui/slot/slot.ts b/fe/src/components/ui/slot/slot.ts new file mode 100644 index 0000000..e7ce2ac --- /dev/null +++ b/fe/src/components/ui/slot/slot.ts @@ -0,0 +1,133 @@ +export type AnyProps = Record; + +export interface SlotParams { + props: AnyProps; + items: T[]; + isValid: (item: T) => boolean; + getProps: (item: T) => AnyProps; + getChildren: (item: T) => any; +} + +export type SlotResult = + | { + type: "slotted"; + target: T; + props: AnyProps; + children: any; + } + | { + type: "wrapper"; + target: "div"; + props: AnyProps; + children: T[]; + }; + +/** + * mergeProps: Pure vanilla logic to combine attributes/props. + * Enhanced to handle framework-agnostic class merging and deduplication. + */ +export function mergeProps(slotProps: AnyProps, childProps: AnyProps): AnyProps { + const result: AnyProps = { ...childProps }; + + for (const key in slotProps) { + const slotValue = slotProps[key]; + + // 1. Event Handlers (Merge them to execute both) + const isHandler = /^on[A-Z]/.test(key); + if (isHandler) { + const childValue = childProps[key]; + if (typeof slotValue === "function" && typeof childValue === "function") { + result[key] = (...args: any[]) => { + childValue(...args); + slotValue(...args); + }; + } else if (slotValue) { + result[key] = slotValue; + } + continue; + } + + // 2. Class Names (Handle 'class' and 'className' as synonyms) + if (key === "class" || key === "className") { + const slotClasses = (slotValue || "").split(/\s+/); + const childClasses = (childProps.class || childProps.className || "").split(/\s+/); + + // Deduplicate classes + const combined = Array.from(new Set([...slotClasses, ...childClasses])) + .filter(Boolean) + .join(" "); + + // Update the key that was provided, and sync the other if it exists + result[key] = combined; + const otherKey = key === "class" ? "className" : "class"; + if (otherKey in childProps) { + result[otherKey] = combined; + } + continue; + } + + // 3. Styles (Object merge) + if (key === "style") { + result[key] = { ...slotValue, ...childProps.style }; + continue; + } + + // 4. Default attribute override + if (childProps[key] === undefined) { + result[key] = slotValue; + } + } + + return result; +} + +/** + * flattenItems: A generic utility to flatten hierarchies based on a marker (like Fragments). + */ +export function flattenItems( + items: T | T[], + isFragment: (item: T) => boolean, + getChildren: (item: T) => T | T[] +): T[] { + const result: T[] = []; + const list = Array.isArray(items) ? items : [items]; + + list.forEach((item) => { + if (item === null || item === undefined) return; + + if (isFragment(item)) { + const children = getChildren(item); + result.push(...flattenItems(children, isFragment, getChildren)); + } else { + result.push(item); + } + }); + + return result; +} + +/** + * calculateSlot: Decides which element gets the props. + */ +export function calculateSlot(params: SlotParams): SlotResult { + const { props, items, isValid, getProps, getChildren } = params; + + if (items.length === 1) { + const primary = items[0]; + if (primary !== undefined && isValid(primary)) { + return { + type: "slotted", + target: primary, + props: mergeProps(props, getProps(primary)), + children: getChildren(primary), + }; + } + } + + return { + type: "wrapper", + target: "div", + props: props, + children: items, + }; +} diff --git a/fe/src/components/ui/styles/accordion.styles.ts b/fe/src/components/ui/styles/accordion.styles.ts new file mode 100644 index 0000000..74b9857 --- /dev/null +++ b/fe/src/components/ui/styles/accordion.styles.ts @@ -0,0 +1,39 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +// Styles +export const accordionRootVariants = cva("flex flex-col", { + variants: { + variant: { + default: "-mt-4 -mb-3", + boxed: "gap-3", + }, + }, + defaultVariants: { + variant: "default", + }, +}); +export const accordionItemVariants = cva("group", { + variants: { + variant: { + default: "border-b border-b-foreground/10 last:border-b-transparent", + boxed: "py-0 px-4", + }, + }, + defaultVariants: { + variant: "default", + }, +}); +export const accordionTrigger = + "group-hover:underline cursor-pointer w-full flex items-center font-medium py-4"; +export const accordionItemIndicator = + "ms-auto opacity-70 [&>svg]:size-4 data-[state=open]:rotate-180 transition"; +export const accordionContent = + "-mt-1 pb-4 opacity-80 data-[state=open]:animate-in data-[state=open]:zoom-in-95 data-[state=open]:fade-in-0"; + +// Types +export type AccordionRootVariants = { + variant?: VariantProps["variant"]; +}; +export type AccordionItemVariants = { + variant?: VariantProps["variant"]; +}; diff --git a/fe/src/components/ui/styles/alert.styles.ts b/fe/src/components/ui/styles/alert.styles.ts new file mode 100644 index 0000000..99077f6 --- /dev/null +++ b/fe/src/components/ui/styles/alert.styles.ts @@ -0,0 +1,134 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +export const alertRootVariants = cva( + [ + "flex flex-col gap-1 [&>svg]:size-5 [&>svg]:absolute [&>svg]:my-auto [&>svg]:inset-y-0 [&>svg]:left-5 [&>svg]:stroke-[1.5] has-[>svg]:ps-14 ps-5 pe-20 py-4", + "rounded-xl shadow-md/5 isolate relative cursor-pointer", + "before:absolute after:absolute before:z-[-1] after:z-[-1] before:rounded-[inherit] after:rounded-[inherit]", + ], + { + variants: { + variant: { + ghost: "after:from-transparent bg-transparent text-foreground", + primary: "after:from-primary/40 bg-primary text-primary-foreground", + secondary: + "after:from-secondary/40 bg-secondary text-secondary-foreground", + success: "after:from-success/40 bg-success text-success-foreground", + danger: "after:from-danger/40 bg-danger text-danger-foreground", + pending: "after:from-pending/40 bg-pending text-pending-foreground", + warning: "after:from-warning/40 bg-warning text-warning-foreground", + }, + look: { + filled: [ + "dark:outline-2 dark:outline-black/30", + "before:inset-0 before:bg-gradient-to-b before:from-white/40 before:to-white/[.05]", + "after:inset-[2px] after:bg-gradient-to-b after:to-white/[.08] after:rounded-[calc(var(--radius)*0.9)]", + "dark:before:from-black/[.5] dark:before:to-black/70", + "dark:after:from-black/[.3] dark:after:to-black/[.1]", + ], + outline: + "before:bg-none before:border before:rounded-[inherit] before:inset-0 after:hidden bg-background", + flat: "before:inset-0 dark:before:inset-0 dark:before:bg-black/60", + text: "before:hidden shadow-none bg-transparent", + }, + }, + compoundVariants: [ + { + variant: "ghost", + look: "flat", + class: "dark:before:bg-transparent", + }, + { + variant: "ghost", + look: "outline", + class: + "before:bg-foreground/10 dark:before:bg-foreground/[.15] before:border-foreground/40 dark:before:border-foreground/20 text-foreground", + }, + { + variant: "primary", + look: "outline", + class: + "before:bg-primary/10 dark:before:bg-primary/[.15] before:border-primary/40 dark:before:border-primary/20 text-primary", + }, + { + variant: "secondary", + look: "outline", + class: + "before:bg-secondary/10 dark:before:bg-secondary/[.15] before:border-secondary/40 dark:before:border-secondary/20 text-secondary", + }, + { + variant: "success", + look: "outline", + class: + "before:bg-success/10 dark:before:bg-success/[.15] before:border-success/40 dark:before:border-success/20 text-success", + }, + { + variant: "danger", + look: "outline", + class: + "before:bg-danger/10 dark:before:bg-danger/[.15] before:border-danger/40 dark:before:border-danger/20 text-danger", + }, + { + variant: "pending", + look: "outline", + class: + "before:bg-pending/10 dark:before:bg-pending/[.15] before:border-pending/40 dark:before:border-pending/20 text-pending", + }, + { + variant: "warning", + look: "outline", + class: + "before:bg-warning/10 dark:before:bg-warning/[.15] before:border-warning/40 dark:before:border-warning/20 text-warning", + }, + { + variant: "ghost", + look: "text", + class: "text-foreground", + }, + { + variant: "primary", + look: "text", + class: "text-primary", + }, + { + variant: "secondary", + look: "text", + class: "text-secondary", + }, + { + variant: "success", + look: "text", + class: "text-success", + }, + { + variant: "danger", + look: "text", + class: "text-danger", + }, + { + variant: "pending", + look: "text", + class: "text-pending", + }, + { + variant: "warning", + look: "text", + class: "text-warning", + }, + ], + defaultVariants: { + variant: "primary", + look: "flat", + }, + } +); + +export type AlertRootVariants = { + look?: VariantProps["look"]; + variant?: VariantProps["variant"]; +}; + +export const alertTitle = "font-medium"; +export const alertDescription = "opacity-70"; +export const alertCloseTrigger = + "absolute right-5 inset-y-0 my-auto size-4 [&>svg]:size-full cursor-pointer"; diff --git a/fe/src/components/ui/styles/avatar.styles.ts b/fe/src/components/ui/styles/avatar.styles.ts new file mode 100644 index 0000000..a941c4c --- /dev/null +++ b/fe/src/components/ui/styles/avatar.styles.ts @@ -0,0 +1,24 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +// Styles +export const avatarRootVariants = cva( + "size-14 rounded-xl overflow-hidden relative bg-foreground/5 flex items-center justify-center border-3 ring-1 border-transparent", + { + variants: { + bordered: { + true: "ring-foreground/20", + false: "ring-transparent border-none", + }, + }, + defaultVariants: { + bordered: true, + }, + } +); +export const avatarFallback = "font-medium"; +export const avatarImage = "absolute top-0 size-full object-cover"; + +// Types +export type AvatarRootVariants = { + bordered?: VariantProps["bordered"]; +}; diff --git a/fe/src/components/ui/styles/badge.styles.ts b/fe/src/components/ui/styles/badge.styles.ts new file mode 100644 index 0000000..44dbac6 --- /dev/null +++ b/fe/src/components/ui/styles/badge.styles.ts @@ -0,0 +1,130 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +// Styles +export const badgeVariants = cva( + [ + "text-xs rounded-lg shadow-md/5 isolate relative px-2 py-0.5 cursor-pointer inline-flex items-center justify-center gap-1 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 [&_svg]:pointer-events-none [&_svg]:size-3.5 [&_svg]:stroke-2 [&_svg]:shrink-0", + "before:absolute after:absolute before:z-[-1] after:z-[-1] before:rounded-[inherit] after:rounded-[inherit]", + ], + { + variants: { + variant: { + ghost: "after:from-transparent bg-transparent text-foreground", + primary: "after:from-primary/40 bg-primary text-primary-foreground", + secondary: + "after:from-secondary/40 bg-secondary text-secondary-foreground", + success: "after:from-success/40 bg-success text-success-foreground", + danger: "after:from-danger/40 bg-danger text-danger-foreground", + pending: "after:from-pending/40 bg-pending text-pending-foreground", + warning: "after:from-warning/40 bg-warning text-warning-foreground", + }, + look: { + filled: [ + "dark:outline-2 dark:outline-black/30", + "before:inset-0 before:bg-gradient-to-b before:from-white/40 before:to-white/[.05]", + "after:inset-[2px] after:bg-gradient-to-b after:to-white/[.08] after:rounded-[calc(var(--radius)*0.7)]", + "dark:before:from-black/[.5] dark:before:to-black/70", + "dark:after:from-black/[.3] dark:after:to-black/[.1]", + ], + outline: + "before:bg-none before:border before:rounded-[inherit] before:inset-0 after:hidden bg-background", + flat: "before:inset-0 dark:before:inset-0 dark:before:bg-black/60", + text: "before:hidden shadow-none bg-transparent", + }, + }, + compoundVariants: [ + { + variant: "ghost", + look: "flat", + class: "dark:before:bg-transparent", + }, + { + variant: "ghost", + look: "outline", + class: + "before:bg-foreground/10 dark:before:bg-foreground/[.15] before:border-foreground/40 dark:before:border-foreground/20 text-foreground", + }, + { + variant: "primary", + look: "outline", + class: + "before:bg-primary/10 dark:before:bg-primary/[.15] before:border-primary/40 dark:before:border-primary/20 text-primary", + }, + { + variant: "secondary", + look: "outline", + class: + "before:bg-secondary/10 dark:before:bg-secondary/[.15] before:border-secondary/40 dark:before:border-secondary/20 text-secondary", + }, + { + variant: "success", + look: "outline", + class: + "before:bg-success/10 dark:before:bg-success/[.15] before:border-success/40 dark:before:border-success/20 text-success", + }, + { + variant: "danger", + look: "outline", + class: + "before:bg-danger/10 dark:before:bg-danger/[.15] before:border-danger/40 dark:before:border-danger/20 text-danger", + }, + { + variant: "pending", + look: "outline", + class: + "before:bg-pending/10 dark:before:bg-pending/[.15] before:border-pending/40 dark:before:border-pending/20 text-pending", + }, + { + variant: "warning", + look: "outline", + class: + "before:bg-warning/10 dark:before:bg-warning/[.15] before:border-warning/40 dark:before:border-warning/20 text-warning", + }, + { + variant: "ghost", + look: "text", + class: "text-foreground", + }, + { + variant: "primary", + look: "text", + class: "text-primary", + }, + { + variant: "secondary", + look: "text", + class: "text-secondary", + }, + { + variant: "success", + look: "text", + class: "text-success", + }, + { + variant: "danger", + look: "text", + class: "text-danger", + }, + { + variant: "pending", + look: "text", + class: "text-pending", + }, + { + variant: "warning", + look: "text", + class: "text-warning", + }, + ], + defaultVariants: { + variant: "primary", + look: "flat", + }, + } +); + +// Types +export type BadgeVariants = { + look?: VariantProps["look"]; + variant?: VariantProps["variant"]; +}; diff --git a/fe/src/components/ui/styles/box.styles.ts b/fe/src/components/ui/styles/box.styles.ts new file mode 100644 index 0000000..eb3a081 --- /dev/null +++ b/fe/src/components/ui/styles/box.styles.ts @@ -0,0 +1,24 @@ +import { cva, type VariantProps } from 'class-variance-authority' + +export const boxVariants = cva( + 'shadow-md/5 bg-background bg-gradient-to-b from-transparent to-foreground/[.03] dark:to-foreground/5 border border-foreground/10 rounded-xl p-5 outline-none relative before:absolute after:absolute', + { + variants: { + raised: { + single: [ + 'mb-3', + 'before:inset-x-2.5 before:h-[10px] before:bg-background before:-bottom-[11px] before:rounded-b-xl before:border-x before:border-b before:border-foreground/10 before:z-[-1] before:shadow-md/5 before:opacity-60 dark:before:opacity-100', + ], + double: [ + 'mb-5', + 'before:inset-x-2.5 before:h-[10px] before:bg-background before:-bottom-[11px] before:rounded-b-xl before:border-x before:border-b before:border-foreground/10 before:z-[-1] before:shadow-md/5 before:opacity-60 dark:before:opacity-100', + 'after:inset-x-5 after:h-[10px] after:bg-background after:-bottom-[21px] after:rounded-b-xl after:border-x after:border-b after:border-foreground/10 after:z-[-2] after:shadow-md/5 after:opacity-50 dark:after:opacity-90', + ], + }, + }, + }, +) + +export type BoxVariants = { + raised?: VariantProps['raised'] +} diff --git a/fe/src/components/ui/styles/breadcrumb.styles.ts b/fe/src/components/ui/styles/breadcrumb.styles.ts new file mode 100644 index 0000000..8cf4c5d --- /dev/null +++ b/fe/src/components/ui/styles/breadcrumb.styles.ts @@ -0,0 +1,4 @@ +export const breadcrumbList = + '[&>svg]:size-4 [&>svg]:stroke-[1.3] flex items-center gap-1.5 text-foreground/70' +export const breadcrumbItem = 'hover:text-foreground last:text-foreground' +export const breadcrumbLink = 'cursor-pointer [&>svg]:size-4 [&>svg]:stroke-[1.5]' diff --git a/fe/src/components/ui/styles/button.styles.ts b/fe/src/components/ui/styles/button.styles.ts new file mode 100644 index 0000000..a8db2d5 --- /dev/null +++ b/fe/src/components/ui/styles/button.styles.ts @@ -0,0 +1,137 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +export const buttonVariants = cva( + [ + "shadow-md/5 isolate relative py-2 cursor-pointer inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-3.5 [&_svg]:stroke-2 [&_svg]:shrink-0", + "before:absolute after:absolute before:z-[-1] after:z-[-1] before:rounded-[inherit] after:rounded-[inherit]", + ], + { + variants: { + variant: { + ghost: + "after:from-transparent bg-transparent hover:bg-foreground/5 text-foreground", + primary: "after:from-primary/40 bg-primary text-primary-foreground", + secondary: + "after:from-secondary/40 bg-secondary text-secondary-foreground", + success: "after:from-success/40 bg-success text-success-foreground", + danger: "after:from-danger/40 bg-danger text-danger-foreground", + pending: "after:from-pending/40 bg-pending text-pending-foreground", + warning: "after:from-warning/40 bg-warning text-warning-foreground", + }, + size: { + sm: "h-9 rounded-(--radius) px-3 text-sm", + default: "h-10 rounded-(--radius) px-4 text-sm", + lg: "h-11 rounded-(--radius) px-5 text-base", + xl: "h-13 rounded-(--radius) px-6 text-base", + }, + look: { + filled: [ + "dark:outline-2 dark:outline-black/30", + "before:inset-0 before:bg-gradient-to-b before:from-white/40 before:to-white/[.05] hover:before:from-white/50", + "after:inset-[2px] after:bg-gradient-to-b after:to-white/[.08] after:rounded-[calc(var(--radius)*0.7)]", + "dark:before:from-black/[.5] dark:before:to-black/70 hover:dark:before:from-black/[.4]", + "dark:after:from-black/[.3] dark:after:to-black/[.1]", + ], + outline: + "before:bg-none before:border before:rounded-[inherit] before:inset-0 after:hidden bg-background", + flat: "hover:before:bg-white/5 before:inset-0 dark:before:inset-0 dark:before:bg-black/60 hover:dark:before:bg-black/55", + text: "before:hidden shadow-none bg-transparent", + }, + }, + compoundVariants: [ + { + variant: "ghost", + look: "flat", + class: "dark:before:bg-transparent", + }, + { + variant: "ghost", + look: "outline", + class: + "before:bg-foreground/10 hover:before:bg-foreground/15 dark:before:bg-foreground/[.15] hover:dark:before:bg-foreground/20 before:border-foreground/40 dark:before:border-foreground/20 text-foreground", + }, + { + variant: "primary", + look: "outline", + class: + "before:bg-primary/10 hover:before:bg-primary/15 dark:before:bg-primary/[.15] hover:dark:before:bg-primary/20 before:border-primary/40 dark:before:border-primary/20 text-primary", + }, + { + variant: "secondary", + look: "outline", + class: + "before:bg-secondary/10 hover:before:bg-secondary/15 dark:before:bg-secondary/[.15] hover:dark:before:bg-secondary/20 before:border-secondary/40 dark:before:border-secondary/20 text-secondary", + }, + { + variant: "success", + look: "outline", + class: + "before:bg-success/10 hover:before:bg-success/15 dark:before:bg-success/[.15] hover:dark:before:bg-success/20 before:border-success/40 dark:before:border-success/20 text-success", + }, + { + variant: "danger", + look: "outline", + class: + "before:bg-danger/10 hover:before:bg-danger/15 dark:before:bg-danger/[.15] hover:dark:before:bg-danger/20 before:border-danger/40 dark:before:border-danger/20 text-danger", + }, + { + variant: "pending", + look: "outline", + class: + "before:bg-pending/10 hover:before:bg-pending/15 dark:before:bg-pending/[.15] hover:dark:before:bg-pending/20 before:border-pending/40 dark:before:border-pending/20 text-pending", + }, + { + variant: "warning", + look: "outline", + class: + "before:bg-warning/10 hover:before:bg-warning/15 dark:before:bg-warning/[.15] hover:dark:before:bg-warning/20 before:border-warning/40 dark:before:border-warning/20 text-warning", + }, + { + variant: "ghost", + look: "text", + class: "text-foreground", + }, + { + variant: "primary", + look: "text", + class: "text-primary", + }, + { + variant: "secondary", + look: "text", + class: "text-secondary", + }, + { + variant: "success", + look: "text", + class: "text-success", + }, + { + variant: "danger", + look: "text", + class: "text-danger", + }, + { + variant: "pending", + look: "text", + class: "text-pending", + }, + { + variant: "warning", + look: "text", + class: "text-warning", + }, + ], + defaultVariants: { + variant: "primary", + size: "default", + look: "flat", + }, + } +); + +export type ButtonVariants = { + look?: VariantProps["look"]; + variant?: VariantProps["variant"]; + size?: VariantProps["size"]; +}; diff --git a/fe/src/components/ui/styles/carousel.styles.ts b/fe/src/components/ui/styles/carousel.styles.ts new file mode 100644 index 0000000..be919a2 --- /dev/null +++ b/fe/src/components/ui/styles/carousel.styles.ts @@ -0,0 +1,14 @@ +export const carouselRoot = "relative"; +export const carouselControl = + "absolute size-full flex place-content-between items-center"; +export const carouselPrevTrigger = + "rounded-full p-2.5 h-9 -ms-12 border border-foreground/15"; +export const carouselNextTrigger = + "rounded-full p-2.5 h-9 -me-12 border border-foreground/15"; +export const carouselIndicatorGroup = + "absolute inset-x-0 bottom-0 z-5 w-full flex gap-1.5 justify-center mb-6"; +export const carouselIndicator = + "size-3 border border-foreground/10 bg-background/40 rounded-full data-[current]:bg-background/70 data-[current]:border-foreground/20 data-[current]:shadow-md/5"; +export const carouselItemGroup = "size-full"; +export const carouselItem = + "relative size-full overflow-hidden [&>img]:absolute [&>img]:inset-0 [&>img]:size-full [&>img]:object-cover"; diff --git a/fe/src/components/ui/styles/chart.styles.ts b/fe/src/components/ui/styles/chart.styles.ts new file mode 100644 index 0000000..3a4730b --- /dev/null +++ b/fe/src/components/ui/styles/chart.styles.ts @@ -0,0 +1 @@ +export const chart = ""; diff --git a/fe/src/components/ui/styles/checkbox.styles.ts b/fe/src/components/ui/styles/checkbox.styles.ts new file mode 100644 index 0000000..3c61118 --- /dev/null +++ b/fe/src/components/ui/styles/checkbox.styles.ts @@ -0,0 +1,7 @@ +export const checkboxRoot = "flex items-center gap-3"; +export const checkboxLabel = ""; +export const checkboxControl = + "bg-background relative size-4 border shadow-md/5 border-foreground/15 dark:border-foreground/25 rounded-md"; +export const checkboxIndicator = + "[&>svg]:absolute [&>svg]:inset-0 [&>svg]:m-auto [&>svg]:size-3 [&>svg]:stroke-3"; +export const checkboxHiddenInput = ""; diff --git a/fe/src/components/ui/styles/combobox.styles.ts b/fe/src/components/ui/styles/combobox.styles.ts new file mode 100644 index 0000000..6f48b60 --- /dev/null +++ b/fe/src/components/ui/styles/combobox.styles.ts @@ -0,0 +1,18 @@ +export const comboboxRoot = + "w-full flex flex-col gap-2.5 [&[data-multiple=true]_[data-part=clear-trigger]]:block"; +export const comboboxLabel = ""; +export const comboboxControl = "relative"; +export const comboboxInput = "flex-none"; +export const comboboxTrigger = + "font-normal text-foreground/70 w-full border border-foreground/15 bg-background [&>div]:truncate [&>svg]:ms-auto"; +export const comboboxClearTrigger = + "text-danger/90 cursor-pointer text-xs hidden ms-1"; +export const comboboxPositioner = "!z-50 !w-auto"; +export const comboboxContent = + "min-w-(--reference-width) p-0 z-(--layer-index) [&>div]:p-4 [&>div]:max-h-78 [&>div]:overflow-y-auto [&>div]:flex [&>div]:flex-col [&>div]:gap-5"; +export const comboboxItemGroup = "flex flex-col gap-3"; +export const comboboxItemGroupLabel = "px-1 text-xs opacity-70 mb-0.5"; +export const comboboxItem = + "flex items-center py-1 px-2.5 -mx-1.5 -my-1 hover:bg-foreground/[.04] rounded-lg cursor-pointer"; +export const comboboxItemText = ""; +export const comboboxItemIndicator = "ms-auto opacity-70"; diff --git a/fe/src/components/ui/styles/datepicker.styles.ts b/fe/src/components/ui/styles/datepicker.styles.ts new file mode 100644 index 0000000..2714064 --- /dev/null +++ b/fe/src/components/ui/styles/datepicker.styles.ts @@ -0,0 +1,34 @@ +export const datePickerRoot = "w-full flex flex-col gap-2.5"; +export const datePickerLabel = ""; +export const datePickerControl = "flex gap-2.5"; +export const datePickerInput = ""; +export const datePickerTrigger = "border border-foreground/15 bg-background"; +export const datePickerClearTrigger = + "border border-foreground/15 bg-background"; +export const datePickerPositioner = "!z-50 !w-auto"; +export const datePickerContent = + "min-w-(--reference-width) p-0 z-(--layer-index) [&>div]:p-4 [&>div]:grid [&>div]:grid-cols-2 [&>div]:gap-x-2.5 [&>div]:gap-y-3 [&>div>div]:col-span-2"; +export const datePickerYearSelect = + "shadow-none w-auto inline px-3 py-1.5 h-auto mb-2.5"; +export const datePickerMonthSelect = + "shadow-none w-auto inline px-3 py-1.5 h-auto mb-2.5"; +export const datePickerView = "flex flex-col gap-2"; +export const datePickerViewControl = "flex"; +export const datePickerPresetTrigger = + "border border-foreground/15 bg-background"; +export const datePickerPrevTrigger = + "px-2.5 py-1.5 rounded-lg hover:bg-foreground/5 cursor-pointer [&>svg]:size-4 [&>svg]:stroke-[1.5]"; +export const datePickerViewTrigger = + "mx-auto px-4 py-1.5 rounded-lg hover:bg-foreground/5 cursor-pointer [&>svg]:size-4 [&>svg]:stroke-[1.5]"; +export const datePickerNextTrigger = + "px-2.5 py-1.5 rounded-lg hover:bg-foreground/5 cursor-pointer [&>svg]:size-4 [&>svg]:stroke-[1.5]"; +export const datePickerRangeText = "mx-auto font-medium"; +export const datePickerTable = + "w-full text-center border-separate border-spacing-y-2 border-spacing-x-1.5"; +export const datePickerTableHead = ""; +export const datePickerTableRow = ""; +export const datePickerTableHeader = "font-normal opacity-70"; +export const datePickerTableBody = ""; +export const datePickerTableCell = ""; +export const datePickerTableCellTrigger = + "px-1.5 py-1 rounded-lg hover:bg-foreground/5 cursor-pointer data-[selected]:bg-foreground/70 data-[in-range]:bg-foreground/10 data-[selected]:text-background data-[disabled]:opacity-70"; diff --git a/fe/src/components/ui/styles/dialog.styles.ts b/fe/src/components/ui/styles/dialog.styles.ts new file mode 100644 index 0000000..8143c71 --- /dev/null +++ b/fe/src/components/ui/styles/dialog.styles.ts @@ -0,0 +1,14 @@ +export const dialogTrigger = ""; +export const dialogBackdrop = + "fixed inset-0 bg-black/80 z-70 [&[data-state='open']]:animate-in [&[data-state='open']]:fade-in-0 [&[data-state='closed']]:animate-out [&[data-state='closed']]:fade-out-0"; +export const dialogPositioner = ""; +export const dialogContent = [ + "px-6 pt-6 pb-7 rounded-2xl outline-none backdrop-blur-lg fixed top-[50%] left-[50%] z-70 w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] sm:max-w-lg", + "[&[data-state='open']]:animate-in [&[data-state='open']]:fade-in-0 [&[data-state='open']]:zoom-in-80 [&[data-state='open']]:duration-250", + "[&[data-state='closed']]:animate-out [&[data-state='closed']]:fade-out-0 [&[data-state='closed']]:zoom-out-80 [&[data-state='closed']]:duration-400", + "before:mx-2.5 after:mx-3.5", +]; +export const dialogTitle = "font-medium text-base relative -mt-.5"; +export const dialogDescription = "opacity-80 py-2 relative"; +export const dialogCloseTrigger = + "absolute right-0 top-0 p-0 size-8 rounded-full -mt-2 -me-2 border border-foreground/10 bg-background dark:bg-background hover:bg-background before:hidden dark:before:block before:absolute before:-inset-px before:bg-background dark:before:bg-foreground/20 before:z-[-1] before:rounded-full"; diff --git a/fe/src/components/ui/styles/field.styles.ts b/fe/src/components/ui/styles/field.styles.ts new file mode 100644 index 0000000..51a50ab --- /dev/null +++ b/fe/src/components/ui/styles/field.styles.ts @@ -0,0 +1,51 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +// Styles +export const fieldVariants = cva( + "data-[invalid=true]:text-danger gap-2 group/field flex w-full", + { + variants: { + orientation: { + vertical: "flex-col *:w-full [&>.sr-only]:w-auto", + horizontal: + "flex-row items-center *:data-[part=field-label]:flex-auto has-[>[data-part=field-content]]:items-start has-[>[data-part=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + responsive: + "flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:*:data-[part=field-label]:flex-auto @md/field-group:has-[>[data-part=field-content]]:items-start @md/field-group:has-[>[data-part=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", + }, + }, + defaultVariants: { + orientation: "vertical", + }, + } +); +export const fieldContent = + "gap-0.5 group/field-content flex flex-1 flex-col leading-snug"; +export const fieldDescription = [ + "text-foreground/70 text-left text-sm [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-data-horizontal/field:text-balance", + "last:mt-0 nth-last-2:-mt-1", + "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", +]; +export const fieldError = + "text-danger text-sm font-normal [&>ul]:ml-4 [&>ul]:flex [&>ul]:list-disc [&>ul]:flex-col [&>ul]:gap-1"; +export const fieldGroup = + "gap-5 data-[part=checkbox-group]:gap-3 *:data-[part=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col"; +export const fieldLabel = [ + "has-data-[state='checked']:bg-foreground/5 has-data-[state='checked']:border-foreground/30 dark:has-data-[state='checked']:border-foreground/20 dark:has-data-[state='checked']:bg-foreground/10 gap-2 group-data-[disabled=true]/field:opacity-50 has-[>[data-part=field]]:rounded-(--radius) has-[>[data-part=field]]:border has-[>[data-part=field]]:border-foreground/15 *:data-[part=field]:p-3.5 group/field-label peer/field-label flex w-fit leading-snug", + "has-[>[data-part=field]]:w-full has-[>[data-part=field]]:flex-col", +]; +export const fieldLegend = + "mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base"; +export const fieldSeparator = [ + "-my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2 relative", + "[&>[data-part='separator']]:absolute [&>[data-part='separator']]:inset-0 [&>[data-part='separator']]:top-1/2", + "[&>[data-part='field-separator-content']]:text-foreground/70 [&>[data-part='field-separator-content']]:px-2 [&>[data-part='field-separator-content']]:bg-background [&>[data-part='field-separator-content']]:relative [&>[data-part='field-separator-content']]:mx-auto [&>[data-part='field-separator-content']]:block [&>[data-part='field-separator-content']]:w-fit", +]; +export const fieldSet = + "gap-4 has-[>[data-part=checkbox-group]]:gap-3 has-[>[data-part=radio-group]]:gap-3 flex flex-col"; +export const fieldTitle = + "gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50 flex w-fit items-center leading-snug"; + +// Types +export type FieldVariants = { + orientation?: VariantProps["orientation"]; +}; diff --git a/fe/src/components/ui/styles/input.styles.ts b/fe/src/components/ui/styles/input.styles.ts new file mode 100644 index 0000000..874a418 --- /dev/null +++ b/fe/src/components/ui/styles/input.styles.ts @@ -0,0 +1,5 @@ +export const input = [ + 'file:text-foreground placeholder:text-foreground/80 selection:bg-foreground/70 selection:text-background flex h-10 w-full min-w-0 rounded-xl border border-foreground/15 backdrop-blur-lg bg-background px-3 py-1 text-base shadow-sm/5 transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', + 'focus-visible:border-ring focus-visible:ring-foreground/10 focus-visible:ring-[3px]', + 'aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40 aria-invalid:border-danger', +] diff --git a/fe/src/components/ui/styles/label.styles.ts b/fe/src/components/ui/styles/label.styles.ts new file mode 100644 index 0000000..3b0b652 --- /dev/null +++ b/fe/src/components/ui/styles/label.styles.ts @@ -0,0 +1 @@ +export const label = "font-medium"; diff --git a/fe/src/components/ui/styles/map.styles.ts b/fe/src/components/ui/styles/map.styles.ts new file mode 100644 index 0000000..5289f24 --- /dev/null +++ b/fe/src/components/ui/styles/map.styles.ts @@ -0,0 +1,9 @@ +export const map = [ + "data-[part='root']:relative data-[part='root']:contrast-105 data-[part='root']:grayscale dark:data-[part='root']:contrast-[.9] dark:data-[part='root']:invert", + "dark:[&_[data-part='controls']]:invert [&_[data-part='controls']]:absolute [&_[data-part='controls']]:top-2 [&_[data-part='controls']]:right-2 [&_[data-part='controls']]:flex [&_[data-part='controls']]:flex-col [&_[data-part='controls']]:gap-1 [&_[data-part='controls']]:z-50", + "[&_[data-part='zoom-in']]:bg-background", + "[&_[data-part='zoom-out']]:bg-background", + "[&_[data-part='reset-north']]:bg-background", + "[&_[data-part='locate']]:bg-background", + "[&_[data-part='toggle-fullscreen']]:bg-background", +]; diff --git a/fe/src/components/ui/styles/menu.styles.ts b/fe/src/components/ui/styles/menu.styles.ts new file mode 100644 index 0000000..43353b2 --- /dev/null +++ b/fe/src/components/ui/styles/menu.styles.ts @@ -0,0 +1,33 @@ +export const menuRoot = "w-full"; +export const menuTrigger = + "w-full font-normal border border-foreground/15 bg-background"; +export const menuIndicator = "ms-auto transition data-[state=open]:rotate-180"; +export const menuPositioner = "!z-50"; +export const menuContent = + "min-w-46 w-(--reference-width) p-0 z-(--layer-index) [&>div]:flex [&>div]:flex-col [&>div]:gap-2.5 [&>div]:p-4 [&>div]:max-h-78 [&>div]:overflow-y-auto"; +export const menuItem = [ + // Default + "[&>div:nth-of-type(1)]:flex [&>div:nth-of-type(1)]:items-center [&>div:nth-of-type(1)]:gap-2.5 relative flex items-center py-1 px-2.5 -mx-1.5 -my-1 hover:bg-foreground/[.04] rounded-lg cursor-pointer", + + // Nested menu chevron + "[&_[data-part='nested-menu-chevron']]:size-4 [&_[data-part='nested-menu-chevron']]:stroke-[1.3] [&_[data-part='nested-menu-chevron']]:-me-1 [&_[data-part='nested-menu-chevron']]:ms-auto", + + // Shortcut + "[&>div:nth-of-type(2)]:text-foreground/50 [&>div:nth-of-type(2)]:text-xs [&>div:nth-of-type(2)]:ms-auto", + + // Disabled + "data-disabled:opacity-50 data-disabled:hover:bg-background data-disabled:cursor-not-allowed", + + // Checkbox + "[&[data-type=checkbox]_[data-part='item-indicator']_svg]:stroke-3 [&[data-type=checkbox]>div:nth-of-type(1)]:before:absolute [&[data-type=checkbox]>div:nth-of-type(1)]:before:size-4 [&[data-type=checkbox]>div:nth-of-type(1)]:before:border [&[data-type=checkbox]>div:nth-of-type(1)]:before:shadow-md/5 [&[data-type=checkbox]>div:nth-of-type(1)]:before:border-foreground/15 [&[data-type=checkbox]>div:nth-of-type(1)]:before:rounded-md [&[data-type=checkbox]>div:nth-of-type(1)]:before:inset-y-0 [&[data-type=checkbox]>div:nth-of-type(1)]:before:my-auto [&[data-type=checkbox]>div:nth-of-type(1)]:before:start-2", + + // Radio + "[&[data-type=radio]_[data-part='item-indicator']_svg]:stroke-10 [&[data-type=radio]>div:nth-of-type(1)]:before:absolute [&[data-type=radio]>div:nth-of-type(1)]:before:size-4 [&[data-type=radio]>div:nth-of-type(1)]:before:border [&[data-type=radio]>div:nth-of-type(1)]:before:shadow-md/5 [&[data-type=radio]>div:nth-of-type(1)]:before:border-foreground/15 [&[data-type=radio]>div:nth-of-type(1)]:before:rounded-full [&[data-type=radio]>div:nth-of-type(1)]:before:inset-y-0 [&[data-type=radio]>div:nth-of-type(1)]:before:my-auto [&[data-type=radio]>div:nth-of-type(1)]:before:start-2", + + // Checkbox & radio svg + "has-[[data-part='item-indicator']]:ps-9 [&_[data-part='item-indicator']]:absolute [&_[data-part='item-indicator']]:start-2.5 [&_[data-part='item-indicator']_svg]:size-3", +]; +export const menuRadioItemGroup = "flex flex-col gap-3"; +export const menuItemGroupLabel = + "-mx-4 px-4 -mt-4 py-2.5 border-b border-foreground/10 text-foreground/70"; +export const menuSeparator = "border-foreground/10 -mx-4 my-1"; diff --git a/fe/src/components/ui/styles/native-select.styles.ts b/fe/src/components/ui/styles/native-select.styles.ts new file mode 100644 index 0000000..3e96a2f --- /dev/null +++ b/fe/src/components/ui/styles/native-select.styles.ts @@ -0,0 +1,3 @@ +export const nativeSelect = "appearance-none text-foreground/70"; +export const nativeSelectOption = ""; +export const NativeSelectOptGroup = ""; diff --git a/fe/src/components/ui/styles/pagination.styles.ts b/fe/src/components/ui/styles/pagination.styles.ts new file mode 100644 index 0000000..c5c3b28 --- /dev/null +++ b/fe/src/components/ui/styles/pagination.styles.ts @@ -0,0 +1,9 @@ +export const paginationRoot = 'flex gap-1' +export const paginationItem = [ + 'h-10 px-4 py-2 inline-flex items-center justify-center rounded-xl cursor-pointer hover:bg-foreground/5', + 'data-[selected]:border data-[selected]:bg-background data-[selected]:border-foreground/10 data-[selected]:font-medium data-[selected]:shadow-md/5', + 'data-[disabled]:opacity-70', +] +export const paginationPrevTrigger = paginationItem +export const paginationNextTrigger = paginationItem +export const paginationEllipsis = paginationItem diff --git a/fe/src/components/ui/styles/popover.styles.ts b/fe/src/components/ui/styles/popover.styles.ts new file mode 100644 index 0000000..65a6aba --- /dev/null +++ b/fe/src/components/ui/styles/popover.styles.ts @@ -0,0 +1,13 @@ +export const popoverRoot = '' +export const popoverTrigger = 'border border-foreground/15 bg-background' +export const popoverPositioner = '!z-50' +export const popoverContent = 'min-w-46 w-(--reference-width) z-(--layer-index)' +export const popoverArrow = '[--arrow-size:10px]' +export const popoverArrowTip = [ + '[--arrow-background:var(--color-background)] rounded-tl-sm', + 'before:absolute before:inset-0 before:bg-background dark:before:bg-background/10 before:rounded-tl-sm before:border-s before:border-t before:border-foreground/10', +] +export const popoverTitle = 'font-medium text-base' +export const popoverDescription = 'opacity-80 py-1 relative' +export const popoverIndicator = 'ms-auto transition data-[state=open]:rotate-180' +export const popoverCloseTrigger = '' diff --git a/fe/src/components/ui/styles/progress-circular.styles.ts b/fe/src/components/ui/styles/progress-circular.styles.ts new file mode 100644 index 0000000..3339881 --- /dev/null +++ b/fe/src/components/ui/styles/progress-circular.styles.ts @@ -0,0 +1,8 @@ +export const progressRoot = "flex flex-col items-center w-full gap-2.5"; +export const progressLabel = ""; +export const progressValueText = "text-xs font-medium flex justify-center"; +export const progressCircle = + "aspect-square [--size:100%] [--thickness:calc(var(--spacing)*1.5)]"; +export const progressCircleTrack = "stroke-foreground/10"; +export const progressCircleRange = + "[stroke-linecap:round] stroke-foreground/70 transition-all duration-500"; diff --git a/fe/src/components/ui/styles/progress-linear.styles.ts b/fe/src/components/ui/styles/progress-linear.styles.ts new file mode 100644 index 0000000..ee1083f --- /dev/null +++ b/fe/src/components/ui/styles/progress-linear.styles.ts @@ -0,0 +1,6 @@ +export const progressRoot = "flex flex-col items-center w-full gap-2.5"; +export const progressLabel = ""; +export const progressValueText = "text-xs font-medium flex justify-center"; +export const progressTrack = "bg-foreground/10 h-1.5 w-full rounded-full"; +export const progressRange = + "bg-foreground/70 h-full rounded-full transition-all duration-500"; diff --git a/fe/src/components/ui/styles/radio-group.styles.ts b/fe/src/components/ui/styles/radio-group.styles.ts new file mode 100644 index 0000000..6d0f3c8 --- /dev/null +++ b/fe/src/components/ui/styles/radio-group.styles.ts @@ -0,0 +1,9 @@ +export const radioGroupRoot = "flex flex-col gap-2.5"; +export const radioGroupLabel = ""; +export const radioGroupIndicator = + "left-(--left) top-(--top) h-(--height) w-4 [&>svg]:absolute [&>svg]:inset-0 [&>svg]:m-auto [&>svg]:size-3 [&>svg]:stroke-10"; +export const radioGroupItem = "relative flex items-center gap-3"; +export const radioGroupItemText = ""; +export const radioGroupItemControl = + "bg-background size-4 border shadow-md/5 border-foreground/15 dark:border-foreground/25 rounded-full"; +export const radioGroupItemHiddenInput = ""; diff --git a/fe/src/components/ui/styles/scroll-area.styles.ts b/fe/src/components/ui/styles/scroll-area.styles.ts new file mode 100644 index 0000000..0e488bf --- /dev/null +++ b/fe/src/components/ui/styles/scroll-area.styles.ts @@ -0,0 +1,16 @@ +export const scrollAreaRoot = ""; +export const scrollAreaViewport = "h-full [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"; +export const scrollAreaScrollbar = [ + "flex relative bg-foreground/5 rounded-md m-2 opacity-0 transition-opacity duration-150 pointer-events-none", + "before:content-[''] before:absolute", + "data-[scrolling]:duration-[0ms]", + "data-[hover]:opacity-100 data-[scrolling]:opacity-100 data-[hover]:pointer-events-auto data-[scrolling]:pointer-events-auto", + "data-[orientation=vertical]:w-2 data-[orientation=vertical]:before:w-5 data-[orientation=vertical]:before:h-full data-[orientation=vertical]:before:left-1/2 data-[orientation=vertical]:before:-translate-x-1/2 data-[orientation=vertical]:[&:not([data-overflow-y])]:hidden", + "data-[orientation=horizontal]:h-2 data-[orientation=horizontal]:before:w-full data-[orientation=horizontal]:before:h-5 data-[orientation=horizontal]:before:left-0 data-[orientation=horizontal]:before:right-0 data-[orientation=horizontal]:before:bottom-[-0.5rem] data-[orientation=horizontal]:[&:not([data-overflow-x])]:hidden", +]; +export const scrollAreaThumb = + "w-full rounded-[inherit] bg-foreground/20 data-[orientation=horizontal]:w-auto data-[orientation=horizontal]:h-full"; +export const scrollAreaContent = ""; +export const scrollAreaCorner = "bg-transparent"; + + diff --git a/fe/src/components/ui/styles/select.styles.ts b/fe/src/components/ui/styles/select.styles.ts new file mode 100644 index 0000000..1f8a7aa --- /dev/null +++ b/fe/src/components/ui/styles/select.styles.ts @@ -0,0 +1,21 @@ +export const selectRoot = + "w-full flex flex-col gap-2.5 [&[data-multiple=true]_[data-part=clear-trigger]]:block"; +export const selectLabel = ""; +export const selectControl = "relative"; +export const selectTrigger = + "w-full font-normal text-foreground/70 border border-foreground/15 bg-background"; +export const selectValueText = "truncate"; +export const selectIndicator = + "ms-auto transition data-[state=open]:rotate-180"; +export const selectClearTrigger = + "text-danger/90 cursor-pointer text-xs hidden ms-1"; +export const selectPositioner = "!z-50 !w-auto"; +export const selectContent = + "min-w-(--reference-width) p-0 z-(--layer-index) [&>div]:p-4 [&>div]:max-h-78 [&>div]:overflow-y-auto [&>div]:flex [&>div]:flex-col [&>div]:gap-5"; +export const selectItemGroup = "flex flex-col gap-3"; +export const selectItemGroupLabel = "px-1 text-xs opacity-70 mb-0.5"; +export const selectItem = + "flex items-center py-1 px-2.5 -mx-1.5 -my-1 hover:bg-foreground/[.04] rounded-lg cursor-pointer"; +export const selectItemText = ""; +export const selectItemIndicator = "ms-auto opacity-70"; +export const selectHiddenSelect = ""; diff --git a/fe/src/components/ui/styles/separator.style.ts b/fe/src/components/ui/styles/separator.style.ts new file mode 100644 index 0000000..b00bbe5 --- /dev/null +++ b/fe/src/components/ui/styles/separator.style.ts @@ -0,0 +1,2 @@ +export const separator = + "bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch"; diff --git a/fe/src/components/ui/styles/separator.styles.ts b/fe/src/components/ui/styles/separator.styles.ts new file mode 100644 index 0000000..050061b --- /dev/null +++ b/fe/src/components/ui/styles/separator.styles.ts @@ -0,0 +1,2 @@ +export const separator = + "bg-foreground/15 shrink-0 data-[orientation='horizontal']:h-px data-[orientation='horizontal']:w-full data-[orientation='vertical']:w-px data-[orientation='vertical']:self-stretch"; diff --git a/fe/src/components/ui/styles/sheet.styles.ts b/fe/src/components/ui/styles/sheet.styles.ts new file mode 100644 index 0000000..d14cf58 --- /dev/null +++ b/fe/src/components/ui/styles/sheet.styles.ts @@ -0,0 +1,30 @@ +export const sheetTrigger = ""; +export const sheetBackdrop = + "fixed inset-0 bg-black/80 z-70 [&[data-state='open']]:animate-in [&[data-state='open']]:fade-in-0 [&[data-state='closed']]:animate-out [&[data-state='closed']]:fade-out-0"; +export const sheetPositioner = ""; +export const sheetContent = [ + "px-6 pt-6 pb-7 rounded-2xl outline-none backdrop-blur-lg fixed z-70", + "[&[data-state='open']]:animate-in [&[data-state='open']]:fade-in-0 [&[data-state='open']]:duration-250", + "[&[data-state='closed']]:animate-out [&[data-state='closed']]:fade-out-0 [&[data-state='closed']]:duration-400", + "before:mx-2.5 after:mx-3.5", + + // Right + "[&[data-side='right']]:inset-y-3 [&[data-side='right']]:right-3 [&[data-side='right']]:w-3/4 [&[data-side='right']]:sm:max-w-sm", + "[&[data-side='right'][data-state='closed']]:slide-out-to-right [&[data-side='right'][data-state='open']]:slide-in-from-right", + + // Left + "[&[data-side='left']]:inset-y-3 [&[data-side='left']]:left-3 [&[data-side='left']]:w-3/4 [&[data-side='left']]:sm:max-w-sm", + "[&[data-side='left'][data-state='closed']]:slide-out-to-left [&[data-side='left'][data-state='open']]:slide-in-from-left", + + // Top + "[&[data-side='top']]:inset-x-3 [&[data-side='top']]:top-3", + "[&[data-side='top'][data-state='closed']]:slide-out-to-top [&[data-side='top'][data-state='open']]:slide-in-from-top", + + // Bottom + "[&[data-side='bottom']]:inset-x-3 [&[data-side='bottom']]:bottom-3", + "[&[data-side='bottom'][data-state='closed']]:slide-out-to-bottom [&[data-side='bottom'][data-state='open']]:slide-in-from-bottom", +]; +export const sheetTitle = "font-medium text-lg relative -mt-.5"; +export const sheetDescription = "opacity-80 py-2 relative"; +export const sheetCloseTrigger = + "absolute right-0 top-0 p-0 size-8 rounded-full -mt-2 -me-2 border border-foreground/10 bg-background dark:bg-background hover:bg-background before:hidden dark:before:block before:absolute before:-inset-px before:bg-background dark:before:bg-foreground/20 before:z-[-1] before:rounded-full"; diff --git a/fe/src/components/ui/styles/slider.styles.ts b/fe/src/components/ui/styles/slider.styles.ts new file mode 100644 index 0000000..0503d33 --- /dev/null +++ b/fe/src/components/ui/styles/slider.styles.ts @@ -0,0 +1,14 @@ +export const sliderRoot = "w-full flex flex-col gap-3"; +export const sliderLabel = ""; +export const sliderValueText = "text-xs font-medium flex justify-center"; +export const sliderControl = ""; +export const sliderTrack = "bg-foreground/10 h-1.5 w-full rounded-full"; +export const sliderRange = "h-full absolute bg-foreground/70 rounded-full"; +export const sliderThumb = + "bg-background size-4 rounded-full inset-y-0 my-auto shadow-md/5 border-2 border-foreground/70"; +export const sliderHiddenInput = ""; +export const sliderMarkerGroup = "h-6"; +export const sliderMarker = [ + "text-xs opacity-80 relative mt-3.5", + "before:size-1 before:absolute before:inset-0 before:rounded-full before:bg-foreground/40 before:mx-auto before:-mt-3", +]; diff --git a/fe/src/components/ui/styles/switch.styles.ts b/fe/src/components/ui/styles/switch.styles.ts new file mode 100644 index 0000000..2298c92 --- /dev/null +++ b/fe/src/components/ui/styles/switch.styles.ts @@ -0,0 +1,7 @@ +export const switchRoot = "flex items-center gap-3"; +export const switchControl = + "relative block h-6 w-12 rounded-full bg-foreground/5 dark:bg-foreground/10 transition border border-foreground/10 data-[state=checked]:bg-foreground/30"; +export const switchThumb = + "data-[state=checked]:start-1/2 transition-all block absolute inset-y-0 start-0 w-1/2 before:absolute before:inset-0.5 before:rounded-full before:bg-background dark:before:bg-foreground/40 before:shadow-md"; +export const switchLabel = ""; +export const switchHiddenInput = ""; diff --git a/fe/src/components/ui/styles/table.styles.ts b/fe/src/components/ui/styles/table.styles.ts new file mode 100644 index 0000000..d7d6bc2 --- /dev/null +++ b/fe/src/components/ui/styles/table.styles.ts @@ -0,0 +1,63 @@ +import { cva, type VariantProps } from "class-variance-authority"; + +// Styles +export const tableContainer = "relative w-full overflow-x-auto pe-2 -me-2"; +export const tableVariants = cva("w-full caption-bottom", { + variants: { + variant: { + default: "", + boxed: "border-separate border-spacing-y-2.5", + }, + raised: { + single: "border-spacing-y-5", + double: "border-spacing-y-7", + }, + }, + defaultVariants: { + variant: "default", + }, +}); +export const tableHeader = "[&_tr]:border-b"; +export const tableBody = + "[&_tr:last-child]:border-0 [&_tr:hover]:bg-foreground/5"; +export const tableFooter = + "bg-foreground/5 border-t border-foreground/10 font-medium [&>tr]:last:border-b-0"; +export const tableHead = + "text-foreground h-11 px-4 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]"; +export const tableRow = + "data-[state=selected]:bg-foreground/5 border-b border-foreground/10 transition-colors"; +export const tableCellVariants = cva( + "px-4 py-3 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]", + { + variants: { + variant: { + default: "", + boxed: [ + "relative bg-background bg-gradient-to-b from-transparent to-foreground/[.03] dark:to-foreground/5 border-y first:border-s last:border-e border-foreground/10 first:rounded-s-xl last:rounded-e-xl shadow-md/5", + ], + }, + raised: { + single: + "before:absolute before:inset-x-0 first:before:start-2 last:before:end-2 before:h-2.5 before:bg-background/20 dark:before:bg-foreground/10 before:-bottom-2.5 first:before:rounded-bl-xl last:before:rounded-br-xl first:before:border-s last:before:border-e before:border-b before:border-foreground/10 before:z-[-1] before:shadow-md/5 before:opacity-60", + double: [ + "before:absolute before:inset-x-0 first:before:start-2 last:before:end-2 before:h-2.5 before:bg-background/20 dark:before:bg-foreground/10 before:-bottom-2.5 first:before:rounded-bl-xl last:before:rounded-br-xl first:before:border-s last:before:border-e before:border-b before:border-foreground/10 before:z-[-1] before:shadow-md/5 before:opacity-60", + "after:absolute after:inset-x-0 first:after:start-4.5 last:after:end-4.5 after:h-[0.5rem] after:bg-background/20 dark:after:bg-foreground/10 after:-bottom-[1.1rem] first:after:rounded-bl-xl last:after:rounded-br-xl first:after:border-s last:after:border-e after:border-b after:border-foreground/10 after:z-[-1] after:shadow-md/5 after:opacity-40", + ], + }, + }, + defaultVariants: { + variant: "default", + }, + } +); +export const tableCaption = "text-foreground/70 mt-4 text-sm"; + +// Types +export type TableVariants = { + variant?: VariantProps["variant"]; + raised?: VariantProps["raised"]; +}; +export type TableCellVariants = { + variant?: VariantProps["variant"]; + raised?: VariantProps["raised"]; +}; diff --git a/fe/src/components/ui/styles/tabs.styles.ts b/fe/src/components/ui/styles/tabs.styles.ts new file mode 100644 index 0000000..94df93b --- /dev/null +++ b/fe/src/components/ui/styles/tabs.styles.ts @@ -0,0 +1,8 @@ +export const tabsRoot = "flex flex-col gap-2.5"; +export const tabsList = + "w-fit relative backdrop-blur-lg bg-foreground/5 dark:bg-background/10 border border-foreground/10 rounded-2xl p-1 [&>div]:gap-1 [&>div]:flex"; +export const tabsIndicator = + "h-(--height) w-(--width) start-(--left) top-(--top) bg-background dark:bg-foreground/10 rounded-xl shadow-md absolute"; +export const tabsTrigger = + "z-5 relative h-9 px-5 opacity-80 cursor-pointer rounded-xl font-medium text-sm data-[selected]:opacity-100 data-[selected]:hover:bg-transparent hover:bg-foreground/5"; +export const tabsContent = ""; diff --git a/fe/src/components/ui/styles/textarea.styles.ts b/fe/src/components/ui/styles/textarea.styles.ts new file mode 100644 index 0000000..f9ce6c0 --- /dev/null +++ b/fe/src/components/ui/styles/textarea.styles.ts @@ -0,0 +1,5 @@ +export const textarea = [ + 'placeholder:text-foreground/70 selection:bg-foreground/80 selection:text-background flex field-sizing-content min-h-16 w-full rounded-xl backdrop-blur-lg border border-foreground/15 bg-background px-3 py-2 text-base shadow-sm/5 transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', + 'focus-visible:border-ring focus-visible:ring-foreground/10', + 'aria-invalid:ring-danger/20 dark:aria-invalid:ring-danger/40 aria-invalid:border-danger', +] diff --git a/fe/src/components/ui/styles/toast.styles.ts b/fe/src/components/ui/styles/toast.styles.ts new file mode 100644 index 0000000..66f3d78 --- /dev/null +++ b/fe/src/components/ui/styles/toast.styles.ts @@ -0,0 +1,15 @@ +export const toastRoot = [ + "min-w-sm flex flex-col gap-1 relative", + "[translate:var(--x)_var(--y)] [scale:var(--scale)] [z-index:var(--z-index)]", + "[height:var(--height)] [opacity:var(--opacity)]", + "[will-change:translate,opacity,scale]", + "[transition:translate_400ms,scale_400ms,opacity_400ms,height_400ms,box-shadow_200ms]", + "[transition-timing-function:cubic-bezier(0.21,1.02,0.73,1)]", + "data-[state=closed]:[transition:translate_400ms,scale_400ms,opacity_200ms]", + "data-[state=closed]:[transition-timing-function:cubic-bezier(0.06,0.71,0.55,1)]", +]; +export const toastTitle = "font-medium text-nowrap"; +export const toastDescription = "text-nowrap opacity-80"; +export const toastCloseTrigger = + "absolute right-0 top-0 p-0 size-6 rounded-full -mt-1.5 -me-1.5 border border-foreground/10 bg-background dark:bg-background hover:bg-background before:hidden dark:before:block before:absolute before:-inset-px before:bg-background dark:before:bg-foreground/20 before:z-[-1] before:rounded-full"; +export const toasterContainer = ""; diff --git a/fe/src/components/ui/styles/tooltip.styles.ts b/fe/src/components/ui/styles/tooltip.styles.ts new file mode 100644 index 0000000..0067a9e --- /dev/null +++ b/fe/src/components/ui/styles/tooltip.styles.ts @@ -0,0 +1,9 @@ +export const tooltipTrigger = ""; +export const tooltipPositioner = "!z-50"; +export const tooltipContent = + "bg-background dark:bg-foreground/15 backdrop-blur-lg border border-foreground/15 shadow-md/5 rounded-xl px-4 py-2 relative"; +export const tooltipArrow = "[--arrow-size:10px]"; +export const tooltipArrowTip = [ + "[--arrow-background:var(--color-background)] rounded-tl-sm", + "before:absolute before:inset-0 before:bg-background dark:before:bg-foreground/[.17] before:rounded-tl-sm before:border-s before:border-t before:border-foreground/15", +]; diff --git a/fe/src/components/ui/switch/SwitchControl.vue b/fe/src/components/ui/switch/SwitchControl.vue new file mode 100644 index 0000000..7fc8b05 --- /dev/null +++ b/fe/src/components/ui/switch/SwitchControl.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/switch/SwitchHiddenInput.vue b/fe/src/components/ui/switch/SwitchHiddenInput.vue new file mode 100644 index 0000000..63ceb27 --- /dev/null +++ b/fe/src/components/ui/switch/SwitchHiddenInput.vue @@ -0,0 +1,21 @@ + + + diff --git a/fe/src/components/ui/switch/SwitchLabel.vue b/fe/src/components/ui/switch/SwitchLabel.vue new file mode 100644 index 0000000..560f114 --- /dev/null +++ b/fe/src/components/ui/switch/SwitchLabel.vue @@ -0,0 +1,28 @@ + + + diff --git a/fe/src/components/ui/switch/SwitchRoot.vue b/fe/src/components/ui/switch/SwitchRoot.vue new file mode 100644 index 0000000..2908240 --- /dev/null +++ b/fe/src/components/ui/switch/SwitchRoot.vue @@ -0,0 +1,36 @@ + + + diff --git a/fe/src/components/ui/switch/SwitchThumb.vue b/fe/src/components/ui/switch/SwitchThumb.vue new file mode 100644 index 0000000..340a118 --- /dev/null +++ b/fe/src/components/ui/switch/SwitchThumb.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/switch/index.ts b/fe/src/components/ui/switch/index.ts new file mode 100644 index 0000000..de96a60 --- /dev/null +++ b/fe/src/components/ui/switch/index.ts @@ -0,0 +1,5 @@ +export { default as SwitchRoot } from "./SwitchRoot.vue"; +export { default as SwitchControl } from "./SwitchControl.vue"; +export { default as SwitchThumb } from "./SwitchThumb.vue"; +export { default as SwitchLabel } from "./SwitchLabel.vue"; +export { default as SwitchHiddenInput } from "./SwitchHiddenInput.vue"; diff --git a/fe/src/components/ui/table/Table.vue b/fe/src/components/ui/table/Table.vue new file mode 100644 index 0000000..5623ba6 --- /dev/null +++ b/fe/src/components/ui/table/Table.vue @@ -0,0 +1,31 @@ + + + diff --git a/fe/src/components/ui/table/TableBody.vue b/fe/src/components/ui/table/TableBody.vue new file mode 100644 index 0000000..8f25b1a --- /dev/null +++ b/fe/src/components/ui/table/TableBody.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/table/TableCaption.vue b/fe/src/components/ui/table/TableCaption.vue new file mode 100644 index 0000000..fc9a022 --- /dev/null +++ b/fe/src/components/ui/table/TableCaption.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/table/TableCell.vue b/fe/src/components/ui/table/TableCell.vue new file mode 100644 index 0000000..6dd3a67 --- /dev/null +++ b/fe/src/components/ui/table/TableCell.vue @@ -0,0 +1,23 @@ + + + diff --git a/fe/src/components/ui/table/TableContainer.vue b/fe/src/components/ui/table/TableContainer.vue new file mode 100644 index 0000000..e3efd43 --- /dev/null +++ b/fe/src/components/ui/table/TableContainer.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/table/TableFooter.vue b/fe/src/components/ui/table/TableFooter.vue new file mode 100644 index 0000000..4d32838 --- /dev/null +++ b/fe/src/components/ui/table/TableFooter.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/table/TableHead.vue b/fe/src/components/ui/table/TableHead.vue new file mode 100644 index 0000000..553a361 --- /dev/null +++ b/fe/src/components/ui/table/TableHead.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/table/TableHeader.vue b/fe/src/components/ui/table/TableHeader.vue new file mode 100644 index 0000000..caa37d3 --- /dev/null +++ b/fe/src/components/ui/table/TableHeader.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/table/TableRow.vue b/fe/src/components/ui/table/TableRow.vue new file mode 100644 index 0000000..d1cfac8 --- /dev/null +++ b/fe/src/components/ui/table/TableRow.vue @@ -0,0 +1,14 @@ + + + diff --git a/fe/src/components/ui/table/index.ts b/fe/src/components/ui/table/index.ts new file mode 100644 index 0000000..5449b06 --- /dev/null +++ b/fe/src/components/ui/table/index.ts @@ -0,0 +1,9 @@ +export { default as Table } from "./Table.vue"; +export { default as TableContainer } from "./TableContainer.vue"; +export { default as TableHeader } from "./TableHeader.vue"; +export { default as TableBody } from "./TableBody.vue"; +export { default as TableFooter } from "./TableFooter.vue"; +export { default as TableHead } from "./TableHead.vue"; +export { default as TableRow } from "./TableRow.vue"; +export { default as TableCell } from "./TableCell.vue"; +export { default as TableCaption } from "./TableCaption.vue"; diff --git a/fe/src/components/ui/tabs/TabsContent.vue b/fe/src/components/ui/tabs/TabsContent.vue new file mode 100644 index 0000000..d9628ed --- /dev/null +++ b/fe/src/components/ui/tabs/TabsContent.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/tabs/TabsIndicator.vue b/fe/src/components/ui/tabs/TabsIndicator.vue new file mode 100644 index 0000000..6285d6f --- /dev/null +++ b/fe/src/components/ui/tabs/TabsIndicator.vue @@ -0,0 +1,27 @@ + + + diff --git a/fe/src/components/ui/tabs/TabsList.vue b/fe/src/components/ui/tabs/TabsList.vue new file mode 100644 index 0000000..a4fa4b8 --- /dev/null +++ b/fe/src/components/ui/tabs/TabsList.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/tabs/TabsRoot.vue b/fe/src/components/ui/tabs/TabsRoot.vue new file mode 100644 index 0000000..477344c --- /dev/null +++ b/fe/src/components/ui/tabs/TabsRoot.vue @@ -0,0 +1,32 @@ + + + diff --git a/fe/src/components/ui/tabs/TabsTrigger.vue b/fe/src/components/ui/tabs/TabsTrigger.vue new file mode 100644 index 0000000..2228b67 --- /dev/null +++ b/fe/src/components/ui/tabs/TabsTrigger.vue @@ -0,0 +1,29 @@ + + + diff --git a/fe/src/components/ui/tabs/index.ts b/fe/src/components/ui/tabs/index.ts new file mode 100644 index 0000000..b8bf09e --- /dev/null +++ b/fe/src/components/ui/tabs/index.ts @@ -0,0 +1,5 @@ +export { default as TabsContent } from "./TabsContent.vue"; +export { default as TabsIndicator } from "./TabsIndicator.vue"; +export { default as TabsList } from "./TabsList.vue"; +export { default as TabsRoot } from "./TabsRoot.vue"; +export { default as TabsTrigger } from "./TabsTrigger.vue"; diff --git a/fe/src/components/ui/textarea/Textarea.vue b/fe/src/components/ui/textarea/Textarea.vue new file mode 100644 index 0000000..b594852 --- /dev/null +++ b/fe/src/components/ui/textarea/Textarea.vue @@ -0,0 +1,12 @@ + + + diff --git a/fe/src/views/Categories.vue b/fe/src/views/Categories.vue new file mode 100644 index 0000000..b3e34c7 --- /dev/null +++ b/fe/src/views/Categories.vue @@ -0,0 +1,203 @@ + + + diff --git a/fe/src/views/Chat.vue b/fe/src/views/Chat.vue new file mode 100644 index 0000000..395eb3e --- /dev/null +++ b/fe/src/views/Chat.vue @@ -0,0 +1,2120 @@ + + + diff --git a/fe/src/views/DashboardOverview1.vue b/fe/src/views/DashboardOverview1.vue new file mode 100644 index 0000000..c59f55b --- /dev/null +++ b/fe/src/views/DashboardOverview1.vue @@ -0,0 +1,591 @@ + + + diff --git a/fe/src/views/ErrorPage.vue b/fe/src/views/ErrorPage.vue new file mode 100644 index 0000000..472080a --- /dev/null +++ b/fe/src/views/ErrorPage.vue @@ -0,0 +1,43 @@ + + + diff --git a/fe/src/views/FaqLayout1.vue b/fe/src/views/FaqLayout1.vue new file mode 100644 index 0000000..3bb5cd4 --- /dev/null +++ b/fe/src/views/FaqLayout1.vue @@ -0,0 +1,271 @@ + + + diff --git a/fe/src/views/FaqLayout2.vue b/fe/src/views/FaqLayout2.vue new file mode 100644 index 0000000..06aca3b --- /dev/null +++ b/fe/src/views/FaqLayout2.vue @@ -0,0 +1,232 @@ + + + diff --git a/fe/src/views/FaqLayout3.vue b/fe/src/views/FaqLayout3.vue new file mode 100644 index 0000000..f953ea3 --- /dev/null +++ b/fe/src/views/FaqLayout3.vue @@ -0,0 +1,271 @@ + + + diff --git a/fe/src/views/FileManager.vue b/fe/src/views/FileManager.vue new file mode 100644 index 0000000..4ef9dad --- /dev/null +++ b/fe/src/views/FileManager.vue @@ -0,0 +1,265 @@ + + + diff --git a/fe/src/views/Inbox.vue b/fe/src/views/Inbox.vue new file mode 100644 index 0000000..6558cf9 --- /dev/null +++ b/fe/src/views/Inbox.vue @@ -0,0 +1,296 @@ + + + diff --git a/fe/src/views/InvoiceLayout1.vue b/fe/src/views/InvoiceLayout1.vue new file mode 100644 index 0000000..f042087 --- /dev/null +++ b/fe/src/views/InvoiceLayout1.vue @@ -0,0 +1,137 @@ + + + diff --git a/fe/src/views/InvoiceLayout2.vue b/fe/src/views/InvoiceLayout2.vue new file mode 100644 index 0000000..c1423e6 --- /dev/null +++ b/fe/src/views/InvoiceLayout2.vue @@ -0,0 +1,142 @@ + + + diff --git a/fe/src/views/PointOfSale.vue b/fe/src/views/PointOfSale.vue new file mode 100644 index 0000000..4947f09 --- /dev/null +++ b/fe/src/views/PointOfSale.vue @@ -0,0 +1,333 @@ + + + diff --git a/fe/src/views/PricingLayout1.vue b/fe/src/views/PricingLayout1.vue new file mode 100644 index 0000000..745ab34 --- /dev/null +++ b/fe/src/views/PricingLayout1.vue @@ -0,0 +1,131 @@ + + + diff --git a/fe/src/views/PricingLayout2.vue b/fe/src/views/PricingLayout2.vue new file mode 100644 index 0000000..b5c1144 --- /dev/null +++ b/fe/src/views/PricingLayout2.vue @@ -0,0 +1,279 @@ + + + diff --git a/fe/src/views/ProductGrid.vue b/fe/src/views/ProductGrid.vue new file mode 100644 index 0000000..79eb6a0 --- /dev/null +++ b/fe/src/views/ProductGrid.vue @@ -0,0 +1,180 @@ + + + diff --git a/fe/src/views/ProductList.vue b/fe/src/views/ProductList.vue new file mode 100644 index 0000000..5624863 --- /dev/null +++ b/fe/src/views/ProductList.vue @@ -0,0 +1,202 @@ + + + diff --git a/fe/src/views/Reviews.vue b/fe/src/views/Reviews.vue new file mode 100644 index 0000000..200f902 --- /dev/null +++ b/fe/src/views/Reviews.vue @@ -0,0 +1,206 @@ + + + diff --git a/fe/src/views/SellerDetail.vue b/fe/src/views/SellerDetail.vue new file mode 100644 index 0000000..ce2dfd3 --- /dev/null +++ b/fe/src/views/SellerDetail.vue @@ -0,0 +1,287 @@ + + + diff --git a/fe/src/views/SellerList.vue b/fe/src/views/SellerList.vue new file mode 100644 index 0000000..9fb4650 --- /dev/null +++ b/fe/src/views/SellerList.vue @@ -0,0 +1,224 @@ + + + diff --git a/fe/src/views/TransactionDetail.vue b/fe/src/views/TransactionDetail.vue new file mode 100644 index 0000000..12ea5bd --- /dev/null +++ b/fe/src/views/TransactionDetail.vue @@ -0,0 +1,209 @@ + + + diff --git a/fe/src/views/TransactionList.vue b/fe/src/views/TransactionList.vue new file mode 100644 index 0000000..7a08e5f --- /dev/null +++ b/fe/src/views/TransactionList.vue @@ -0,0 +1,220 @@ + + + diff --git a/fe/src/views/UsersLayout1.vue b/fe/src/views/UsersLayout1.vue new file mode 100644 index 0000000..f31a555 --- /dev/null +++ b/fe/src/views/UsersLayout1.vue @@ -0,0 +1,168 @@ + + + diff --git a/fe/src/views/UsersLayout2.vue b/fe/src/views/UsersLayout2.vue new file mode 100644 index 0000000..f52f2cb --- /dev/null +++ b/fe/src/views/UsersLayout2.vue @@ -0,0 +1,108 @@ + + + diff --git a/fe/src/views/UsersLayout3.vue b/fe/src/views/UsersLayout3.vue new file mode 100644 index 0000000..f1bbe6d --- /dev/null +++ b/fe/src/views/UsersLayout3.vue @@ -0,0 +1,142 @@ + + + diff --git a/fe/src/views/WizardLayout1.vue b/fe/src/views/WizardLayout1.vue new file mode 100644 index 0000000..c4faaac --- /dev/null +++ b/fe/src/views/WizardLayout1.vue @@ -0,0 +1,76 @@ + + + diff --git a/fe/src/views/WizardLayout2.vue b/fe/src/views/WizardLayout2.vue new file mode 100644 index 0000000..35c9b0c --- /dev/null +++ b/fe/src/views/WizardLayout2.vue @@ -0,0 +1,97 @@ + + + diff --git a/fe/src/views/WizardLayout3.vue b/fe/src/views/WizardLayout3.vue new file mode 100644 index 0000000..b3c6df6 --- /dev/null +++ b/fe/src/views/WizardLayout3.vue @@ -0,0 +1,101 @@ + + + diff --git a/fe/tsconfig.app.json b/fe/tsconfig.app.json new file mode 100644 index 0000000..e8ef49c --- /dev/null +++ b/fe/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": [ + "env.d.ts", + "src/**/*", + "src/**/*.vue" + ], + "exclude": [ + "src/**/__tests__/*" + ], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "paths": { + "@/*": [ + "./src/*" + ], + "@mykopkb/core/*": [ + "./src/components/ui/*" + ], + } + } +} \ No newline at end of file diff --git a/fe/tsconfig.json b/fe/tsconfig.json new file mode 100644 index 0000000..07ba558 --- /dev/null +++ b/fe/tsconfig.json @@ -0,0 +1,17 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.node.json" + }, + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.vitest.json" + } + ], + "compilerOptions": { + "module": "NodeNext" + } +} \ No newline at end of file diff --git a/fe/tsconfig.node.json b/fe/tsconfig.node.json new file mode 100644 index 0000000..822562d --- /dev/null +++ b/fe/tsconfig.node.json @@ -0,0 +1,19 @@ +{ + "extends": "@tsconfig/node24/tsconfig.json", + "include": [ + "vite.config.*", + "vitest.config.*", + "cypress.config.*", + "nightwatch.conf.*", + "playwright.config.*", + "eslint.config.*" + ], + "compilerOptions": { + "noEmit": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + + "module": "ESNext", + "moduleResolution": "Bundler", + "types": ["node"] + } +} diff --git a/fe/tsconfig.vitest.json b/fe/tsconfig.vitest.json new file mode 100644 index 0000000..7d1d8ce --- /dev/null +++ b/fe/tsconfig.vitest.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.app.json", + "include": ["src/**/__tests__/*", "env.d.ts"], + "exclude": [], + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo", + + "lib": [], + "types": ["node", "jsdom"] + } +} diff --git a/fe/vite.config.ts b/fe/vite.config.ts new file mode 100644 index 0000000..8717bea --- /dev/null +++ b/fe/vite.config.ts @@ -0,0 +1,21 @@ +import { fileURLToPath, URL } from 'node:url' +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import vueDevTools from 'vite-plugin-vue-devtools' +import tailwindcss from '@tailwindcss/vite' + + +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + vue(), + vueDevTools(), + tailwindcss(), + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + '@mykopkb/core': fileURLToPath(new URL('./src/components/ui', import.meta.url)) + }, + }, +}) diff --git a/fe/vitest.config.ts b/fe/vitest.config.ts new file mode 100644 index 0000000..c328717 --- /dev/null +++ b/fe/vitest.config.ts @@ -0,0 +1,14 @@ +import { fileURLToPath } from 'node:url' +import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' +import viteConfig from './vite.config' + +export default mergeConfig( + viteConfig, + defineConfig({ + test: { + environment: 'jsdom', + exclude: [...configDefaults.exclude, 'e2e/**'], + root: fileURLToPath(new URL('./', import.meta.url)), + }, + }), +) diff --git a/my-kopkb.code-workspace b/my-kopkb.code-workspace new file mode 100644 index 0000000..ef9f5d2 --- /dev/null +++ b/my-kopkb.code-workspace @@ -0,0 +1,7 @@ +{ + "folders": [ + { + "path": "." + } + ] +} \ No newline at end of file