diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 000000000..67e35fc47 Binary files /dev/null and b/.DS_Store differ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..8f0de65c5 --- /dev/null +++ b/.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/.htaccess b/.htaccess new file mode 100644 index 000000000..68d45accb --- /dev/null +++ b/.htaccess @@ -0,0 +1,32 @@ + + + Options -MultiViews -Indexes + + + + Order Allow,Deny + Deny from all + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Handle Front Controller... + RewriteCond %{REQUEST_URI} !(\.css|\.js|\.png|\.jpg|\.jpeg|\.gif|\.pdf|robots\.txt|\.ico|\.woff|\.woff2|.ttf|\.svg)$ [NC] + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_URI} !^/public/ + RewriteRule ^(css|assets|market_assets|images|landing|uploads|storage|installer|js|vendor|build|screenshots)/(.*)$ public/$1/$2 [L,NC] + diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..df954ecde --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +resources/js/components/ui/* +resources/js/ziggy.js +resources/views/mail/* diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..f2264ac45 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,18 @@ +{ + "semi": true, + "singleQuote": true, + "singleAttributePerLine": false, + "htmlWhitespaceSensitivity": "css", + "printWidth": 150, + "plugins": ["prettier-plugin-organize-imports", "prettier-plugin-tailwindcss"], + "tailwindFunctions": ["clsx", "cn"], + "tabWidth": 4, + "overrides": [ + { + "files": "**/*.yml", + "options": { + "tabWidth": 2 + } + } + ] +} diff --git a/README.md b/README.md new file mode 100644 index 000000000..8fbd081b7 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# HRM \ No newline at end of file diff --git a/app/Console/Commands/AssignDefaultPlanToUsers.php b/app/Console/Commands/AssignDefaultPlanToUsers.php new file mode 100644 index 000000000..0112971d6 --- /dev/null +++ b/app/Console/Commands/AssignDefaultPlanToUsers.php @@ -0,0 +1,45 @@ +error(__('No default plan found. Please create a default plan first.')); + return 1; + } + + $count = User::where('type', 'company') + ->whereNull('plan_id') + ->update(['plan_id' => $defaultPlan->id, 'plan_is_active' => 1]); + + $this->info("Successfully assigned default plan to {$count} users."); + + return 0; + } +} \ No newline at end of file diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php new file mode 100644 index 000000000..f04b3d03b --- /dev/null +++ b/app/Console/Kernel.php @@ -0,0 +1,27 @@ +command('inspire')->hourly(); + } + + /** + * Register the commands for the application. + */ + protected function commands(): void + { + $this->load(__DIR__.'/Commands'); + + require base_path('routes/console.php'); + } +} \ No newline at end of file diff --git a/app/Events/UserCreated.php b/app/Events/UserCreated.php new file mode 100644 index 000000000..ad8d2ac38 --- /dev/null +++ b/app/Events/UserCreated.php @@ -0,0 +1,16 @@ +getSize(); + } + } + + return number_format($file_size / 1000000, 2); + } +} + +if (!function_exists('settings')) { + function settings($user_id = null) + { + // Skip database queries during installation + if (request()->is('install/*') || request()->is('update/*') || !file_exists(storage_path('installed'))) { + return []; + } + + if (is_null($user_id)) { + if (auth()->user()) { + if (isSaas()) { + if (!in_array(auth()->user()->type, ['superadmin', 'company'])) { + $autherUserCreatedBy = auth()->user()->created_by; + $user_id = getCompanyId($autherUserCreatedBy); + } else { + $user_id = auth()->id(); + } + } else { + // Non-SaaS: Company is top level + if (auth()->user()->type === 'company') { + $user_id = auth()->id(); + } else { + $createdBy = getCompanyId(auth()->id()); + $user_id = $createdBy; + } + } + } else { + if (isSaas()) { + $user = User::where('type', 'superadmin')->first(); + } else { + $user = User::where('type', 'company')->first(); + } + $user_id = $user ? $user->id : null; + } + } + + if (!$user_id) { + return collect(); + } + $userSettings = Setting::where('user_id', $user_id)->pluck('value', 'key')->toArray(); + + // If user is not superadmin in SaaS mode, merge with superadmin settings for specific keys + if (isSaas() && auth()->check() && auth()->user()->type !== 'superadmin') { + $superAdmin = User::where('type', 'superadmin')->first(); + if ($superAdmin) { + $superAdminKeys = ['decimalFormat', 'defaultCurrency', 'thousandsSeparator', 'floatNumber', 'currencySymbolSpace', 'currencySymbolPosition', 'dateFormat', 'timeFormat', 'calendarStartDay', 'defaultTimezone', 'contactUsUrl', 'contactUsDescription', 'strictlyCookieDescription', 'cookieDescription', 'strictlyCookieTitle', 'cookieTitle', 'strictlyNecessaryCookies', 'enableLogging']; + $superAdminSettings = Setting::where('user_id', $superAdmin->id) + ->whereIn('key', $superAdminKeys) + ->pluck('value', 'key') + ->toArray(); + $userSettings = array_merge($superAdminSettings, $userSettings); + } + } + + return $userSettings; + } +} + +if (!function_exists('formatDateTime')) { + function formatDateTime($date, $includeTime = true) + { + if (!$date) { + return null; + } + + $settings = settings(); + + $dateFormat = $settings['dateFormat'] ?? 'Y-m-d'; + $timeFormat = $settings['timeFormat'] ?? 'H:i'; + $timezone = $settings['defaultTimezone'] ?? config('app.timezone', 'UTC'); + + $format = $includeTime ? "$dateFormat $timeFormat" : $dateFormat; + + return Carbon::parse($date)->timezone($timezone)->format($format); + } +} + +if (!function_exists('getSetting')) { + function getSetting($key, $default = null, $user_id = null) + { + $settings = settings($user_id); + + // If no value found and no default provided, try to get from defaultSettings + if (!isset($settings[$key]) && $default === null) { + $defaultSettings = defaultSettings(); + $default = $defaultSettings[$key] ?? null; + } + + return $settings[$key] ?? $default; + } +} + +if (!function_exists('updateSetting')) { + function updateSetting($key, $value, $user_id = null) + { + if (is_null($user_id)) { + if (auth()->user()) { + if (isSaas()) { + if (!in_array(auth()->user()->type, ['superadmin', 'company'])) { + $user_id = auth()->user()->created_by; + } else { + $user_id = auth()->id(); + } + } else { + // Non-SaaS: Company is top level + if (auth()->user()->hasRole('company')) { + $user_id = auth()->id(); + } else { + $user_id = auth()->user()->created_by; + } + } + } else { + if (isSaas()) { + $user = User::where('type', 'superadmin')->first(); + } else { + $user = User::where('type', 'company')->first(); + } + $user_id = $user ? $user->id : null; + } + } + + if (!$user_id) { + return false; + } + + return Setting::updateOrCreate( + ['user_id' => $user_id, 'key' => $key], + ['value' => $value] + ); + } +} + +if (!function_exists('isLandingPageEnabled')) { + function isLandingPageEnabled() + { + return getSetting('landingPageEnabled', true) === true || getSetting('landingPageEnabled', true) === '1'; + } +} + +if (!function_exists('isUserRegistrationEnabled')) { + function isUserRegistrationEnabled() + { + return getSetting('userRegistrationEnabled', true) === true || getSetting('userRegistrationEnabled', true) === '1'; + } +} + +if (!function_exists('defaultRoleAndSetting')) { + function defaultRoleAndSetting($user) + { + $companyRole = Role::where('name', 'company')->first(); + + if ($companyRole) { + $user->assignRole($companyRole); + } + + // Create default settings for the user + if ($user->type === 'superadmin') { + createDefaultSettings($user->id); + } elseif ($user->type === 'company') { + copySettingsFromSuperAdmin($user->id); + $user->companyDefaultData($user); + // Create NOC Template For Company + NocTemplate::createTemplatesForCompany($user->id); + // Create Joining Letter templates For Company + JoiningLetterTemplate::createTemplatesForCompany($user->id); + // Create Experience Certificate templates For Company + ExperienceCertificateTemplate::createTemplatesForCompany($user->id); + } + + return true; + } +} + +if (!function_exists('getPaymentSettings')) { + /** + * Get payment settings for a user + * + * @param int|null $userId + * @return array + */ + function getPaymentSettings($userId = null) + { + if (is_null($userId)) { + if (auth()->check() && auth()->user()->type == 'superadmin') { + $userId = auth()->id(); + } else { + $user = User::where('type', 'superadmin')->first(); + $userId = $user ? $user->id : null; + } + } + + return PaymentSetting::getUserSettings($userId); + } +} + +if (!function_exists('updatePaymentSetting')) { + /** + * Update or create a payment setting + * + * @param string $key + * @param mixed $value + * @param int|null $userId + * @return \App\Models\PaymentSetting + */ + function updatePaymentSetting($key, $value, $userId = null) + { + if (is_null($userId)) { + $userId = auth()->id(); + } + + return PaymentSetting::updateOrCreateSetting($userId, $key, $value); + } +} + +if (!function_exists('isPaymentMethodEnabled')) { + /** + * Check if a payment method is enabled + * + * @param string $method (stripe, paypal, razorpay, mercadopago, bank) + * @param int|null $userId + * @return bool + */ + function isPaymentMethodEnabled($method, $userId = null) + { + $settings = getPaymentSettings($userId); + $key = "is_{$method}_enabled"; + + return isset($settings[$key]) && ($settings[$key] === true || $settings[$key] === '1'); + } +} + +if (!function_exists('getPaymentMethodConfig')) { + /** + * Get configuration for a specific payment method + * + * @param string $method (stripe, paypal, razorpay, mercadopago) + * @param int|null $userId + * @return array + */ + function getPaymentMethodConfig($method, $userId = null) + { + $settings = getPaymentSettings($userId); + + switch ($method) { + case 'stripe': + return [ + 'enabled' => isPaymentMethodEnabled('stripe', $userId), + 'key' => $settings['stripe_key'] ?? null, + 'secret' => $settings['stripe_secret'] ?? null, + ]; + + case 'paypal': + return [ + 'enabled' => isPaymentMethodEnabled('paypal', $userId), + 'mode' => $settings['paypal_mode'] ?? 'sandbox', + 'client_id' => $settings['paypal_client_id'] ?? null, + 'secret' => $settings['paypal_secret_key'] ?? null, + ]; + + case 'razorpay': + return [ + 'enabled' => isPaymentMethodEnabled('razorpay', $userId), + 'key' => $settings['razorpay_key'] ?? null, + 'secret' => $settings['razorpay_secret'] ?? null, + ]; + + case 'mercadopago': + return [ + 'enabled' => isPaymentMethodEnabled('mercadopago', $userId), + 'mode' => $settings['mercadopago_mode'] ?? 'sandbox', + 'access_token' => $settings['mercadopago_access_token'] ?? null, + ]; + + case 'paystack': + return [ + 'enabled' => isPaymentMethodEnabled('paystack', $userId), + 'public_key' => $settings['paystack_public_key'] ?? null, + 'secret_key' => $settings['paystack_secret_key'] ?? null, + ]; + + case 'flutterwave': + return [ + 'enabled' => isPaymentMethodEnabled('flutterwave', $userId), + 'public_key' => $settings['flutterwave_public_key'] ?? null, + 'secret_key' => $settings['flutterwave_secret_key'] ?? null, + ]; + + case 'bank': + return [ + 'enabled' => isPaymentMethodEnabled('bank', $userId), + 'details' => $settings['bank_detail'] ?? null, + ]; + + case 'paytabs': + return [ + 'enabled' => isPaymentMethodEnabled('paytabs', $userId), + 'mode' => $settings['paytabs_mode'] ?? 'sandbox', + 'profile_id' => $settings['paytabs_profile_id'] ?? null, + 'server_key' => $settings['paytabs_server_key'] ?? null, + 'region' => $settings['paytabs_region'] ?? 'ARE', + ]; + + case 'skrill': + return [ + 'enabled' => isPaymentMethodEnabled('skrill', $userId), + 'merchant_id' => $settings['skrill_merchant_id'] ?? null, + 'secret_word' => $settings['skrill_secret_word'] ?? null, + ]; + + case 'coingate': + return [ + 'enabled' => isPaymentMethodEnabled('coingate', $userId), + 'mode' => $settings['coingate_mode'] ?? 'sandbox', + 'api_token' => $settings['coingate_api_token'] ?? null, + ]; + + case 'payfast': + return [ + 'enabled' => isPaymentMethodEnabled('payfast', $userId), + 'mode' => $settings['payfast_mode'] ?? 'sandbox', + 'merchant_id' => $settings['payfast_merchant_id'] ?? null, + 'merchant_key' => $settings['payfast_merchant_key'] ?? null, + 'passphrase' => $settings['payfast_passphrase'] ?? null, + ]; + + case 'tap': + return [ + 'enabled' => isPaymentMethodEnabled('tap', $userId), + 'secret_key' => $settings['tap_secret_key'] ?? null, + ]; + + case 'xendit': + return [ + 'enabled' => isPaymentMethodEnabled('xendit', $userId), + 'api_key' => $settings['xendit_api_key'] ?? null, + ]; + + case 'paytr': + return [ + 'enabled' => isPaymentMethodEnabled('paytr', $userId), + 'merchant_id' => $settings['paytr_merchant_id'] ?? null, + 'merchant_key' => $settings['paytr_merchant_key'] ?? null, + 'merchant_salt' => $settings['paytr_merchant_salt'] ?? null, + ]; + + case 'mollie': + return [ + 'enabled' => isPaymentMethodEnabled('mollie', $userId), + 'api_key' => $settings['mollie_api_key'] ?? null, + ]; + + case 'toyyibpay': + return [ + 'enabled' => isPaymentMethodEnabled('toyyibpay', $userId), + 'category_code' => $settings['toyyibpay_category_code'] ?? null, + 'secret_key' => $settings['toyyibpay_secret_key'] ?? null, + 'mode' => $settings['toyyibpay_mode'] ?? 'sandbox', + ]; + + case 'cashfree': + return [ + 'enabled' => isPaymentMethodEnabled('cashfree', $userId), + 'mode' => $settings['cashfree_mode'] ?? 'sandbox', + 'public_key' => $settings['cashfree_public_key'] ?? null, + 'secret_key' => $settings['cashfree_secret_key'] ?? null, + ]; + + case 'iyzipay': + return [ + 'enabled' => isPaymentMethodEnabled('iyzipay', $userId), + 'mode' => $settings['iyzipay_mode'] ?? 'sandbox', + 'public_key' => $settings['iyzipay_public_key'] ?? null, + 'secret_key' => $settings['iyzipay_secret_key'] ?? null, + ]; + + case 'benefit': + return [ + 'enabled' => isPaymentMethodEnabled('benefit', $userId), + 'mode' => $settings['benefit_mode'] ?? 'sandbox', + 'public_key' => $settings['benefit_public_key'] ?? null, + 'secret_key' => $settings['benefit_secret_key'] ?? null, + ]; + + case 'ozow': + return [ + 'enabled' => isPaymentMethodEnabled('ozow', $userId), + 'mode' => $settings['ozow_mode'] ?? 'sandbox', + 'site_key' => $settings['ozow_site_key'] ?? null, + 'private_key' => $settings['ozow_private_key'] ?? null, + 'api_key' => $settings['ozow_api_key'] ?? null, + ]; + + case 'easebuzz': + return [ + 'enabled' => isPaymentMethodEnabled('easebuzz', $userId), + 'merchant_key' => $settings['easebuzz_merchant_key'] ?? null, + 'salt_key' => $settings['easebuzz_salt_key'] ?? null, + 'environment' => $settings['easebuzz_environment'] ?? 'test', + ]; + + case 'khalti': + return [ + 'enabled' => isPaymentMethodEnabled('khalti', $userId), + 'public_key' => $settings['khalti_public_key'] ?? null, + 'secret_key' => $settings['khalti_secret_key'] ?? null, + ]; + + case 'authorizenet': + return [ + 'enabled' => isPaymentMethodEnabled('authorizenet', $userId), + 'mode' => $settings['authorizenet_mode'] ?? 'sandbox', + 'merchant_id' => $settings['authorizenet_merchant_id'] ?? null, + 'transaction_key' => $settings['authorizenet_transaction_key'] ?? null, + 'supported_countries' => ['US', 'CA', 'GB', 'AU'], + 'supported_currencies' => ['USD', 'CAD', 'CHF', 'DKK', 'EUR', 'GBP', 'NOK', 'PLN', 'SEK', 'AUD', 'NZD'], + ]; + + case 'fedapay': + return [ + 'enabled' => isPaymentMethodEnabled('fedapay', $userId), + 'mode' => $settings['fedapay_mode'] ?? 'sandbox', + 'public_key' => $settings['fedapay_public_key'] ?? null, + 'secret_key' => $settings['fedapay_secret_key'] ?? null, + ]; + + case 'payhere': + return [ + 'enabled' => isPaymentMethodEnabled('payhere', $userId), + 'mode' => $settings['payhere_mode'] ?? 'sandbox', + 'merchant_id' => $settings['payhere_merchant_id'] ?? null, + 'merchant_secret' => $settings['payhere_merchant_secret'] ?? null, + 'app_id' => $settings['payhere_app_id'] ?? null, + 'app_secret' => $settings['payhere_app_secret'] ?? null, + ]; + + case 'cinetpay': + return [ + 'enabled' => isPaymentMethodEnabled('cinetpay', $userId), + 'site_id' => $settings['cinetpay_site_id'] ?? null, + 'api_key' => $settings['cinetpay_api_key'] ?? null, + 'secret_key' => $settings['cinetpay_secret_key'] ?? null, + ]; + + case 'paymentwall': + return [ + 'enabled' => isPaymentMethodEnabled('paymentwall', $userId), + 'mode' => $settings['paymentwall_mode'] ?? 'sandbox', + 'public_key' => $settings['paymentwall_public_key'] ?? null, + 'private_key' => $settings['paymentwall_private_key'] ?? null, + ]; + + default: + return []; + } + } +} + +if (!function_exists('getEnabledPaymentMethods')) { + /** + * Get all enabled payment methods + * + * @param int|null $userId + * @return array + */ + function getEnabledPaymentMethods($userId = null) + { + $methods = ['stripe', 'paypal', 'razorpay', 'mercadopago', 'paystack', 'flutterwave', 'bank', 'paytabs', 'skrill', 'coingate', 'payfast', 'tap', 'xendit', 'paytr', 'mollie', 'toyyibpay', 'cashfree', 'iyzipay', 'benefit', 'ozow', 'easebuzz', 'khalti', 'authorizenet', 'fedapay', 'payhere', 'cinetpay', 'paymentwall']; + $enabled = []; + + foreach ($methods as $method) { + if (isPaymentMethodEnabled($method, $userId)) { + $enabled[$method] = getPaymentMethodConfig($method, $userId); + } + } + + return $enabled; + } +} + +if (!function_exists('validatePaymentMethodConfig')) { + /** + * Validate payment method configuration + * + * @param string $method + * @param array $config + * @return array [valid => bool, errors => array] + */ + function validatePaymentMethodConfig($method, $config) + { + $errors = []; + + switch ($method) { + case 'stripe': + if (empty($config['key'])) { + $errors[] = 'Stripe publishable key is required'; + } + if (empty($config['secret'])) { + $errors[] = 'Stripe secret key is required'; + } + break; + + case 'paypal': + if (empty($config['client_id'])) { + $errors[] = 'PayPal client ID is required'; + } + if (empty($config['secret'])) { + $errors[] = 'PayPal secret key is required'; + } + break; + + case 'razorpay': + if (empty($config['key'])) { + $errors[] = 'Razorpay key ID is required'; + } + if (empty($config['secret'])) { + $errors[] = 'Razorpay secret key is required'; + } + break; + + case 'mercadopago': + if (empty($config['access_token'])) { + $errors[] = 'MercadoPago access token is required'; + } + break; + + case 'bank': + if (empty($config['details'])) { + $errors[] = 'Bank details are required'; + } + break; + + case 'paytabs': + if (empty($config['server_key'])) { + $errors[] = 'PayTabs server key is required'; + } + if (empty($config['profile_id'])) { + $errors[] = 'PayTabs profile id is required'; + } + if (empty($config['region'])) { + $errors[] = 'PayTabs region is required'; + } + break; + + case 'skrill': + if (empty($config['merchant_id'])) { + $errors[] = 'Skrill merchant ID is required'; + } + if (empty($config['secret_word'])) { + $errors[] = 'Skrill secret word is required'; + } + break; + + case 'coingate': + if (empty($config['api_token'])) { + $errors[] = 'CoinGate API token is required'; + } + break; + + case 'payfast': + if (empty($config['merchant_id'])) { + $errors[] = 'Payfast merchant ID is required'; + } + if (empty($config['merchant_key'])) { + $errors[] = 'Payfast merchant key is required'; + } + break; + + case 'tap': + if (empty($config['secret_key'])) { + $errors[] = 'Tap secret key is required'; + } + break; + + case 'xendit': + if (empty($config['api_key'])) { + $errors[] = 'Xendit api key is required'; + } + break; + + case 'paytr': + if (empty($config['merchant_id'])) { + $errors[] = 'PayTR merchant ID is required'; + } + if (empty($config['merchant_key'])) { + $errors[] = 'PayTR merchant key is required'; + } + if (empty($config['merchant_salt'])) { + $errors[] = 'PayTR merchant salt is required'; + } + break; + + case 'mollie': + if (empty($config['api_key'])) { + $errors[] = 'Mollie API key is required'; + } + break; + + case 'toyyibpay': + if (empty($config['category_code'])) { + $errors[] = 'toyyibPay category code is required'; + } + if (empty($config['secret_key'])) { + $errors[] = 'toyyibPay secret key is required'; + } + break; + + case 'cashfree': + if (empty($config['public_key'])) { + $errors[] = 'Cashfree App ID is required'; + } + if (empty($config['secret_key'])) { + $errors[] = 'Cashfree Secret Key is required'; + } + break; + + case 'iyzipay': + if (empty($config['public_key'])) { + $errors[] = 'Iyzipay API key is required'; + } + if (empty($config['secret_key'])) { + $errors[] = 'Iyzipay secret key is required'; + } + break; + + case 'benefit': + if (empty($config['public_key'])) { + $errors[] = 'Benefit API key is required'; + } + if (empty($config['secret_key'])) { + $errors[] = 'Benefit secret key is required'; + } + break; + + case 'ozow': + if (empty($config['site_key'])) { + $errors[] = 'Ozow site key is required'; + } + if (empty($config['private_key'])) { + $errors[] = 'Ozow private key is required'; + } + break; + + case 'easebuzz': + if (empty($config['merchant_key'])) { + $errors[] = 'Easebuzz merchant key is required'; + } + if (empty($config['salt_key'])) { + $errors[] = 'Easebuzz salt key is required'; + } + break; + + case 'khalti': + if (empty($config['public_key'])) { + $errors[] = 'Khalti public key is required'; + } + if (empty($config['secret_key'])) { + $errors[] = 'Khalti secret key is required'; + } + break; + + case 'authorizenet': + if (empty($config['merchant_id'])) { + $errors[] = 'AuthorizeNet merchant ID is required'; + } + if (empty($config['transaction_key'])) { + $errors[] = 'AuthorizeNet transaction key is required'; + } + break; + + case 'fedapay': + if (empty($config['public_key'])) { + $errors[] = 'FedaPay public key is required'; + } + if (empty($config['secret_key'])) { + $errors[] = 'FedaPay secret key is required'; + } + break; + + case 'payhere': + if (empty($config['merchant_id'])) { + $errors[] = 'PayHere merchant ID is required'; + } + if (empty($config['merchant_secret'])) { + $errors[] = 'PayHere merchant secret is required'; + } + break; + + case 'cinetpay': + if (empty($config['site_id'])) { + $errors[] = 'CinetPay site ID is required'; + } + if (empty($config['api_key'])) { + $errors[] = 'CinetPay API key is required'; + } + break; + + case 'paiement': + if (empty($config['merchant_id'])) { + $errors[] = 'Paiement Pro merchant ID is required'; + } + break; + + case 'nepalste': + if (empty($config['public_key'])) { + $errors[] = 'Nepalste public key is required'; + } + if (empty($config['secret_key'])) { + $errors[] = 'Nepalste secret key is required'; + } + break; + + case 'yookassa': + if (empty($config['shop_id'])) { + $errors[] = 'YooKassa shop ID is required'; + } + if (empty($config['secret_key'])) { + $errors[] = 'YooKassa secret key is required'; + } + break; + + case 'midtrans': + if (empty($config['secret_key'])) { + $errors[] = 'Midtrans secret key is required'; + } + break; + + case 'aamarpay': + if (empty($config['store_id'])) { + $errors[] = 'Aamarpay store ID is required'; + } + if (empty($config['signature'])) { + $errors[] = 'Aamarpay signature is required'; + } + break; + + case 'paymentwall': + if (empty($config['public_key'])) { + $errors[] = 'PaymentWall public key is required'; + } + if (empty($config['private_key'])) { + $errors[] = 'PaymentWall private key is required'; + } + break; + + case 'sspay': + if (empty($config['secret_key'])) { + $errors[] = 'SSPay secret key is required'; + } + break; + } + + return [ + 'valid' => empty($errors), + 'errors' => $errors, + ]; + } +} + +if (!function_exists('calculatePlanPricing')) { + function calculatePlanPricing($plan, $couponCode = null, $billingCycle = 'monthly') + { + // $originalPrice = $plan->price; + $originalPrice = $plan->getPriceForCycle($billingCycle); + $discountAmount = 0; + $finalPrice = $originalPrice; + $couponId = null; + + if ($couponCode) { + $coupon = Coupon::where('code', $couponCode) + ->where('status', 1) + ->first(); + + if ($coupon) { + if ($coupon->type === 'percentage') { + $discountAmount = ($originalPrice * $coupon->discount_amount) / 100; + } else { + $discountAmount = min($coupon->discount_amount, $originalPrice); + } + $finalPrice = max(0, $originalPrice - $discountAmount); + $couponId = $coupon->id; + } + } + + return [ + 'original_price' => $originalPrice, + 'discount_amount' => $discountAmount, + 'final_price' => $finalPrice, + 'coupon_id' => $couponId, + ]; + } +} + +if (!function_exists('createPlanOrder')) { + function createPlanOrder($data) + { + $plan = Plan::findOrFail($data['plan_id']); + $pricing = calculatePlanPricing($plan, $data['coupon_code'] ?? null, $data['billing_cycle'] ?? 'monthly'); + + return PlanOrder::create([ + 'user_id' => $data['user_id'], + 'plan_id' => $plan->id, + 'coupon_id' => $pricing['coupon_id'], + 'billing_cycle' => $data['billing_cycle'], + 'payment_method' => $data['payment_method'], + 'coupon_code' => $data['coupon_code'] ?? null, + 'original_price' => $pricing['original_price'], + 'discount_amount' => $pricing['discount_amount'], + 'final_price' => $pricing['final_price'], + 'payment_id' => $data['payment_id'], + 'status' => $data['status'] ?? 'pending', + 'ordered_at' => now(), + ]); + } +} + +if (!function_exists('assignPlanToUser')) { + function assignPlanToUser($user, $plan, $billingCycle) + { + $expiresAt = $billingCycle === 'yearly' ? now()->addYear() : now()->addMonth(); + + \Log::info('Assigning plan ' . $plan->id . ' to user ' . $user->id . ' with billing cycle ' . $billingCycle); + + $updated = $user->update([ + 'plan_id' => $plan->id, + 'plan_expire_date' => $expiresAt, + 'plan_is_active' => 1, + ]); + + \Log::info('Plan assignment result: ' . ($updated ? 'success' : 'failed')); + } +} + +if (!function_exists('processPaymentSuccess')) { + function processPaymentSuccess($data) + { + $plan = Plan::findOrFail($data['plan_id']); + $user = User::findOrFail($data['user_id']); + + $planOrder = createPlanOrder(array_merge($data, ['status' => 'approved'])); + assignPlanToUser($user, $plan, $data['billing_cycle']); + + // Verify the plan was assigned + $user->refresh(); + + // Create referral record if user was referred + \App\Http\Controllers\ReferralController::createReferralRecord($user); + + return $planOrder; + } +} + +if (!function_exists('getPaymentGatewaySettings')) { + function getPaymentGatewaySettings() + { + $superAdminId = User::where('type', 'superadmin')->first()?->id; + + return [ + 'payment_settings' => PaymentSetting::getUserSettings($superAdminId), + 'general_settings' => Setting::getUserSettings($superAdminId), + 'super_admin_id' => $superAdminId, + ]; + } +} + +if (!function_exists('validatePaymentRequest')) { + function validatePaymentRequest($request, $additionalRules = []) + { + $baseRules = [ + 'plan_id' => 'required|exists:plans,id', + 'billing_cycle' => 'required|in:monthly,yearly', + 'coupon_code' => 'nullable|string', + ]; + + return $request->validate(array_merge($baseRules, $additionalRules)); + } +} + +if (!function_exists('handlePaymentError')) { + function handlePaymentError($e, $method = 'payment') + { + return back()->withErrors(['error' => __('Payment processing failed: :message', ['message' => $e->getMessage()])]); + } +} + +if (!function_exists('defaultSettings')) { + function defaultSettings() + { + $productName = isSaas() ? 'HRM SaaS' : 'HRM'; + $settings = [ + 'defaultLanguage' => 'en', + 'dateFormat' => 'Y-m-d', + 'timeFormat' => 'H:i', + 'calendarStartDay' => 'sunday', + 'defaultTimezone' => 'UTC', + 'emailVerification' => false, + 'landingPageEnabled' => true, + 'userRegistrationEnabled' => true, + + 'logoDark' => 'logo/logo-dark.png', + 'logoLight' => 'logo/logo-light.png', + 'favicon' => 'logo/favicon.png', + 'titleText' => $productName, + 'footerText' => '© 2026 ' . $productName . '. All rights reserved.', + 'themeColor' => 'green', + 'customColor' => '#10b77f', + 'sidebarVariant' => 'inset', + 'sidebarStyle' => 'plain', + 'layoutDirection' => 'left', + 'themeMode' => 'light', + + 'storage_type' => 'local', + 'storage_file_types' => 'jpg,png,webp,gif,pdf,doc,docx,txt,csv', + 'storage_max_upload_size' => 2048, + 'aws_access_key_id' => '', + 'aws_secret_access_key' => '', + 'aws_default_region' => 'us-east-1', + 'aws_bucket' => '', + 'aws_url' => '', + 'aws_endpoint' => '', + 'wasabi_access_key' => '', + 'wasabi_secret_key' => '', + 'wasabi_region' => 'us-east-1', + 'wasabi_bucket' => '', + 'wasabi_url' => '', + 'wasabi_root' => '', + + 'decimalFormat' => 2, + 'defaultCurrency' => 'USD', + 'decimalSeparator' => '.', + 'thousandsSeparator' => ',', + 'floatNumber' => true, + 'currencySymbolSpace' => false, + 'currencySymbolPosition' => 'before', + + 'working_days' => '[1,2,3,4,5]', + + 'metaKeywords' => $productName . ' - All-in-One HR Management Software', + 'metaDescription' => 'Simplify employee management, payroll, attendance, recruitment, and performance with ' . $productName . ' — a modern HR management platform.', + 'metaImage' => 'seo/seo-banner.jpg', + ]; + + if (isDemo()) { + $cookieSettingArray = [ + 'enableLogging' => true, + 'strictlyNecessaryCookies' => true, + 'cookieTitle' => 'Cookie Consent', + 'strictlyCookieTitle' => 'Strictly Necessary Cookies', + 'cookieDescription' => 'We use cookies to enhance your browsing experience and provide personalized content.', + 'strictlyCookieDescription' => 'These cookies are essential for the website to function properly.', + 'contactUsDescription' => 'If you have any questions about our cookie policy, please contact us.', + 'contactUsUrl' => 'https://example.com/contact', + ]; + $settings = array_merge($settings, $cookieSettingArray); + } + + return $settings; + } +} + +if (!function_exists('createDefaultSettings')) { + function createDefaultSettings($userId) + { + $defaults = defaultSettings(); + $settingsData = []; + + foreach ($defaults as $key => $value) { + $settingsData[] = [ + 'user_id' => $userId, + 'key' => $key, + 'value' => is_bool($value) ? ($value ? '1' : '0') : (string) $value, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + + Setting::insert($settingsData); + } +} + +if (!function_exists('copySettingsFromSuperAdmin')) { + function copySettingsFromSuperAdmin($companyUserId) + { + // $superAdmin = User::where('type', 'superadmin')->first(); + // if (!$superAdmin) { + // createDefaultSettings($companyUserId); + // return; + // } + + if (isSaas()) { + $superAdmin = User::where('type', 'superadmin')->first(); + if (!$superAdmin) { + createDefaultSettings($companyUserId); + + return; + } + } else { + // Non-SaaS: Create default settings directly + createDefaultSettings($companyUserId); + + return; + } + + // Settings to copy from superadmin (system and brand settings only) + $settingsToCopy = [ + 'defaultLanguage', + 'dateFormat', + 'timeFormat', + 'calendarStartDay', + 'defaultTimezone', + 'emailVerification', + // 'landingPageEnabled', + 'logoDark', + 'logoLight', + 'favicon', + 'titleText', + 'footerText', + 'themeColor', + 'customColor', + 'sidebarVariant', + 'sidebarStyle', + 'layoutDirection', + 'themeMode', + ]; + + $superAdminSettings = Setting::where('user_id', $superAdmin->id) + ->whereIn('key', $settingsToCopy) + ->get(); + + $settingsData = []; + + // Only copy existing superadmin settings + foreach ($superAdminSettings as $setting) { + $settingsData[] = [ + 'user_id' => $companyUserId, + 'key' => $setting->key, + 'value' => $setting->value, + 'created_at' => now(), + 'updated_at' => now(), + ]; + } + + Setting::insertOrIgnore($settingsData); + } +} + +if (!function_exists('createdBy')) { + function createdBy() + { + if (Auth::user()->type == 'superadmin') { + return Auth::user()->id; + } elseif (Auth::user()->type == 'company') { + return Auth::user()->id; + } else { + return Auth::user()->created_by; + } + } +} + +if (!function_exists('creatorId')) { + function creatorId() + { + return Auth::user()->id; + } +} + +// For Auth User +if (!function_exists('getCompanyAndUsersId')) { + function getCompanyAndUsersId() + { + $user = Auth::user(); + if ($user->hasRole(['company'])) { + $companyId = getCompanyId($user->id); + if ($companyId) { + // Get all users in the company hierarchy + $allUsers = getAllCompanyUsers($companyId); + $allUsers[] = $companyId; // Include company itself + + return array_unique($allUsers); + } + + return []; + + // Old code + // $companyUserIds = User::where('created_by', $user->id)->pluck('id')->toArray(); + // $companyUserIds[] = $user->id; + // return $companyUserIds; + + } else { + // Find the root company ID using recursive function + $companyId = getCompanyId($user->id); + if ($companyId) { + // Get all users in the company hierarchy + $allUsers = getAllCompanyUsers($companyId); + $allUsers[] = $companyId; // Include company itself + + return array_unique($allUsers); + } + + return []; + } + } +} + +// Recursive Function For Get the All Users of the company in tree hierarchy +if (!function_exists('getAllCompanyUsers')) { + function getAllCompanyUsers($companyId, &$allUsers = []) + { + // Get direct users created by this company/user + $directUsers = User::where('created_by', $companyId)->pluck('id')->toArray(); + foreach ($directUsers as $userId) { + if (!in_array($userId, $allUsers)) { + $allUsers[] = $userId; + // Recursively get users created by this user + getAllCompanyUsers($userId, $allUsers); + } + } + + return $allUsers; + } +} + +// For Non Auth User For Career page +if (!function_exists('getCompanyUsers')) { + function getCompanyUsers($companyId) + { + $user = User::where('id', $companyId)->first(); + if (!$user) { + return []; + } + if ($user->hasRole(['company'])) { + $companyId = $user->id; + + if ($companyId) { + // Get all users in the company hierarchy + $allUsers = getAllCompanyUsers($companyId); + $allUsers[] = $companyId; // Include company itself + + return array_unique($allUsers); + } + + return []; + + // Old Code + // $companyUserIds = User::where('created_by', $user->id)->pluck('id')->toArray(); + // $companyUserIds[] = $user->id; + // return $companyUserIds; + } else { + $userCreatedBy = User::where('id', $user->created_by)->value('id'); + $companyUserIds = User::where('created_by', $userCreatedBy)->pluck('id')->toArray(); + $companyUserIds[] = $userCreatedBy; + + return $companyUserIds; + } + } +} + +// Get Image URL Path +if (!function_exists('getImageUrlPrefix')) { + function getImageUrlPrefix(): string + { + $settings = settings(); + $storageType = $settings['storage_type'] ?? 'local'; + switch ($storageType) { + case 's3': + $endpoint = $settings['aws_endpoint']; + if ($endpoint) { + return rtrim($endpoint, '/') . '/media/'; + } + $bucket = $settings['aws_bucket']; + $region = $settings['aws_default_region']; + + return "https://{$bucket}.s3.{$region}.amazonaws.com/media/"; + + case 'wasabi': + $url = $settings['wasabi_url']; + + return $url ? rtrim($url, '/') . '/media/' : url('/storage/media/'); + + case 'local': + default: + return url('/'); + } + } +} + +// Get Company and User +if (!function_exists('getUser')) { + function getUser() + { + $autheUser = Auth::user(); + if ($autheUser->hasRole('superadmin')) { + return $autheUser; + } elseif ($autheUser->hasRole('company')) { + return $autheUser; + } else { + $company = User::where('id', $autheUser->created_by)->first(); + + return $company; + } + } +} + +if (!function_exists('getStorageFilePath')) { + /** + * Get storage file path for downloads + */ + function getStorageFilePath($filename) + { + if (empty($filename)) { + return null; + } + + // Remove any path separators to ensure only filename + $filename = basename($filename); + + return storage_path('app/public/media/' . $filename); + } +} + +if (!function_exists('randomImage')) { + function randomImage() + { + if (isSaas() && isDemo()) { + $images = [ + 'apex-industries-building-exterior.png', + 'apex-industries-business-card.png', + 'default-avatar.png', + 'global-systems-inc-social-banner.png', + 'apex-industries-logo.png', + 'phoenix-corporation-team-photo.png', + 'stellar-enterprises-social-banner.png', + 'techcorp-solutions-office-photo.png', + 'vortex-systems-building-exterior.png', + 'vortex-systems-business-card.png', + 'techcorp-solutions-business-card.png', + 'quantum-dynamics-office-photo.png', + 'phoenix-corporation-building-exterior.png', + 'infinity-solutions-office-photo.png', + 'nexus-technologies-business-card.png', + 'loading-animation.png', + 'global-systems-inc-office-photo.png', + 'digital-innovations-ltd-team-photo.png', + 'certificate-template.png', + 'apex-industries-team-photo.png', + ]; + } else { + $images = [ + 'company-logo.png', + 'company-office-photo.png', + 'company-business-card.png', + 'company-letterhead.png', + 'company-team-photo.png', + 'company-building-exterior.png', + 'company-social-banner.png', + ]; + } + + $randomImage = collect($images)->random(); + + return $randomImage; + } +} + +if (!function_exists('isSaas')) { + function isSaas() + { + $isSaas = config('app.is_saas'); + + return $isSaas; + } +} +if (!function_exists('isDemo')) { + function isDemo() + { + $isDemo = config('app.is_demo'); + + return $isDemo; + } +} + +if (!function_exists('isNotEditableRoles')) { + function isNotEditableRoles() + { + // Roles that cannot be edited + $notEditableRoles = [ + 'employee', + 'hr', + + ]; + + return $notEditableRoles; + } +} + +if (!function_exists('isNotDeletableRoles')) { + function isNotDeletableRoles() + { + $notDeletableRoles = [ + 'employee', + 'hr', + ]; + + return $notDeletableRoles; + } +} + +if (!function_exists('formatCurrency')) { + function formatCurrency($amount, $user_id = null) + { + $settings = settings($user_id); + $currencyCode = $settings['defaultCurrency'] ?? 'USD'; + + // Get currency symbol from database + $currency = Currency::where('code', $currencyCode)->first(); + $symbol = $currency ? $currency->symbol : $currencyCode; + + $decimalPlaces = (int) ($settings['decimalFormat'] ?? 2); + $decimalSeparator = $settings['decimalSeparator'] ?? '.'; + $thousandsSeparator = $settings['thousandsSeparator'] ?? ','; + $symbolPosition = $settings['currencySymbolPosition'] ?? 'before'; + $symbolSpace = $settings['currencySymbolSpace'] ?? false; + + $formattedAmount = number_format($amount, $decimalPlaces, $decimalSeparator, $thousandsSeparator); + + if ($symbolPosition === 'before') { + return $symbol . ($symbolSpace ? ' ' : '') . $formattedAmount; + } else { + return $formattedAmount . ($symbolSpace ? ' ' : '') . $symbol; + } + } + + // For generate the Unique Slug for Missing Slug Users + if (!function_exists('fixMissingUserSlugs')) { + function fixMissingUserSlugs() + { + $updatedCount = 0; + + $users = User::whereNull('slug') + ->orWhere('slug', '') + ->get(); + + foreach ($users as $user) { + if (empty($user->name)) { + continue; + } + + $baseSlug = Str::slug($user->name); + $slug = $baseSlug; + $counter = 1; + + while ( + User::where('slug', $slug) + ->where('id', '!=', $user->id) + ->exists() + ) { + $slug = $baseSlug . '-' . $counter; + $counter++; + } + + $user->slug = $slug; + $user->save(); + + $updatedCount++; + } + + return $updatedCount; + } + } +} + +if (!function_exists('getCompanyId')) { + function getCompanyId($userId) + { + $user = User::find($userId); + + if (!$user) { + return null; + } + if ($user->type === 'company' || $user->hasRole('company')) { + return $user->id; + } + + if ($user->created_by) { + return getCompanyId($user->created_by); + } + + return null; + } +} + +// Set Email Configurations +if (!function_exists('setEmailConfigurations')) { + + function setEmailConfigurations(): void + { + try { + $user = Auth::user(); + if (!$user) { + return; + } + if (isSaas()) { + if ($user->hasRole('superadmin')) { + $user = $user; + } elseif ($user->hasRole('company')) { + $user = $user; + } else { + $getUserCreatedBy = getCompanyId($user->id); + $user = User::where('id', $getUserCreatedBy)->first(); + } + } else { + if ($user->hasRole('company')) { + $user = $user; + } else { + $getUserCreatedBy = getCompanyId($user->id); + $user = User::where('id', $getUserCreatedBy)->first(); + } + } + + $getSettings = settings($user->id); + + $settings = [ + 'driver' => $getSettings['email_driver'] ?? '', + 'host' => $getSettings['email_host'] ?? '', + 'port' => $getSettings['email_port'] ?? '', + 'username' => $getSettings['email_username'] ?? '', + 'password' => $getSettings['email_password'] ?? '', + 'encryption' => $getSettings['email_encryption'] ?? '', + 'fromAddress' => $getSettings['email_from_address'] ?? '', + 'fromName' => $getSettings['email_from_name'] ?? '', + ]; + + Config::set([ + 'mail.default' => $settings['driver'], + 'mail.mailers.smtp.host' => $settings['host'], + 'mail.mailers.smtp.port' => $settings['port'], + 'mail.mailers.smtp.encryption' => $settings['encryption'] === 'none' ? null : $settings['encryption'], + 'mail.mailers.smtp.username' => $settings['username'], + 'mail.mailers.smtp.password' => $settings['password'], + 'mail.from.address' => $settings['fromAddress'], + 'mail.from.name' => $settings['fromName'], + ]); + } catch (\Exception $e) { + throw new \Exception('Email config error: ' . $e->getMessage()); + } + } +} + +// Set Email Configurations +if (!function_exists('getDeviceType')) { + + function getDeviceType($userAgent) + { + $mobile_regex = '/(?:phone|windows\s+phone|ipod|blackberry|(?:android|bb\d+|meego|silk|googlebot) .+? mobile|palm|windows\s+ce|opera mini|avantgo|mobilesafari|docomo)/i'; + $tablet_regex = '/(?:ipad|playbook|(?:android|bb\d+|meego|silk)(?! .+? mobile))/i'; + + if (preg_match_all($mobile_regex, $userAgent)) { + return 'mobile'; + } else { + + if (preg_match_all($tablet_regex, $userAgent)) { + return 'tablet'; + } else { + return 'desktop'; + } + } + } +} + +// Get super admin settings +if (!function_exists('getAdminAllSetting')) { + function getAdminAllSetting() + { + // Laravel cache + return Cache::rememberForever('admin_settings', function () { + if (isSaas()) { + $superAdmin = User::where('type', 'superadmin')->first(); + } else { + $superAdmin = User::where('type', 'company')->first(); + } + + $settings = []; + if ($superAdmin) { + $settings = Setting::where('user_id', $superAdmin->id)->pluck('value', 'key')->toArray(); + } + + return $settings; + }); + } +} + +// File Upload Function +if (!function_exists('upload_file')) { + function upload_file($request, $key_name, $name, $path, $custom_validation = []) + { + try { + $storage_settings = getAdminAllSetting(); + + if (isset($storage_settings['storage_type'])) { + if ($storage_settings['storage_type'] == 'wasabi') { + config( + [ + 'filesystems.disks.wasabi.driver' => 's3', + 'filesystems.disks.wasabi.key' => $storage_settings['wasabi_access_key'], + 'filesystems.disks.wasabi.secret' => $storage_settings['wasabi_secret_key'], + 'filesystems.disks.wasabi.region' => $storage_settings['wasabi_region'] ?? 'us-east-1', + 'filesystems.disks.wasabi.bucket' => $storage_settings['wasabi_bucket'], + 'filesystems.disks.wasabi.endpoint' => $storage_settings['wasabi_url'], + 'filesystems.disks.wasabi.root' => $storage_settings['wasabi_root'], + 'filesystems.disks.use_path_style_endpoint' => false, + 'filesystems.disks.wasabi.visibility' => 'public', + ] + ); + $max_size = !empty($storage_settings['storage_max_upload_size']) ? $storage_settings['storage_max_upload_size'] : '2048'; + $mimes = !empty($storage_settings['storage_file_types']) ? $storage_settings['storage_file_types'] : 'jpeg,jpg,png,svg,zip,txt,gif,docx'; + } elseif ($storage_settings['storage_type'] == 'aws_s3') { + config( + [ + 'filesystems.disks.s3.driver' => 's3', + 'filesystems.disks.s3.key' => $storage_settings['aws_access_key_id'], + 'filesystems.disks.s3.secret' => $storage_settings['aws_secret_access_key'], + 'filesystems.disks.s3.region' => $storage_settings['aws_default_region'] ?? 'us-east-1', + 'filesystems.disks.s3.bucket' => $storage_settings['aws_bucket'], + 'filesystems.disks.s3.url' => $storage_settings['aws_url'], + 'filesystems.disks.s3.endpoint' => $storage_settings['aws_endpoint'], + 'filesystems.disks.s3.use_path_style_endpoint' => false, + 'filesystems.disks.s3.visibility' => 'public', + ] + ); + $max_size = !empty($storage_settings['storage_max_upload_size']) ? $storage_settings['storage_max_upload_size'] : '2048'; + $mimes = !empty($storage_settings['storage_file_types']) ? $storage_settings['storage_file_types'] : 'jpeg,jpg,png,svg,zip,txt,gif,docx'; + } else { + $max_size = !empty($storage_settings['storage_max_upload_size']) ? $storage_settings['storage_max_upload_size'] : '2048'; + $mimes = !empty($storage_settings['storage_file_types']) ? $storage_settings['storage_file_types'] : 'jpeg,jpg,png,svg,zip,txt,gif,docx'; + } + $file = $request->$key_name; + + $extension = strtolower($file->getClientOriginalExtension()); + $allowed_extensions = explode(',', $mimes); + + if (empty($extension) || !in_array($extension, $allowed_extensions)) { + return [ + 'status' => false, + 'msg' => 'The ' . $key_name . ' must be a file of type: ' . implode(', ', $allowed_extensions) . '.', + ]; + } + + if (count($custom_validation) > 0) { + $validation = $custom_validation; + } else { + $validation = [ + 'mimes:' . $mimes, + 'max:' . $max_size, + ]; + } + $validator = Validator::make($request->all(), [ + $key_name => $validation, + ]); + if ($validator->fails()) { + $res = [ + 'status' => false, + 'msg' => $validator->messages()->first(), + ]; + + return $res; + } else { + $storageType = $settings['storage_type'] ?? 'local'; + $diskName = match ($storageType) { + 'local' => 'public', + 'aws_s3' => 's3', + 'wasabi' => 'wasabi', + default => 'public' + }; + + // Store file directly to storage + $file->storeAs('media/' . $path, $name, $diskName); + + $res = [ + 'status' => true, + 'msg' => 'success', + 'url' => $path . '/' . $name, + ]; + + return $res; + } + } else { + $res = [ + 'status' => false, + 'msg' => __('Not set configurations'), + ]; + + return $res; + } + } catch (\Exception $e) { + $res = [ + 'status' => false, + 'msg' => $e->getMessage(), + ]; + + return $res; + } + } +} + +// Multiple File Uploads +if (!function_exists('multi_upload_file')) { + function multi_upload_file($file, $key_name, $name, $path, $custom_validation = []) + { + try { + $storage_settings = getAdminAllSetting(); + + if (isset($storage_settings['storage_type'])) { + if ($storage_settings['storage_type'] == 'wasabi') { + config( + [ + 'filesystems.disks.wasabi.key' => $storage_settings['wasabi_access_key'], + 'filesystems.disks.wasabi.secret' => $storage_settings['wasabi_secret_key'], + 'filesystems.disks.wasabi.region' => $storage_settings['wasabi_region'] ?? 'us-east-1', + 'filesystems.disks.wasabi.bucket' => $storage_settings['wasabi_bucket'], + 'filesystems.disks.wasabi.root' => $storage_settings['wasabi_root'], + 'filesystems.disks.wasabi.endpoint' => $storage_settings['wasabi_url'], + ] + ); + $max_size = !empty($storage_settings['storage_max_upload_size']) ? $storage_settings['storage_max_upload_size'] : '2048'; + $mimes = !empty($storage_settings['storage_file_types']) ? $storage_settings['storage_file_types'] : 'jpeg,jpg,png,svg,zip,txt,gif,docx'; + } elseif ($storage_settings['storage_type'] == 'aws_s3') { + config( + [ + 'filesystems.disks.s3.key' => $storage_settings['aws_access_key_id'], + 'filesystems.disks.s3.secret' => $storage_settings['aws_secret_access_key'], + 'filesystems.disks.s3.region' => $storage_settings['aws_default_region'] ?? 'us-east-1', + 'filesystems.disks.s3.bucket' => $storage_settings['aws_bucket'], + // 'filesystems.disks.s3.url' => $storage_settings['aws_url'], + // 'filesystems.disks.s3.endpoint' => $storage_settings['aws_endpoint'], + ] + ); + $max_size = !empty($storage_settings['storage_max_upload_size']) ? $storage_settings['storage_max_upload_size'] : '2048'; + $mimes = !empty($storage_settings['storage_file_types']) ? $storage_settings['storage_file_types'] : 'jpeg,jpg,png,svg,zip,txt,gif,docx'; + } else { + $max_size = !empty($storage_settings['storage_max_upload_size']) ? $storage_settings['storage_max_upload_size'] : '2048'; + $mimes = !empty($storage_settings['storage_file_types']) ? $storage_settings['storage_file_types'] : 'jpeg,jpg,png,svg,zip,txt,gif,docx'; + } + + $extension = strtolower($file->getClientOriginalExtension()); + $allowed_extensions = explode(',', $mimes); + + if (empty($extension) || !in_array($extension, $allowed_extensions)) { + return [ + 'status' => false, + 'msg' => 'The ' . $key_name . ' must be a file of type: ' . implode(', ', $allowed_extensions) . '.', + ]; + } + + if (count($custom_validation) > 0) { + $validation = $custom_validation; + } else { + $validation = [ + 'mimes:' . $mimes, + 'max:' . $max_size, + ]; + } + + $validator = Validator::make([$key_name => $file], [ + $key_name => $validation, + ]); + + if ($validator->fails()) { + $res = [ + 'status' => false, + 'msg' => $validator->messages()->first(), + ]; + + return $res; + } else { + $storageType = $storage_settings['storage_type'] ?? 'local'; + $diskName = match ($storageType) { + 'local' => 'public', + 'aws_s3' => 's3', + 'wasabi' => 'wasabi', + default => 'public' + }; + + // Store file directly to storage + $file->storeAs('media/' . $path, $name, $diskName); + + $res = [ + 'status' => true, + 'msg' => 'success', + 'url' => $path . '/' . $name, + ]; + + return $res; + } + } else { + $res = [ + 'status' => false, + 'msg' => __('Not set configurations'), + ]; + + return $res; + } + } catch (\Exception $e) { + $res = [ + 'status' => false, + 'msg' => $e->getMessage(), + ]; + + return $res; + } + } +} + + +if (!function_exists('check_file')) { + function check_file($path) + { + try { + if (empty($path)) { + return false; + } + $storage_settings = getAdminAllSetting(); + if (!isset($storage_settings['storage_type'])) { + return false; + } + + $storageType = $storage_settings['storage_type']; + + // Handle local storage + if ($storageType === 'local' || $storageType === null) { + // Check in public storage path + $publicPath = storage_path('app/public/media/' . ltrim($path, '/')); + if (file_exists($publicPath)) { + return true; + } + + // Check in base path as fallback + $basePath = base_path($path); + + return file_exists($basePath); + } + + // Handle AWS S3 storage + if ($storageType === 'aws_s3') { + if ( + empty($storage_settings['aws_access_key_id']) || + empty($storage_settings['aws_secret_access_key']) || + empty($storage_settings['aws_default_region']) || + empty($storage_settings['aws_bucket']) + ) { + return false; + } + + config([ + 'filesystems.disks.s3.key' => $storage_settings['aws_access_key_id'], + 'filesystems.disks.s3.secret' => $storage_settings['aws_secret_access_key'], + 'filesystems.disks.s3.region' => $storage_settings['aws_default_region'] ?? 'us-east-1', + 'filesystems.disks.s3.bucket' => $storage_settings['aws_bucket'], + ]); + + // Normalize path for S3 + $s3Path = 'media/' . ltrim($path, '/'); + + return Storage::disk('s3')->exists($s3Path); + } + + // Handle Wasabi storage + if ($storageType === 'wasabi') { + if ( + empty($storage_settings['wasabi_access_key']) || + empty($storage_settings['wasabi_secret_key']) || + empty($storage_settings['wasabi_region']) || + empty($storage_settings['wasabi_bucket']) || + empty($storage_settings['wasabi_url']) || + empty($storage_settings['wasabi_root']) + ) { + return false; + } + + config([ + 'filesystems.disks.wasabi.key' => $storage_settings['wasabi_access_key'], + 'filesystems.disks.wasabi.secret' => $storage_settings['wasabi_secret_key'], + 'filesystems.disks.wasabi.region' => $storage_settings['wasabi_region'] ?? 'us-east-1', + 'filesystems.disks.wasabi.bucket' => $storage_settings['wasabi_bucket'], + 'filesystems.disks.wasabi.endpoint' => $storage_settings['wasabi_url'] ?? null, + 'filesystems.disks.wasabi.root' => $storage_settings['wasabi_root'] ?? '', + ]); + + // Normalize path for Wasabi + $wasabiPath = 'media/' . ltrim($path, '/'); + + return Storage::disk('wasabi')->exists($wasabiPath); + } + + // Unknown storage type + return false; + + } catch (\Exception $e) { + // Log error for debugging + Log::error('check_file error: ' . $e->getMessage(), [ + 'path' => $path, + 'trace' => $e->getTraceAsString(), + ]); + + return false; + } + } +} + +if (!function_exists('get_file')) { + function get_file($path) + { + try { + // Return empty string if path is empty + if (empty($path)) { + return ''; + } + + $storage_settings = getAdminAllSetting(); + + // Check if storage settings exist, fallback to local + if (!isset($storage_settings['storage_type'])) { + return url('storage/media/' . ltrim($path, '/')); + } + + $storageType = $storage_settings['storage_type']; + + // Handle AWS S3 storage + if ($storageType === 'aws_s3' || $storageType === 's3') { + if ( + empty($storage_settings['s3_key']) || + empty($storage_settings['s3_secret']) || + empty($storage_settings['s3_region']) || + empty($storage_settings['s3_bucket']) + ) { + return url('storage/media/' . ltrim($path, '/')); + } + + config([ + 'filesystems.disks.s3.key' => $storage_settings['s3_key'], + 'filesystems.disks.s3.secret' => $storage_settings['s3_secret'], + 'filesystems.disks.s3.region' => $storage_settings['s3_region'], + 'filesystems.disks.s3.bucket' => $storage_settings['s3_bucket'], + ]); + + // Normalize path for S3 + $s3Path = 'media/' . ltrim($path, '/'); + return Storage::disk('s3')->url($s3Path); + } + + // Handle Wasabi storage + if ($storageType === 'wasabi') { + if ( + empty($storage_settings['wasabi_key']) || + empty($storage_settings['wasabi_secret']) || + empty($storage_settings['wasabi_region']) || + empty($storage_settings['wasabi_bucket']) || + empty($storage_settings['wasabi_root']) || + empty($storage_settings['wasabi_url']) + ) { + return url('storage/media/' . ltrim($path, '/')); + } + + config([ + 'filesystems.disks.wasabi.key' => $storage_settings['wasabi_key'], + 'filesystems.disks.wasabi.secret' => $storage_settings['wasabi_secret'], + 'filesystems.disks.wasabi.region' => $storage_settings['wasabi_region'], + 'filesystems.disks.wasabi.bucket' => $storage_settings['wasabi_bucket'], + 'filesystems.disks.wasabi.root' => $storage_settings['wasabi_root'], + 'filesystems.disks.wasabi.endpoint' => $storage_settings['wasabi_url'], + ]); + + // Normalize path for Wasabi + $wasabiPath = 'media/' . ltrim($path, '/'); + return Storage::disk('wasabi')->url($wasabiPath); + } + + // Handle local storage (default) + return url('storage/media/' . ltrim($path, '/')); + + } catch (\Exception $e) { + // Log error for debugging + Log::error('get_file error: ' . $e->getMessage(), [ + 'path' => $path, + 'trace' => $e->getTraceAsString() + ]); + // Return asset path as fallback + return asset($path); + } + } +} + +if (!function_exists('delete_file')) { + function delete_file($path) + { + try { + // Return false if path is empty + if (empty($path)) { + return false; + } + + // Check if file exists first + if (!check_file($path)) { + return false; + } + + $storage_settings = getAdminAllSetting(); + + // Check if storage settings exist + if (!isset($storage_settings['storage_type'])) { + return false; + } + + $storageType = $storage_settings['storage_type']; + + // Handle local storage + if ($storageType === 'local' || $storageType === null) { + $publicPath = storage_path('app/public/media/' . ltrim($path, '/')); + if (file_exists($publicPath)) { + return unlink($publicPath); + } + return false; + } + + // Handle AWS S3 storage + if ($storageType === 'aws_s3' || $storageType === 's3') { + if ( + empty($storage_settings['s3_key']) || + empty($storage_settings['s3_secret']) || + empty($storage_settings['s3_region']) || + empty($storage_settings['s3_bucket']) + ) { + return false; + } + + config([ + 'filesystems.disks.s3.key' => $storage_settings['s3_key'], + 'filesystems.disks.s3.secret' => $storage_settings['s3_secret'], + 'filesystems.disks.s3.region' => $storage_settings['s3_region'], + 'filesystems.disks.s3.bucket' => $storage_settings['s3_bucket'], + ]); + + // Normalize path for S3 + $s3Path = 'media/' . ltrim($path, '/'); + return Storage::disk('s3')->delete($s3Path); + } + + // Handle Wasabi storage + if ($storageType === 'wasabi') { + if ( + empty($storage_settings['wasabi_key']) || + empty($storage_settings['wasabi_secret']) || + empty($storage_settings['wasabi_region']) || + empty($storage_settings['wasabi_bucket']) || + empty($storage_settings['wasabi_root']) || + empty($storage_settings['wasabi_url']) + ) { + return false; + } + + config([ + 'filesystems.disks.wasabi.key' => $storage_settings['wasabi_key'], + 'filesystems.disks.wasabi.secret' => $storage_settings['wasabi_secret'], + 'filesystems.disks.wasabi.region' => $storage_settings['wasabi_region'], + 'filesystems.disks.wasabi.bucket' => $storage_settings['wasabi_bucket'], + 'filesystems.disks.wasabi.root' => $storage_settings['wasabi_root'], + 'filesystems.disks.wasabi.endpoint' => $storage_settings['wasabi_url'], + ]); + + // Normalize path for Wasabi + $wasabiPath = 'media/' . ltrim($path, '/'); + return Storage::disk('wasabi')->delete($wasabiPath); + } + + // Unknown storage type + return false; + + } catch (\Exception $e) { + // Log error for debugging + Log::error('delete_file error: ' . $e->getMessage(), [ + 'path' => $path, + 'trace' => $e->getTraceAsString() + ]); + return false; + } + } +} diff --git a/app/Http/Controllers/AamarpayPaymentController.php b/app/Http/Controllers/AamarpayPaymentController.php new file mode 100644 index 000000000..caaec2feb --- /dev/null +++ b/app/Http/Controllers/AamarpayPaymentController.php @@ -0,0 +1,211 @@ + 'required|string', + 'mer_txnid' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['aamarpay_store_id'])) { + return back()->withErrors(['error' => __('Aamarpay not configured')]); + } + + if ($validated['pay_status'] === 'Successful') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'aamarpay', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['mer_txnid'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'aamarpay'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['aamarpay_store_id']) || !isset($settings['payment_settings']['aamarpay_signature'])) { + return response()->json(['error' => __('Aamarpay not configured')], 400); + } + + $user = auth()->user(); + $orderID = strtoupper(str_replace('.', '', uniqid('', true))); + $currency = $settings['payment_settings']['currency'] ?? 'BDT'; + $url = 'https://sandbox.aamarpay.com/request.php'; + + // Use proper test store_id for sandbox + $storeId = $settings['payment_settings']['aamarpay_store_id']; + if ($storeId === 'aamarpaytest') { + $storeId = 'aamarpaytest'; // This might need to be changed to actual test store ID + } + + $fields = [ + 'store_id' => $storeId, + 'amount' => $pricing['final_price'], + 'payment_type' => '', + 'currency' => $currency, + 'tran_id' => $orderID, + 'cus_name' => $user->name ?? 'Customer', + 'cus_email' => $user->email, + 'cus_add1' => '', + 'cus_add2' => '', + 'cus_city' => '', + 'cus_state' => '', + 'cus_postcode' => '', + 'cus_country' => '', + 'cus_phone' => '1234567890', + 'success_url' => route('aamarpay.success', [ + 'response' => 'success', + 'coupon' => $validated['coupon_code'] ?? '', + 'plan_id' => $plan->id, + 'price' => $pricing['final_price'], + 'order_id' => $orderID, + 'user_id' => $user->id, + 'billing_cycle' => $validated['billing_cycle'] + ]), + 'fail_url' => route('aamarpay.success', [ + 'response' => 'failure', + 'coupon' => $validated['coupon_code'] ?? '', + 'plan_id' => $plan->id, + 'price' => $pricing['final_price'], + 'order_id' => $orderID + ]), + 'cancel_url' => route('aamarpay.success', ['response' => 'cancel']), + 'signature_key' => $settings['payment_settings']['aamarpay_signature'], + 'desc' => 'Plan: ' . $plan->name, + ]; + + $fields_string = http_build_query($fields); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_VERBOSE, true); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $response = curl_exec($ch); + $url_forward = str_replace('"', '', stripslashes($response)); + curl_close($ch); + + if ($url_forward) { + return $this->redirectToMerchant($url_forward); + } + + return response()->json(['error' => __('Payment creation failed')], 500); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + private function redirectToMerchant($url) + { + $token = csrf_token(); + $redirectUrl = 'https://sandbox.aamarpay.com/' . $url; + + return response(view('aamarpay-redirect', compact('redirectUrl', 'token'))); + } + + public function success(Request $request) + { + try { + $response = $request->input('response'); + $planId = $request->input('plan_id'); + $userId = $request->input('user_id'); + $coupon = $request->input('coupon'); + $billingCycle = $request->input('billing_cycle', 'monthly'); + $orderId = $request->input('order_id'); + + if ($response === 'success' && $planId && $userId) { + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $billingCycle, + 'payment_method' => 'aamarpay', + 'coupon_code' => $coupon, + 'payment_id' => $orderId, + ]); + + // Log the user in if not already authenticated + if (!auth()->check()) { + auth()->login($user); + } + + return redirect()->route('plans.index')->with('success', __('Payment completed successfully and plan activated')); + } + } + + return redirect()->route('plans.index')->with('error', __('Payment failed or cancelled')); + + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment processing failed')); + } + } + + public function callback(Request $request) + { + try { + $transactionId = $request->input('mer_txnid'); + $status = $request->input('pay_status'); + + if ($transactionId && $status === 'Successful') { + $parts = explode('_', $transactionId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', + 'payment_method' => 'aamarpay', + 'payment_id' => $request->input('pg_txnid'), + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ActionItemController.php b/app/Http/Controllers/ActionItemController.php new file mode 100644 index 000000000..2738078a7 --- /dev/null +++ b/app/Http/Controllers/ActionItemController.php @@ -0,0 +1,248 @@ +can('manage-action-items')) { + $query = ActionItem::with(['meeting.type', 'assignee'])->where(function ($q) { + if (Auth::user()->can('manage-any-action-items')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-action-items')) { + $q->where('created_by', Auth::id())->orWhere('assigned_to', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhereHas('assignee', function ($aq) use ($request) { + $aq->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('priority') && !empty($request->priority) && $request->priority !== 'all') { + $query->where('priority', $request->priority); + } + + if ($request->has('assigned_to') && !empty($request->assigned_to) && $request->assigned_to !== 'all') { + $query->where('assigned_to', $request->assigned_to); + } + + if ($request->has('meeting_id') && !empty($request->meeting_id) && $request->meeting_id !== 'all') { + $query->where('meeting_id', $request->meeting_id); + } + + // Auto-update overdue items + ActionItem::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', '!=', 'Completed') + ->where('due_date', '<', Carbon::today()) + ->update(['status' => 'Overdue']); + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['title', 'due_date'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $actionItems = $query->paginate($request->per_page ?? 10); + + $meetings = Meeting::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'title', 'meeting_date') + ->orderBy('meeting_date', 'desc') + ->get(); + + $employees = User::whereIn('created_by', getCompanyAndUsersId()) + ->where('type', 'employee') + ->select('id', 'name') + ->get(); + + return Inertia::render('meetings/action-items/index', [ + 'actionItems' => $actionItems, + 'meetings' => $meetings, + 'employees' => $employees, + 'filters' => $request->all(['search', 'status', 'priority', 'assigned_to', 'meeting_id', 'per_page', 'sort_field', 'sort_direction']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'meeting_id' => 'required|exists:meetings,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'assigned_to' => 'required|exists:users,id', + 'due_date' => 'required|date|after_or_equal:today', + 'priority' => 'required|in:Low,Medium,High,Critical', + 'progress_percentage' => 'nullable|integer|min:0|max:100', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $status = 'Not Started'; + $progress = $request->progress_percentage ?? 0; + + if ($progress > 0 && $progress < 100) { + $status = 'In Progress'; + } elseif ($progress == 100) { + $status = 'Completed'; + } + + ActionItem::create([ + 'meeting_id' => $request->meeting_id, + 'title' => $request->title, + 'description' => $request->description, + 'assigned_to' => $request->assigned_to, + 'due_date' => $request->due_date, + 'priority' => $request->priority, + 'status' => $status, + 'progress_percentage' => $progress, + 'notes' => $request->notes, + 'completed_date' => $status === 'Completed' ? now() : null, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Action item created successfully')); + } + + public function update(Request $request, ActionItem $actionItem) + { + if (!in_array($actionItem->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this action item')); + } + + $validator = Validator::make($request->all(), [ + 'meeting_id' => 'required|exists:meetings,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'assigned_to' => 'required|exists:users,id', + 'due_date' => 'required|date', + 'priority' => 'required|in:Low,Medium,High,Critical', + 'progress_percentage' => 'nullable|integer|min:0|max:100', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $progress = $request->progress_percentage ?? $actionItem->progress_percentage; + $status = $actionItem->status; + $completedDate = $actionItem->completed_date; + + if ($progress == 0) { + $status = 'Not Started'; + $completedDate = null; + } elseif ($progress > 0 && $progress < 100) { + $status = 'In Progress'; + $completedDate = null; + } elseif ($progress == 100) { + $status = 'Completed'; + $completedDate = $completedDate ?? now(); + } + + // Check if overdue + if ($status !== 'Completed' && Carbon::parse($request->due_date) < Carbon::today()) { + $status = 'Overdue'; + } + + $actionItem->update([ + 'meeting_id' => $request->meeting_id, + 'title' => $request->title, + 'description' => $request->description, + 'assigned_to' => $request->assigned_to, + 'due_date' => $request->due_date, + 'priority' => $request->priority, + 'status' => $status, + 'progress_percentage' => $progress, + 'notes' => $request->notes, + 'completed_date' => $completedDate, + ]); + + return redirect()->back()->with('success', __('Action item updated successfully')); + } + + public function destroy(ActionItem $actionItem) + { + if (!in_array($actionItem->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this action item')); + } + + $actionItem->delete(); + return redirect()->back()->with('success', __('Action item deleted successfully')); + } + + public function updateProgress(Request $request, ActionItem $actionItem) + { + if (!in_array($actionItem->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this action item')); + } + + $validator = Validator::make($request->all(), [ + 'progress_percentage' => 'required|integer|min:0|max:100', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $progress = $request->progress_percentage; + $status = $actionItem->status; + $completedDate = $actionItem->completed_date; + + if ($progress == 0) { + $status = 'Not Started'; + $completedDate = null; + } elseif ($progress > 0 && $progress < 100) { + $status = 'In Progress'; + $completedDate = null; + } elseif ($progress == 100) { + $status = 'Completed'; + $completedDate = now(); + } + + $actionItem->update([ + 'progress_percentage' => $progress, + 'status' => $status, + 'completed_date' => $completedDate, + 'notes' => $request->notes ?? $actionItem->notes, + ]); + + return redirect()->back()->with('success', __('Progress updated successfully')); + } +} diff --git a/app/Http/Controllers/AnnouncementController.php b/app/Http/Controllers/AnnouncementController.php new file mode 100644 index 000000000..aaf09ef69 --- /dev/null +++ b/app/Http/Controllers/AnnouncementController.php @@ -0,0 +1,631 @@ +can('manage-announcements')) { + $query = Announcement::with(['departments', 'branches'])->where(function ($q) { + if (Auth::user()->can('manage-any-announcements')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-announcements')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhere('content', 'like', '%' . $request->search . '%'); + }); + } + + // Handle category filter + if ($request->has('category') && !empty($request->category)) { + $query->where('category', $request->category); + } + + // Handle department filter + if ($request->has('department_id') && !empty($request->department_id)) { + $query->where(function ($q) use ($request) { + $q->where('is_company_wide', true) + ->orWhereHas('departments', function ($q) use ($request) { + $q->where('departments.id', $request->department_id); + }); + }); + } + + // Handle branch filter + if ($request->has('branch_id') && !empty($request->branch_id)) { + $query->where(function ($q) use ($request) { + $q->where('is_company_wide', true) + ->orWhereHas('branches', function ($q) use ($request) { + $q->where('branches.id', $request->branch_id); + }); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status)) { + $today = now()->format('Y-m-d'); + + if ($request->status === 'active') { + $query->where('start_date', '<=', $today) + ->where(function ($q) use ($today) { + $q->whereNull('end_date') + ->orWhere('end_date', '>=', $today); + }); + } elseif ($request->status === 'upcoming') { + $query->where('start_date', '>', $today); + } elseif ($request->status === 'expired') { + $query->whereNotNull('end_date') + ->where('end_date', '<', $today); + } + } + + // Handle priority filter + if ($request->has('priority') && !empty($request->priority)) { + if ($request->priority === 'high') { + $query->where('is_high_priority', true); + } elseif ($request->priority === 'normal') { + $query->where('is_high_priority', false); + } + } + + // Handle featured filter + if ($request->has('featured') && $request->featured === 'true') { + $query->where('is_featured', true); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->where(function ($q) use ($request) { + $q->where('start_date', '>=', $request->date_from) + ->orWhere('end_date', '>=', $request->date_from); + }); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->where(function ($q) use ($request) { + $q->where('start_date', '<=', $request->date_to) + ->orWhere('end_date', '<=', $request->date_to); + }); + } + + // Handle sorting + $allowedSortFields = ['id', 'title', 'category', 'start_date', 'end_date', 'is_featured', 'is_high_priority', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field === 'date_range' ? 'start_date' : $request->sort_field; + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + } else { + $query->orderBy('created_at', 'desc'); + } + + $announcements = $query->paginate($request->per_page ?? 10); + + // Get departments for filter dropdown + $departments = Department::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get branches for filter dropdown + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get categories for filter dropdown + $categories = Announcement::whereIn('created_by', getCompanyAndUsersId()) + ->select('category') + ->distinct() + ->pluck('category') + ->toArray(); + + return Inertia::render('hr/announcements/index', [ + 'announcements' => $announcements, + 'departments' => $departments, + 'branches' => $branches, + 'categories' => $categories, + 'filters' => $request->all(['search', 'category', 'department_id', 'branch_id', 'status', 'priority', 'featured', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Display the dashboard view. + */ + public function dashboard(Request $request) + { + $today = now()->format('Y-m-d'); + + // Get all announcements (active, expired, upcoming) + $allAnnouncements = Announcement::with(['departments', 'branches']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->orderBy('is_high_priority', 'desc') + ->orderBy('is_featured', 'desc') + ->orderBy('created_at', 'desc') + ->get(); + + // Get featured announcements (from all announcements) + $featuredAnnouncements = Announcement::with(['departments', 'branches']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('is_featured', true) + ->orderBy('created_at', 'desc') + ->get(); + + // Get high priority announcements (from all announcements) + $highPriorityAnnouncements = Announcement::with(['departments', 'branches']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('is_high_priority', true) + ->orderBy('created_at', 'desc') + ->get(); + + // Get upcoming announcements + $upcomingAnnouncements = Announcement::with(['departments', 'branches']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('start_date', '>', $today) + ->orderBy('start_date', 'asc') + ->take(5) + ->get(); + + // Get categories for filter + $categories = Announcement::whereIn('created_by', getCompanyAndUsersId()) + ->select('category') + ->distinct() + ->pluck('category') + ->toArray(); + + // Get departments for filter + $departments = Department::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get branches for filter + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get employee for marking announcements as read + $employee = null; + if (Auth::user()->type !== 'company' && Auth::user()->type !== 'superadmin') { + $employee = User::where('id', Auth::id())->first(); + } + + return Inertia::render('hr/announcements/dashboard', [ + 'allAnnouncements' => $allAnnouncements, + 'featuredAnnouncements' => $featuredAnnouncements, + 'highPriorityAnnouncements' => $highPriorityAnnouncements, + 'upcomingAnnouncements' => $upcomingAnnouncements, + 'categories' => $categories, + 'departments' => $departments, + 'branches' => $branches, + 'employee' => $employee, + 'filters' => $request->all(['category', 'department_id', 'branch_id']), + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'category' => 'required|string|max:255', + 'description' => 'nullable|string', + 'content' => 'required|string', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'attachments' => 'nullable|string', + 'is_featured' => 'nullable|boolean', + 'is_high_priority' => 'nullable|boolean', + 'is_company_wide' => 'nullable|boolean', + 'department_ids' => 'nullable|string|required_if:is_company_wide,false', + 'branch_ids' => 'nullable|string|required_if:is_company_wide,false', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if departments and branches belong to current company + if ( + !$request->is_company_wide && + (empty($request->department_ids) && empty($request->branch_ids)) + ) { + return redirect()->back()->with('error', 'You must select at least one department or branch if the announcement is not company-wide'); + } + + if (!empty($request->department_ids)) { + $validDepartment = Department::where('created_by', createdBy()) + ->where('id', $request->department_ids) + ->exists(); + + if (!$validDepartment) { + return redirect()->back()->with('error', 'Invalid department selection'); + } + } + + if (!empty($request->branch_ids)) { + $validBranch = Branch::where('created_by', createdBy()) + ->where('id', $request->branch_ids) + ->exists(); + + if (!$validBranch) { + return redirect()->back()->with('error', 'Invalid branch selection'); + } + } + + $announcementData = [ + 'title' => $request->title, + 'category' => $request->category, + 'description' => $request->description, + 'content' => $request->content, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'is_featured' => $request->is_featured ?? false, + 'is_high_priority' => $request->is_high_priority ?? false, + 'is_company_wide' => $request->is_company_wide ?? true, + 'created_by' => creatorId(), + ]; + + // Handle attachment from media library + if ($request->has('attachments')) { + $announcementData['attachments'] = $request->attachments; + } + + $announcement = Announcement::create($announcementData); + + // Attach departments and branches if not company-wide + if (!$request->is_company_wide) { + if (!empty($request->department_ids)) { + $announcement->departments()->attach([$request->department_ids]); + } + + if (!empty($request->branch_ids)) { + $announcement->branches()->attach([$request->branch_ids]); + } + } + + return redirect()->back()->with('success', __('Announcement created successfully')); + } + + /** + * Display the specified resource. + */ + public function show(Announcement $announcement) + { + // Check if announcement belongs to current company + if (!in_array($announcement->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view this announcement')); + } + + // Load relationships + $announcement->load(['departments', 'branches']); + + // Get view statistics + $viewCount = $announcement->viewedBy()->count(); + $totalEmployees = User::where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->count(); + $viewPercentage = $totalEmployees > 0 ? round(($viewCount / $totalEmployees) * 100) : 0; + + // Mark as viewed if current user is an employee + if (Auth::user()->type !== 'company' && Auth::user()->type !== 'superadmin') { + $employee = User::where('id', Auth::id())->first(); + + if ($employee) { + // Check if already viewed + $existingView = AnnouncementView::where('announcement_id', $announcement->id) + ->where('employee_id', $employee->id) + ->first(); + + if (!$existingView) { + // Mark as viewed + $announcement->viewedBy()->attach($employee->id, [ + 'viewed_at' => now() + ]); + } + } + } + + return Inertia::render('hr/announcements/show', [ + 'announcement' => $announcement, + 'viewCount' => $viewCount, + 'totalEmployees' => $totalEmployees, + 'viewPercentage' => $viewPercentage, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Announcement $announcement) + { + // Check if announcement belongs to current company + if (!in_array($announcement->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this announcement'); + } + + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'category' => 'required|string|max:255', + 'description' => 'nullable|string', + 'content' => 'required|string', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'attachments' => 'nullable|string', + 'is_featured' => 'nullable|boolean', + 'is_high_priority' => 'nullable|boolean', + 'is_company_wide' => 'nullable|boolean', + 'department_ids' => 'nullable|string|required_if:is_company_wide,false', + 'branch_ids' => 'nullable|string|required_if:is_company_wide,false', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if departments and branches belong to current company + if ( + !$request->is_company_wide && + (empty($request->department_ids) && empty($request->branch_ids)) + ) { + return redirect()->back()->with('error', 'You must select at least one department or branch if the announcement is not company-wide'); + } + + if (!empty($request->department_ids)) { + $departmentId = $request->department_ids; + $validDepartment = Department::where('created_by', createdBy()) + ->where('id', $departmentId) + ->exists(); + + if (!$validDepartment) { + return redirect()->back()->with('error', 'Invalid department selection'); + } + } + + if (!empty($request->branch_ids)) { + $branchId = $request->branch_ids; + $validBranch = Branch::where('created_by', createdBy()) + ->where('id', $branchId) + ->exists(); + + if (!$validBranch) { + return redirect()->back()->with('error', 'Invalid branch selection'); + } + } + + $announcementData = [ + 'title' => $request->title, + 'category' => $request->category, + 'description' => $request->description, + 'content' => $request->content, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'is_featured' => $request->is_featured ?? false, + 'is_high_priority' => $request->is_high_priority ?? false, + 'is_company_wide' => $request->is_company_wide ?? true, + ]; + + // Handle attachment from media library + if ($request->has('attachments')) { + $announcementData['attachments'] = $request->attachments; + } + + $announcement->update($announcementData); + + // Sync departments and branches + if ($request->is_company_wide) { + $announcement->departments()->detach(); + $announcement->branches()->detach(); + } else { + if (!empty($request->department_ids)) { + $announcement->departments()->sync([$request->department_ids]); + } else { + $announcement->departments()->detach(); + } + + if (!empty($request->branch_ids)) { + $announcement->branches()->sync([$request->branch_ids]); + } else { + $announcement->branches()->detach(); + } + } + + return redirect()->back()->with('success', __('Announcement updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Announcement $announcement) + { + // Check if announcement belongs to current company + if (!in_array($announcement->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this announcement'); + } + + // Detach all departments and branches + $announcement->departments()->detach(); + $announcement->branches()->detach(); + + // Delete all views + $announcement->viewedBy()->detach(); + + // Delete the announcement + $announcement->delete(); + + return redirect()->back()->with('success', __('Announcement deleted successfully')); + } + + /** + * Download attachment file. + */ + public function downloadAttachment(Announcement $announcement) + { + // Check if announcement belongs to current company + if (!in_array($announcement->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this attachment')); + } + + if (!$announcement->attachments) { + return redirect()->back()->with('error', __('Attachment file not found')); + } + + $filePath = getStorageFilePath($announcement->attachments); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Attachment file not found')); + } + + return response()->download($filePath); + } + + /** + * Mark announcement as read for current employee. + */ + public function markAsRead(Request $request, Announcement $announcement) + { + $employee = User::where('id', Auth::id())->first(); + + if (!$employee) { + return response()->json(['error' => 'Employee not found'], 404); + } + + // Check if already viewed + $existingView = AnnouncementView::where('announcement_id', $announcement->id) + ->where('employee_id', $employee->id) + ->first(); + + if (!$existingView) { + // Mark as viewed + $announcement->viewedBy()->attach($employee->id, [ + 'viewed_at' => now() + ]); + } + + return response()->json(['success' => true]); + } + + /** + * Get announcement view statistics. + */ + public function viewStatistics(Announcement $announcement) + { + // Check if announcement belongs to current company + if (!in_array($announcement->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view these statistics')); + } + + // Load viewed by (employees) + $views = $announcement->viewedBy()->get(); + + // Get total employees + $totalEmployees = User::where('type', 'employee')->whereIn('created_by', getCompanyAndUsersId())->count(); + + // Get statistics only for announcement's target branch and department + $departmentStats = []; + $branchStats = []; + + if (!$announcement->is_company_wide) { + // Get target branch and department + $targetBranch = $announcement->branches->first(); + $targetDepartment = $announcement->departments->first(); + + if ($targetBranch) { + $branchEmployees = User::where('type', 'employee')->whereIn('created_by', getCompanyAndUsersId())->whereHas('employee', function ($q) use ($targetBranch) { + $q->where('branch_id', $targetBranch->id); + })->count(); + $branchViews = $announcement->viewedBy() + ->whereHas('employee', function ($q) use ($targetBranch) { + $q->where('branch_id', $targetBranch->id); + }) + ->count(); + + $branchStats[] = [ + 'branch' => $targetBranch->name, + 'total' => $branchEmployees, + 'viewed' => $branchViews, + 'percentage' => $branchEmployees > 0 ? round(($branchViews / $branchEmployees) * 100) : 0 + ]; + } + + if ($targetDepartment) { + $departmentEmployees = User::where('type', 'employee')->whereIn('created_by', getCompanyAndUsersId())->whereHas('employee', function ($q) use ($targetDepartment) { + $q->where('department_id', $targetDepartment->id); + })->count(); + + $departmentViews = $announcement->viewedBy() + ->whereHas('employee', function ($q) use ($targetDepartment) { + $q->where('department_id', $targetDepartment->id); + }) + ->count(); + + $departmentStats[] = [ + 'branch_name' => $targetBranch ? $targetBranch->name : 'Unknown', + 'departments' => [[ + 'department' => $targetDepartment->name, + 'total' => $departmentEmployees, + 'viewed' => $departmentViews, + 'percentage' => $departmentEmployees > 0 ? round(($departmentViews / $departmentEmployees) * 100) : 0 + ]] + ]; + } + } + + return Inertia::render('hr/announcements/statistics', [ + 'announcement' => $announcement, + 'totalEmployees' => $totalEmployees, + 'viewedCount' => $views->count(), + 'viewPercentage' => $totalEmployees > 0 ? round(($views->count() / $totalEmployees) * 100) : 0, + 'departmentStats' => $departmentStats, + 'branchStats' => $branchStats, + ]); + } + + /** + * Get departments based on selected branches. + */ + public function getDepartments($branchIds) + { + $branchIdArray = explode(',', $branchIds); + + $departments = Department::whereIn('branch_id', $branchIdArray) + ->whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get() + ->map(function ($dept) { + return [ + 'value' => $dept->id, + 'label' => $dept->name + ]; + }); + return response()->json($departments); + } +} diff --git a/app/Http/Controllers/AssetController.php b/app/Http/Controllers/AssetController.php new file mode 100644 index 000000000..8f1faa66c --- /dev/null +++ b/app/Http/Controllers/AssetController.php @@ -0,0 +1,1088 @@ +can('manage-assets')) { + // $query = Asset::withPermissionCheck()->with(['assetType', 'currentAssignment.employee']); + + $query = Asset::with(['assetType', 'currentAssignment.employee', 'assignments'])->where(function ($q) { + if (Auth::user()->can('manage-any-assets')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-assets')) { + $q->where('created_by', Auth::id()) + ->orWhereHas('currentAssignment', function ($aq) { + $aq->where('employee_id', Auth::id()); + }); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%'.$request->search.'%') + ->orWhere('serial_number', 'like', '%'.$request->search.'%') + ->orWhere('asset_code', 'like', '%'.$request->search.'%') + ->orWhere('description', 'like', '%'.$request->search.'%') + ->orWhere('location', 'like', '%'.$request->search.'%') + ->orWhere('supplier', 'like', '%'.$request->search.'%'); + }); + } + + // Handle asset type filter + if ($request->has('asset_type_id') && ! empty($request->asset_type_id)) { + $query->where('asset_type_id', $request->asset_type_id); + } + + // Handle status filter + if ($request->has('status') && ! empty($request->status)) { + $query->where('status', $request->status); + } + + // Handle condition filter + if ($request->has('condition') && ! empty($request->condition)) { + $query->where('condition', $request->condition); + } + + // Handle location filter + if ($request->has('location') && ! empty($request->location)) { + $query->where('location', $request->location); + } + + // Handle purchase date range filter + if ($request->has('purchase_date_from') && ! empty($request->purchase_date_from)) { + $query->whereDate('purchase_date', '>=', $request->purchase_date_from); + } + if ($request->has('purchase_date_to') && ! empty($request->purchase_date_to)) { + $query->whereDate('purchase_date', '<=', $request->purchase_date_to); + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'asset_code', 'serial_number', 'purchase_date', 'purchase_cost', 'status', 'condition', 'location', 'created_at']; + if ($request->has('sort_field') && ! empty($request->sort_field) && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + + $assets = $query->paginate($request->per_page ?? 10); + + // Get asset types for filter dropdown + $assetTypes = AssetType::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get unique locations for filter dropdown + $locations = Asset::whereIn('created_by', getCompanyAndUsersId()) + ->select('location') + ->distinct() + ->whereNotNull('location') + ->pluck('location') + ->toArray(); + + // Get employees for assignment dropdown + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + + return Inertia::render('hr/assets/index', [ + 'assets' => $assets, + 'assetTypes' => $assetTypes, + 'locations' => $locations, + 'employees' => $employees, + 'filters' => $request->all(['search', 'asset_type_id', 'status', 'condition', 'location', 'purchase_date_from', 'purchase_date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Display the dashboard view. + */ + public function dashboard(Request $request) + { + // Get asset counts by status + $assetCounts = [ + 'total' => Asset::whereIn('created_by', getCompanyAndUsersId())->count(), + 'available' => Asset::whereIn('created_by', getCompanyAndUsersId())->where('status', 'available')->count(), + 'assigned' => Asset::whereIn('created_by', getCompanyAndUsersId())->where('status', 'assigned')->count(), + 'under_maintenance' => Asset::whereIn('created_by', getCompanyAndUsersId())->where('status', 'under_maintenance')->count(), + 'disposed' => Asset::whereIn('created_by', getCompanyAndUsersId())->where('status', 'disposed')->count(), + ]; + + // Get asset counts by type + $assetTypeData = AssetType::whereIn('created_by', getCompanyAndUsersId()) + ->withCount('assets') + ->get() + ->map(function ($type) { + return [ + 'name' => $type->name, + 'count' => $type->assets_count, + ]; + }); + + // Get recent assignments + $recentAssignments = AssetAssignment::with(['asset', 'employee']) + ->whereHas('asset', function ($q) { + $q->whereIn('created_by', getCompanyAndUsersId()); + }) + ->orderBy('created_at', 'desc') + ->take(5) + ->get(); + + // Get upcoming maintenance + $upcomingMaintenance = AssetMaintenance::with('asset') + ->whereHas('asset', function ($q) { + $q->whereIn('created_by', getCompanyAndUsersId()); + }) + ->where('status', 'scheduled') + ->orderBy('start_date', 'asc') + ->take(5) + ->get(); + + // Get assets with expiring warranties + $expiringWarranties = Asset::whereIn('created_by', getCompanyAndUsersId()) + ->whereNotNull('warranty_expiry_date') + ->where('warranty_expiry_date', '>=', now()) + ->where('warranty_expiry_date', '<=', now()->addMonths(3)) + ->orderBy('warranty_expiry_date', 'asc') + ->take(5) + ->get(); + + // Get asset value summary + $assetValueSummary = [ + 'total_purchase_value' => Asset::whereIn('created_by', getCompanyAndUsersId())->sum('purchase_cost'), + 'total_current_value' => AssetDepreciation::whereHas('asset', function ($q) { + $q->whereIn('created_by', getCompanyAndUsersId()); + })->sum('current_value'), + 'total_depreciation' => Asset::whereIn('created_by', getCompanyAndUsersId())->sum('purchase_cost') - + AssetDepreciation::whereHas('asset', function ($q) { + $q->whereIn('created_by', getCompanyAndUsersId()); + })->sum('current_value'), + ]; + + return Inertia::render('hr/assets/dashboard', [ + 'assetCounts' => $assetCounts, + 'assetTypeData' => $assetTypeData, + 'recentAssignments' => $recentAssignments, + 'upcomingMaintenance' => $upcomingMaintenance, + 'expiringWarranties' => $expiringWarranties, + 'assetValueSummary' => $assetValueSummary, + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'asset_type_id' => 'required|exists:asset_types,id', + 'serial_number' => 'nullable|string|max:255', + 'asset_code' => 'nullable|string|max:255', + 'purchase_date' => 'nullable|date', + 'purchase_cost' => 'nullable|numeric|min:0', + 'status' => 'required|string|in:available,assigned,under_maintenance,disposed', + 'condition' => 'nullable|string|in:new,good,fair,poor', + 'description' => 'nullable|string', + 'location' => 'nullable|string|max:255', + 'supplier' => 'nullable|string|max:255', + 'warranty_info' => 'nullable|string|max:255', + 'warranty_expiry_date' => 'nullable|date', + 'images' => 'nullable|string', + 'documents' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if asset type belongs to current company + $assetType = AssetType::find($request->asset_type_id); + if (! $assetType || ! in_array($assetType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid asset type selected'); + } + + $assetData = [ + 'name' => $request->name, + 'asset_type_id' => $request->asset_type_id, + 'serial_number' => $request->serial_number, + 'asset_code' => $request->asset_code, + 'purchase_date' => $request->purchase_date, + 'purchase_cost' => $request->purchase_cost, + 'status' => $request->status, + 'condition' => $request->condition, + 'description' => $request->description, + 'location' => $request->location, + 'supplier' => $request->supplier, + 'warranty_info' => $request->warranty_info, + 'warranty_expiry_date' => $request->warranty_expiry_date, + 'created_by' => creatorId(), + ]; + + // Handle image from media library + if ($request->has('images')) { + $assetData['images'] = $request->images; + } + + // Handle document from media library + if ($request->has('documents')) { + $assetData['documents'] = $request->documents; + } + + $asset = Asset::create($assetData); + + // Generate QR code + // $qrCodeContent = json_encode([ + // 'id' => $asset->id, + // 'name' => $asset->name, + // 'asset_code' => $asset->asset_code, + // 'serial_number' => $asset->serial_number, + // 'type' => $assetType->name, + // ]); + + // $qrCodePath = 'assets/qrcodes/' . $asset->id . '.png'; + // $qrCode = QrCode::format('png') + // ->size(200) + // ->generate($qrCodeContent); + + // Storage::disk('public')->put($qrCodePath, $qrCode); + // $asset->update(['qr_code' => $qrCodePath]); + + // Create depreciation record if purchase cost and date are provided + if ($request->has('depreciation_method') && $request->purchase_cost && $request->purchase_date) { + AssetDepreciation::create([ + 'asset_id' => $asset->id, + 'method' => $request->depreciation_method, + 'useful_life_years' => $request->useful_life_years ?? 5, + 'salvage_value' => $request->salvage_value ?? ($request->purchase_cost * 0.1), // Default to 10% of purchase cost + 'current_value' => $request->purchase_cost, + 'last_calculated_date' => now(), + 'created_by' => creatorId(), + ]); + } + + return redirect()->back()->with('success', __('Asset created successfully')); + } + + /** + * Display the specified resource. + */ + public function show(Asset $asset) + { + // Check if asset belongs to current company + if (! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view this asset')); + } + + // Load relationships + $asset->load([ + 'assetType', + 'assignments.employee', + 'assignments.assigner', + 'assignments.receiver', + 'maintenances', + 'depreciation', + 'currentAssignment.employee', + ]); + + // Get asset types for form dropdown + $assetTypes = AssetType::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get employees for assignment dropdown + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + + return Inertia::render('hr/assets/show', [ + 'asset' => $asset, + 'assetTypes' => $assetTypes, + 'employees' => $employees, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Asset $asset) + { + // Check if asset belongs to current company + if (! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this asset'); + } + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'asset_type_id' => 'required|exists:asset_types,id', + 'serial_number' => 'nullable|string|max:255', + 'asset_code' => 'nullable|string|max:255', + 'purchase_date' => 'nullable|date', + 'purchase_cost' => 'nullable|numeric|min:0', + 'status' => 'required|string|in:available,assigned,under_maintenance,disposed', + 'condition' => 'nullable|string|in:new,good,fair,poor', + 'description' => 'nullable|string', + 'location' => 'nullable|string|max:255', + 'supplier' => 'nullable|string|max:255', + 'warranty_info' => 'nullable|string|max:255', + 'warranty_expiry_date' => 'nullable|date', + 'images' => 'nullable|string', + 'documents' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if asset type belongs to current company + $assetType = AssetType::find($request->asset_type_id); + if (! $assetType || ! in_array($assetType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid asset type selected'); + } + + $assetData = [ + 'name' => $request->name, + 'asset_type_id' => $request->asset_type_id, + 'serial_number' => $request->serial_number, + 'asset_code' => $request->asset_code, + 'purchase_date' => $request->purchase_date, + 'purchase_cost' => $request->purchase_cost, + 'status' => $request->status, + 'condition' => $request->condition, + 'description' => $request->description, + 'location' => $request->location, + 'supplier' => $request->supplier, + 'warranty_info' => $request->warranty_info, + 'warranty_expiry_date' => $request->warranty_expiry_date, + ]; + + // Handle image from media library + if ($request->has('images')) { + $assetData['images'] = $request->images; + } + + // Handle document from media library + if ($request->has('documents')) { + $assetData['documents'] = $request->documents; + } + + $asset->update($assetData); + + // Update or create depreciation record if purchase cost and date are provided + if ($request->has('depreciation_method') && $request->purchase_cost && $request->purchase_date) { + $depreciation = $asset->depreciation; + + if ($depreciation) { + $depreciation->update([ + 'method' => $request->depreciation_method, + 'useful_life_years' => $request->useful_life_years ?? $depreciation->useful_life_years, + 'salvage_value' => $request->salvage_value ?? $depreciation->salvage_value, + 'last_calculated_date' => now(), + ]); + + // Recalculate current value + $depreciation->updateCurrentValue(); + } else { + AssetDepreciation::create([ + 'asset_id' => $asset->id, + 'method' => $request->depreciation_method, + 'useful_life_years' => $request->useful_life_years ?? 5, + 'salvage_value' => $request->salvage_value ?? ($request->purchase_cost * 0.1), // Default to 10% of purchase cost + 'current_value' => $request->purchase_cost, + 'last_calculated_date' => now(), + 'created_by' => creatorId(), + ]); + } + } + + return redirect()->back()->with('success', __('Asset updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Asset $asset) + { + // Check if asset belongs to current company + if (! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this asset')); + } + + // Check if asset is currently assigned + if ($asset->status === 'assigned') { + return redirect()->back()->with('error', __('Cannot delete an asset that is currently assigned')); + } + + // Delete associated files + if ($asset->images) { + Storage::disk('public')->delete($asset->images); + } + if ($asset->documents) { + Storage::disk('public')->delete($asset->documents); + } + if ($asset->qr_code) { + Storage::disk('public')->delete($asset->qr_code); + } + + // Delete associated records + $asset->assignments()->delete(); + $asset->maintenances()->delete(); + if ($asset->depreciation) { + $asset->depreciation->delete(); + } + + $asset->delete(); + + return redirect()->back()->with('success', __('Asset deleted successfully')); + } + + /** + * Assign asset to an employee. + */ + public function assign(Request $request, Asset $asset) + { + // Check if asset belongs to current company + if (! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to assign this asset')); + } + + // Check if asset is available + if ($asset->status !== 'available') { + return redirect()->back()->with('error', __('Only available assets can be assigned')); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'checkout_date' => 'required|date', + 'expected_return_date' => 'nullable|date|after_or_equal:checkout_date', + 'checkout_condition' => 'nullable|string|in:new,good,fair,poor', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $user = User::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (! $user) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Create assignment + AssetAssignment::create([ + 'asset_id' => $asset->id, + 'employee_id' => $request->employee_id, + 'checkout_date' => $request->checkout_date, + 'expected_return_date' => $request->expected_return_date, + 'checkout_condition' => $request->checkout_condition ?? $asset->condition, + 'notes' => $request->notes, + 'assigned_by' => auth()->id(), + ]); + + // Update asset status + $asset->update([ + 'status' => 'assigned', + ]); + + return redirect()->back()->with('success', __('Asset assigned successfully')); + } + + /** + * Return an assigned asset. + */ + public function returnAsset(Request $request, Asset $asset) + { + // Check if asset belongs to current company + if (! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to return this asset')); + } + + // Check if asset is assigned + if ($asset->status !== 'assigned') { + return redirect()->back()->with('error', __('Only assigned assets can be returned')); + } + + $validator = Validator::make($request->all(), [ + 'checkin_date' => 'required|date', + 'checkin_condition' => 'nullable|string|in:new,good,fair,poor', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Get current assignment + $assignment = $asset->currentAssignment; + if (! $assignment) { + return redirect()->back()->with('error', __('No active assignment found for this asset')); + } + + // Update assignment + $assignment->update([ + 'checkin_date' => $request->checkin_date, + 'checkin_condition' => $request->checkin_condition ?? $asset->condition, + 'notes' => $assignment->notes."\n\nReturn notes: ".($request->notes ?? 'No notes provided.'), + 'received_by' => auth()->id(), + ]); + + // Update asset status and condition + $asset->update([ + 'status' => 'available', + 'condition' => $request->checkin_condition ?? $asset->condition, + ]); + + return redirect()->back()->with('success', __('Asset returned successfully')); + } + + /** + * Schedule maintenance for an asset. + */ + public function scheduleMaintenance(Request $request, Asset $asset) + { + // Check if asset belongs to current company + if (! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to schedule maintenance for this asset')); + } + + $validator = Validator::make($request->all(), [ + 'maintenance_type' => 'required|string|max:255', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'cost' => 'nullable|numeric|min:0', + 'details' => 'nullable|string', + 'supplier' => 'nullable|string|max:255', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Create maintenance record + AssetMaintenance::create([ + 'asset_id' => $asset->id, + 'maintenance_type' => $request->maintenance_type, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'cost' => $request->cost, + 'status' => 'scheduled', + 'details' => $request->details, + 'supplier' => $request->supplier, + 'created_by' => auth()->id(), + ]); + + // Update asset status if maintenance is starting today or has already started + if ($request->start_date <= now()->format('Y-m-d')) { + $asset->update([ + 'status' => 'under_maintenance', + ]); + } + + return redirect()->back()->with('success', __('Maintenance scheduled successfully')); + } + + /** + * Update maintenance status. + */ + public function updateMaintenance(Request $request, AssetMaintenance $maintenance) + { + // Check if maintenance belongs to current company + $asset = $maintenance->asset; + if (! $asset || ! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this maintenance record')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:scheduled,in_progress,completed,cancelled', + 'end_date' => 'nullable|date|after_or_equal:'.$maintenance->start_date, + 'completion_notes' => 'nullable|string', + 'cost' => 'nullable|numeric|min:0', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Update maintenance record + $maintenance->update([ + 'status' => $request->status, + 'end_date' => $request->end_date, + 'completion_notes' => $request->completion_notes, + 'cost' => $request->cost ?? $maintenance->cost, + ]); + + // Update asset status based on maintenance status + if (in_array($request->status, ['completed', 'cancelled'])) { + $asset->update([ + 'status' => 'available', + ]); + } elseif (in_array($request->status, ['scheduled', 'in_progress'])) { + if ($request->status === 'in_progress' || $maintenance->start_date <= now()->format('Y-m-d')) { + $asset->update([ + 'status' => 'under_maintenance', + ]); + } + } + + return redirect()->back()->with('success', __('Maintenance record updated successfully')); + } + + /** + * Download document file. + */ + public function downloadDocument(Asset $asset) + { + // Check if asset belongs to current company + if (! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this document')); + } + + if (! $asset->documents) { + return redirect()->back()->with('error', __('Document file not found')); + } + + // Handle cloud storage URLs (already full URLs) + if (filter_var($asset->documents, FILTER_VALIDATE_URL)) { + return Storage::download($asset->documents); + } + + // Handle local storage paths + $relativePath = str_replace('/Product/hrmgo-saas-react/storage/', '', $asset->documents); + + if (! Storage::exists($relativePath)) { + return redirect()->back()->with('error', __('Document file not found')); + } + + return Storage::download($relativePath); + } + + /** + * View image file. + */ + public function viewImage(Asset $asset) + { + // Check if asset belongs to current company + if (! in_array($asset->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this image')); + } + + if (! $asset->images) { + return redirect()->back()->with('error', __('Image file not found')); + } + + // Handle cloud storage URLs (already full URLs) + if (filter_var($asset->images, FILTER_VALIDATE_URL)) { + return redirect($asset->images); + } + + // Handle local storage paths + $relativePath = str_replace('/Product/hrmgo-saas-react/storage/', '', $asset->images); + + if (! Storage::exists($relativePath)) { + return redirect()->back()->with('error', __('Image file not found')); + } + + return Storage::response($relativePath); + } + + /** + * Generate depreciation report. + */ + public function depreciationReport(Request $request) + { + $companyUserIds = getCompanyAndUsersId(); + + $query = Asset::with(['depreciation', 'assetType']) + ->whereHas('depreciation') + ->whereIn('created_by', $companyUserIds); + + // Handle asset type filter + if ($request->filled('asset_type_id')) { + $query->where('asset_type_id', $request->asset_type_id); + } + + // Handle purchase date range filter + if ($request->filled('purchase_date_from')) { + $query->whereDate('purchase_date', '>=', $request->purchase_date_from); + } + if ($request->filled('purchase_date_to')) { + $query->whereDate('purchase_date', '<=', $request->purchase_date_to); + } + + // Handle sorting + $allowedSortFields = ['name', 'purchase_date', 'purchase_cost']; + if ($request->filled('sort_field') && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('purchase_date', 'desc'); + } + + // Calculate totals across ALL filtered records (not just current page) + $totalsQuery = Asset::whereHas('depreciation') + ->whereIn('created_by', $companyUserIds); + + if ($request->filled('asset_type_id')) { + $totalsQuery->where('asset_type_id', $request->asset_type_id); + } + if ($request->filled('purchase_date_from')) { + $totalsQuery->whereDate('purchase_date', '>=', $request->purchase_date_from); + } + if ($request->filled('purchase_date_to')) { + $totalsQuery->whereDate('purchase_date', '<=', $request->purchase_date_to); + } + $totalPurchaseValue = $totalsQuery->sum('purchase_cost'); + $totalCurrentValue = AssetDepreciation::whereIn('asset_id', (clone $totalsQuery)->pluck('id'))->sum('current_value'); + $totalDepreciation = $totalPurchaseValue - $totalCurrentValue; + + $assets = $query->paginate($request->per_page ?? 10); + + // Get asset types for filter dropdown + $assetTypes = AssetType::whereIn('created_by', $companyUserIds) + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/assets/depreciation-report', [ + 'assets' => $assets, + 'assetTypes' => $assetTypes, + 'totalPurchaseValue' => $totalPurchaseValue, + 'totalCurrentValue' => $totalCurrentValue, + 'totalDepreciation' => $totalDepreciation, + 'filters' => $request->all(['asset_type_id', 'purchase_date_from', 'purchase_date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } + + /** + * Export depreciation report to CSV. + */ + public function exportDepreciationCsv(Request $request) + { + $query = Asset::with(['assetType', 'depreciation']) + ->whereHas('depreciation') + ->whereIn('created_by', getCompanyAndUsersId()); + + // Apply same filters as report + if ($request->has('asset_type_id') && ! empty($request->asset_type_id)) { + $query->where('asset_type_id', $request->asset_type_id); + } + + if ($request->has('purchase_date_from') && ! empty($request->purchase_date_from)) { + $query->whereDate('purchase_date', '>=', $request->purchase_date_from); + } + if ($request->has('purchase_date_to') && ! empty($request->purchase_date_to)) { + $query->whereDate('purchase_date', '<=', $request->purchase_date_to); + } + + $assets = $query->orderBy('purchase_date', 'desc')->get(); + + $csvData = []; + $csvData[] = ['Asset Name', 'Asset Type', 'Purchase Date', 'Purchase Cost', 'Current Value', 'Depreciation', 'Depreciation Method', 'Useful Life (Years)']; + + foreach ($assets as $asset) { + $depreciation = $asset->depreciation; + $csvData[] = [ + $asset->name, + $asset->assetType->name ?? '', + $asset->purchase_date ? date('Y-m-d', strtotime($asset->purchase_date)) : '', + number_format($asset->purchase_cost, 2), + number_format($depreciation->current_value ?? 0, 2), + number_format(($asset->purchase_cost - ($depreciation->current_value ?? 0)), 2), + ucfirst(str_replace('_', ' ', $depreciation->method ?? '')), + $depreciation->useful_life_years ?? '', + ]; + } + + $filename = 'depreciation-report-'.date('Y-m-d').'.csv'; + + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="'.$filename.'"', + ]; + + $callback = function () use ($csvData) { + $file = fopen('php://output', 'w'); + foreach ($csvData as $row) { + fputcsv($file, $row); + } + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } + + /** + * Export assets to CSV. + */ + public function export() + { + if (Auth::user()->can('export-assets')) { + try { + $assets = Asset::with(['assetType', 'currentAssignment.employee', 'assignments'])->where(function ($q) { + if (Auth::user()->can('manage-any-assets')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-assets')) { + $q->where('created_by', Auth::id()) + ->orWhereHas('currentAssignment', function ($aq) { + $aq->where('employee_id', Auth::id()); + }); + } else { + $q->whereRaw('1 = 0'); + } + })->orderBy('id', 'desc')->get(); + + $fileName = 'assets_'.date('Y-m-d_His').'.csv'; + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="'.$fileName.'"', + ]; + + $callback = function () use ($assets) { + $file = fopen('php://output', 'w'); + fputcsv($file, [ + 'Name', + 'Asset Type', + 'Serial Number', + 'Asset Code', + 'Purchase Date', + 'Purchase Cost', + 'Status', + 'Assigned To', + 'Condition', + 'Description', + 'Location', + 'Supplier', + 'Warranty Info', + 'Warranty Expiry Date', + ]); + + foreach ($assets as $asset) { + fputcsv($file, [ + $asset->name, + $asset->assetType->name ?? '', + $asset->serial_number ?? '', + $asset->asset_code ?? '', + $asset->purchase_date ?? '', + $asset->purchase_cost ?? '', + $asset->status ?? '', + $asset->currentAssignment->employee->name ?? 'Not Assign', + $asset->condition ?? '', + $asset->description ?? '', + $asset->location ?? '', + $asset->supplier ?? '', + $asset->warranty_info ?? '', + $asset->warranty_expiry_date ?? '', + ]); + } + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to export assets: :message', ['message' => $e->getMessage()])], 500); + } + } else { + return response()->json(['message' => __('Permission Denied.')], 403); + } + } + + /** + * Download sample template. + */ + public function downloadTemplate() + { + $filePath = storage_path('uploads/sample/sample-asset.xlsx'); + if (! file_exists($filePath)) { + return response()->json(['error' => __('Template file not available')], 404); + } + + return response()->download($filePath, 'sample-asset.xlsx'); + } + + /** + * Parse uploaded file. + */ + public function parseFile(Request $request) + { + if (Auth::user()->can('import-assets')) { + $rules = ['file' => 'required|mimes:csv,txt,xlsx,xls']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return response()->json(['message' => $validator->getMessageBag()->first()]); + } + + try { + $file = $request->file('file'); + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file->getRealPath()); + $worksheet = $spreadsheet->getActiveSheet(); + $highestColumn = $worksheet->getHighestColumn(); + $highestRow = $worksheet->getHighestRow(); + $headers = []; + + for ($col = 'A'; $col <= $highestColumn; $col++) { + $value = $worksheet->getCell($col.'1')->getValue(); + if ($value) { + $headers[] = (string) $value; + } + } + + $previewData = []; + for ($row = 2; $row <= $highestRow; $row++) { + $rowData = []; + $colIndex = 0; + for ($col = 'A'; $col <= $highestColumn; $col++) { + if ($colIndex < count($headers)) { + $rowData[$headers[$colIndex]] = (string) $worksheet->getCell($col.$row)->getValue(); + } + $colIndex++; + } + $previewData[] = $rowData; + } + + return response()->json(['excelColumns' => $headers, 'previewData' => $previewData]); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to parse file: :error', ['error' => $e->getMessage()])]); + } + } else { + return response()->json(['message' => __('Permission denied.')], 403); + } + } + + /** + * Import assets from file. + */ + public function fileImport(Request $request) + { + if (Auth::user()->can('import-assets')) { + $rules = ['data' => 'required|array']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return redirect()->back()->with('error', $validator->getMessageBag()->first()); + } + + try { + $data = $request->data; + $imported = 0; + $skipped = 0; + + foreach ($data as $row) { + try { + if (empty($row['name'])) { + $skipped++; + + continue; + } + + // Resolve asset type + $assetTypeId = null; + if (! empty($row['asset_type'])) { + $assetType = AssetType::whereIn('created_by', getCompanyAndUsersId()) + ->where('name', $row['asset_type']) + ->first(); + $assetTypeId = $assetType ? $assetType->id : null; + } + + if (! $assetTypeId) { + $skipped++; + + continue; + } + + // Check if asset with same name and asset type already exists + $existingAsset = Asset::whereIn('created_by', getCompanyAndUsersId()) + ->where('name', $row['name']) + ->where('asset_type_id', $assetTypeId) + ->exists(); + + if ($existingAsset) { + $skipped++; + + continue; + } + + Asset::create([ + 'name' => $row['name'], + 'asset_type_id' => $assetTypeId, + 'serial_number' => $row['serial_number'] ?? null, + 'asset_code' => $row['asset_code'] ?? null, + 'purchase_date' => ! empty($row['purchase_date']) ? $row['purchase_date'] : null, + 'purchase_cost' => $row['purchase_cost'] ?? null, + 'status' => $row['status'] ?? 'available', + 'condition' => $row['condition'] ?? 'good', + 'description' => $row['description'] ?? null, + 'location' => $row['location'] ?? null, + 'supplier' => $row['supplier'] ?? null, + 'warranty_info' => $row['warranty_info'] ?? null, + 'warranty_expiry_date' => ! empty($row['warranty_expiry_date']) ? $row['warranty_expiry_date'] : null, + 'created_by' => creatorId(), + ]); + + $imported++; + } catch (\Exception $e) { + $skipped++; + } + } + + return redirect()->back()->with('success', + __('Import completed: :added assets added, :skipped assets skipped', [ + 'added' => $imported, + 'skipped' => $skipped, + ]) + ); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to import: :error', ['error' => $e->getMessage()])); + } + } else { + return redirect()->back()->with('error', __('Permission denied.')); + } + } +} diff --git a/app/Http/Controllers/AssetTypeController.php b/app/Http/Controllers/AssetTypeController.php new file mode 100644 index 000000000..bba42d119 --- /dev/null +++ b/app/Http/Controllers/AssetTypeController.php @@ -0,0 +1,126 @@ +can('manage-asset-types')) { + $query = AssetType::withCount('assets')->where(function ($q) { + if (Auth::user()->can('manage-any-asset-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-asset-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'description', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field) && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + + $assetTypes = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/assets/types/index', [ + 'assetTypes' => $assetTypes, + 'filters' => $request->all(['search', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + AssetType::create([ + 'name' => $request->name, + 'description' => $request->description, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Asset type created successfully')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, AssetType $assetType) + { + // Check if asset type belongs to current company + if (!in_array($assetType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this asset type')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $assetType->update([ + 'name' => $request->name, + 'description' => $request->description, + ]); + + return redirect()->back()->with('success', __('Asset type updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(AssetType $assetType) + { + // Check if asset type belongs to current company + if (!in_array($assetType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this asset type')); + } + + // Check if asset type is being used by any assets + if ($assetType->assets()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete asset type that is being used by assets')); + } + + $assetType->delete(); + + return redirect()->back()->with('success', __('Asset type deleted successfully')); + } +} diff --git a/app/Http/Controllers/AttendancePolicyController.php b/app/Http/Controllers/AttendancePolicyController.php new file mode 100644 index 000000000..8698c979b --- /dev/null +++ b/app/Http/Controllers/AttendancePolicyController.php @@ -0,0 +1,190 @@ +can('manage-attendance-policies')) { + $query = AttendancePolicy::with(['creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-attendance-policies')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-attendance-policies')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle overtime calculation filter + if ($request->has('overtime_calculation') && !empty($request->overtime_calculation) && $request->overtime_calculation !== 'all') { + $query->where('overtime_calculation', $request->overtime_calculation); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'name') { + $query->orderBy('name', $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + } else { + $query->orderBy('created_at', 'desc'); + } + + $attendancePolicies = $query->paginate($request->per_page ?? 9); + + // Stats always calculated from ALL records — never affected by filters or pagination + $allPolicies = AttendancePolicy::where(function ($q) { + if (Auth::user()->can('manage-any-attendance-policies')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-attendance-policies')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + $stats = [ + 'total' => (clone $allPolicies)->count(), + 'active' => (clone $allPolicies)->where('status', 'active')->count(), + 'avg_late_grace' => (int) round((clone $allPolicies)->avg('late_arrival_grace') ?? 0), + 'avg_overtime_rate'=> (float) ((clone $allPolicies)->avg('overtime_rate_per_hour') ?? 0), + ]; + + return Inertia::render('hr/attendance-policies/index', [ + 'attendancePolicies' => $attendancePolicies, + 'stats' => $stats, + 'filters' => $request->all(['search', 'status', 'overtime_calculation', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'late_arrival_grace' => 'required|integer|min:0', + 'early_departure_grace' => 'required|integer|min:0', + 'overtime_rate_per_hour' => 'required|numeric|min:0', + 'status' => 'nullable|in:active,inactive', + ]); + + $validated['created_by'] = creatorId(); + $validated['status'] = $validated['status'] ?? 'active'; + + // Check if policy with same name already exists + $exists = AttendancePolicy::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Attendance policy with this name already exists.')); + } + + AttendancePolicy::create($validated); + + return redirect()->back()->with('success', __('Attendance policy created successfully.')); + } + + public function update(Request $request, $attendancePolicyId) + { + $attendancePolicy = AttendancePolicy::where('id', $attendancePolicyId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($attendancePolicy) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'late_arrival_grace' => 'required|integer|min:0', + 'early_departure_grace' => 'required|integer|min:0', + 'overtime_rate_per_hour' => 'required|numeric|min:0', + 'status' => 'nullable|in:active,inactive', + ]); + + // Check if policy with same name already exists (excluding current) + $exists = AttendancePolicy::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('id', '!=', $attendancePolicyId) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Attendance policy with this name already exists.')); + } + + $attendancePolicy->update($validated); + + return redirect()->back()->with('success', __('Attendance policy updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update attendance policy')); + } + } else { + return redirect()->back()->with('error', __('Attendance policy Not Found.')); + } + } + + public function destroy($attendancePolicyId) + { + $attendancePolicy = AttendancePolicy::where('id', $attendancePolicyId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($attendancePolicy) { + try { + $attendancePolicy->delete(); + return redirect()->back()->with('success', __('Attendance policy deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete attendance policy')); + } + } else { + return redirect()->back()->with('error', __('Attendance policy Not Found.')); + } + } + + public function toggleStatus($attendancePolicyId) + { + $attendancePolicy = AttendancePolicy::where('id', $attendancePolicyId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($attendancePolicy) { + try { + $attendancePolicy->status = $attendancePolicy->status === 'active' ? 'inactive' : 'active'; + $attendancePolicy->save(); + + return redirect()->back()->with('success', __('Attendance policy status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update attendance policy status')); + } + } else { + return redirect()->back()->with('error', __('Attendance policy Not Found.')); + } + } +} diff --git a/app/Http/Controllers/AttendanceRecordController.php b/app/Http/Controllers/AttendanceRecordController.php new file mode 100644 index 000000000..d5aea4d5f --- /dev/null +++ b/app/Http/Controllers/AttendanceRecordController.php @@ -0,0 +1,742 @@ +can('manage-attendance-records')) { + $query = AttendanceRecord::with(['employee', 'shift', 'attendancePolicy', 'creator']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-attendance-records')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-attendance-records')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->whereHas('employee', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%'.$request->search.'%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && ! empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + // Handle status filter + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && ! empty($request->date_from)) { + $query->where('date', '>=', $request->date_from); + } + if ($request->has('date_to') && ! empty($request->date_to)) { + $query->where('date', '<=', $request->date_to); + } + + // Handle sorting + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'date') { + $query->orderBy('date', $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('date', 'desc'); + } + + $attendanceRecords = $query->paginate($request->per_page ?? 9); + + // Load avatar dynamically — same pattern as AwardController + $attendanceRecords->getCollection()->transform(function ($record) { + if ($record->employee) { + $rawAvatar = $record->employee->getRawOriginal('avatar'); + $record->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + + // Add leave type information for on_leave records + if ($record->status === 'on_leave') { + $leaveApplication = LeaveApplication::where('employee_id', $record->employee_id) + ->whereDate('start_date', '<=', $record->date) + ->whereDate('end_date', '>=', $record->date) + ->where('status', 'approved') + ->with('leaveType') + ->first(); + + $record->leave_type = $leaveApplication?->leaveType; + } + + return $record; + }); + + // Get employees for filter dropdown + $employees = User::where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name']); + + $companyUserIds = getCompanyAndUsersId(); + + if (isDemo()) { + $statsRecords = AttendanceRecord::whereIn('created_by', $companyUserIds)->get(); + + $todayStats = [ + 'present' => $statsRecords->where('status', 'present')->count(), + 'on_leave' => LeaveApplication::whereIn('employee_id', function ($q) use ($companyUserIds) { + $q->select('user_id')->from('employees')->whereIn('created_by', $companyUserIds); + })->where('status', 'approved')->count(), + 'late_arrivals' => $statsRecords->where('is_late', true)->count(), + 'overtime' => $statsRecords->where('overtime_hours', '>', 0)->count(), + ]; + } else { + $today = Carbon::today(); + + $todayRecords = AttendanceRecord::whereIn('created_by', $companyUserIds) + ->whereDate('date', $today) + ->get(); + + $todayStats = [ + 'present' => $todayRecords->where('status', 'present')->count(), + 'on_leave' => LeaveApplication::whereIn('employee_id', function ($q) use ($companyUserIds) { + $q->select('user_id')->from('employees')->whereIn('created_by', $companyUserIds); + })->where('status', 'approved')->whereDate('start_date', '<=', $today)->whereDate('end_date', '>=', $today)->count(), + 'late_arrivals' => $todayRecords->where('is_late', true)->count(), + 'overtime' => $todayRecords->where('overtime_hours', '>', 0)->count(), + ]; + } + + return Inertia::render('hr/attendance-records/index', [ + 'attendanceRecords' => $attendanceRecords, + 'employees' => $this->getFilteredEmployees(), + 'hasSampleFile' => file_exists(storage_path('uploads/sample/sample-attendance-record.xlsx')), + 'filters' => $request->all(['search', 'employee_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + 'todayStats' => $todayStats, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-attendance-records') && ! Auth::user()->can('manage-any-attendance-records')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + + return $employees; + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'date' => 'required|date', + 'clock_in' => 'nullable|date_format:H:i', + 'clock_out' => 'nullable|date_format:H:i', + 'is_holiday' => 'boolean', + 'notes' => 'nullable|string', + ]); + + // Get employee with shift and policy + $employee = Employee::where('user_id', $validated['employee_id'])->first(); + + // Get working days from settings + $globalSettings = settings(); + $workingDaysIndices = json_decode($globalSettings['working_days'] ?? '[]', true); + + if (empty($workingDaysIndices)) { + return redirect()->back()->with('error', __('Please configure working days first.')); + } + + $dateIndex = Carbon::parse($validated['date'])->dayOfWeek; + if (! in_array($dateIndex, $workingDaysIndices)) { + return redirect()->back()->with('error', __('Cannot create attendance record for non-working day.')); + } + + // Check if employee has approved leave for this date + $hasApprovedLeave = LeaveApplication::where('employee_id', $validated['employee_id']) + ->where('status', 'approved') + ->whereDate('start_date', '<=', $validated['date']) + ->whereDate('end_date', '>=', $validated['date']) + ->exists(); + + if ($hasApprovedLeave) { + return redirect()->back()->with('error', __('Employee has approved leave for this date. Cannot create attendance record.')); + } + + // Check if record already exists + $exists = AttendanceRecord::where('employee_id', $validated['employee_id']) + ->where('date', $validated['date']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Attendance record already exists for this employee and date.')); + } + + // Use employee's assigned shift and policy, or get defaults + $shift = $employee && $employee->shift_id ? + Shift::find($employee->shift_id) : + Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); + + $policy = $employee && $employee->attendance_policy_id ? + AttendancePolicy::find($employee->attendance_policy_id) : + AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); + + $validated['shift_id'] = $shift?->id; + $validated['attendance_policy_id'] = $policy?->id; + $validated['created_by'] = creatorId(); + $validated['is_holiday'] = $validated['is_holiday'] ?? false; + $validated['break_hours'] = $validated['break_hours'] ?? 0; + + // Set weekend flag + $validated['is_weekend'] = Carbon::parse($validated['date'])->isWeekend(); + + $record = AttendanceRecord::create($validated); + + // Process complete attendance calculation + $record->fresh(); // Reload to get relationships + $record->processAttendance(); + + return redirect()->back()->with('success', __('Attendance record created successfully.')); + } + + public function update(Request $request, $attendanceRecordId) + { + + $attendanceRecord = AttendanceRecord::where('id', $attendanceRecordId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + // Get working days from settings + $globalSettings = settings(); + $workingDaysIndices = json_decode($globalSettings['working_days'] ?? '[]', true); + + if (empty($workingDaysIndices)) { + return redirect()->back()->with('error', __('Please configure working days first.')); + } + + $dateIndex = Carbon::parse($request->date)->dayOfWeek; + if (! in_array($dateIndex, $workingDaysIndices)) { + return redirect()->back()->with('error', __('Cannot create attendance record for non-working day.')); + } + + // Check if employee has approved leave for this date + $hasApprovedLeave = LeaveApplication::where('employee_id', $request->employee_id) + ->where('status', 'approved') + ->whereDate('start_date', '<=', $request->date) + ->whereDate('end_date', '>=', $request->date) + ->exists(); + + if ($hasApprovedLeave) { + return redirect()->back()->with('error', __('Employee has approved leave for this date. Cannot create attendance record.')); + } + + if ($attendanceRecord) { + try { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'date' => 'required|date', + 'clock_in' => 'nullable|date_format:H:i', + 'clock_out' => 'nullable|date_format:H:i', + 'break_hours' => 'nullable|numeric|min:0', + 'is_holiday' => 'boolean', + 'status' => 'required|in:present,absent,half_day,on_leave,holiday', + 'notes' => 'nullable|string', + ]); + + // Check if employee or date changed and if duplicate exists + if ($attendanceRecord->employee_id != $validated['employee_id'] || $attendanceRecord->date != $validated['date']) { + $exists = AttendanceRecord::where('employee_id', $validated['employee_id']) + ->where('date', $validated['date']) + ->where('id', '!=', $attendanceRecordId) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Attendance record already exists for this employee and date.')); + } + } + + // Get employee with shift and policy + $employee = \App\Models\Employee::where('user_id', $validated['employee_id'])->first(); + + // Use employee's assigned shift and policy, or get defaults + $shift = $employee && $employee->shift_id ? + Shift::find($employee->shift_id) : + Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); + + $policy = $employee && $employee->attendance_policy_id ? + AttendancePolicy::find($employee->attendance_policy_id) : + AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); + + $validated['shift_id'] = $shift?->id; + $validated['attendance_policy_id'] = $policy?->id; + + // Set weekend flag + $validated['is_weekend'] = Carbon::parse($validated['date'])->isWeekend(); + + $attendanceRecord->update($validated); + + // Process complete attendance calculation + $attendanceRecord->fresh(); // Reload to get relationships + $attendanceRecord->processAttendance(); + + return redirect()->back()->with('success', __('Attendance record updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update attendance record')); + } + } else { + return redirect()->back()->with('error', __('Attendance record Not Found.')); + } + } + + public function destroy($attendanceRecordId) + { + $attendanceRecord = AttendanceRecord::where('id', $attendanceRecordId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($attendanceRecord) { + try { + $attendanceRecord->delete(); + + return redirect()->back()->with('success', __('Attendance record deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete attendance record')); + } + } else { + return redirect()->back()->with('error', __('Attendance record Not Found.')); + } + } + + public function clockIn(Request $request) + { + if (Auth::user()->can('clock-in-out')) { + try { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + ]); + + $settings = settings(); + if (! empty($settings['ipRestrictionEnabled']) && $settings['ipRestrictionEnabled'] == 1) { + $loginUserIp = request()->ip(); + $ip = IpRestriction::whereIn('created_by', getCompanyAndUsersId())->where('ip_address', $loginUserIp)->first(); + if (empty($ip) || is_null($ip)) { + return redirect()->back()->with('error', __('This IP Address Is Not Allowed For Clock In & Clock Out.')); + } + } + + $today = Carbon::today(); + $now = Carbon::now(); + + // Get working days from settings + $globalSettings = settings(); + $workingDaysIndices = json_decode($globalSettings['working_days'] ?? '[]', true); + + if (empty($workingDaysIndices)) { + return redirect()->back()->with('error', __('Please configure working days first.')); + } + + $dateIndex = Carbon::parse($today)->dayOfWeek; + if (! in_array($dateIndex, $workingDaysIndices)) { + return redirect()->back()->with('error', __('Cannot create attendance record for non-working day.')); + } + + // Check if employee has approved leave for this date + $hasApprovedLeave = LeaveApplication::where('employee_id', $validated['employee_id']) + ->where('status', 'approved') + ->whereDate('start_date', '<=', $today) + ->whereDate('end_date', '>=', $today) + ->exists(); + + if ($hasApprovedLeave) { + return redirect()->back()->with('error', __('Employee has approved leave for this date. Cannot create attendance record.')); + } + + // Check if already clocked in today + $existingRecord = AttendanceRecord::where('employee_id', $validated['employee_id']) + ->where('date', $today) + ->first(); + + if ($existingRecord && $existingRecord->clock_in) { + return redirect()->back()->with('error', __('Already clocked in today.')); + } + + // Get employee with shift and policy + $employee = \App\Models\Employee::where('user_id', $validated['employee_id'])->first(); + + if (! $employee) { + return redirect()->back()->with('error', __('Employee profile not found.')); + } + + // Use employee's assigned shift and policy, or get defaults + $shift = $employee->shift_id ? + Shift::find($employee->shift_id) : + Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); + + $policy = $employee->attendance_policy_id ? + AttendancePolicy::find($employee->attendance_policy_id) : + AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); + + if (! $shift || ! $policy) { + return redirect()->back()->with('error', __('No active shift or attendance policy found. Please contact HR.')); + } + + if ($existingRecord) { + $existingRecord->update([ + 'clock_in' => $now->format('H:i:s'), + 'shift_id' => $shift->id, + 'attendance_policy_id' => $policy->id, + 'status' => 'present', + ]); + $record = $existingRecord; + } else { + $record = AttendanceRecord::create([ + 'employee_id' => $validated['employee_id'], + 'date' => $today, + 'clock_in' => $now->format('H:i:s'), + 'shift_id' => $shift->id, + 'attendance_policy_id' => $policy->id, + 'is_weekend' => $today->isWeekend(), + 'status' => 'present', + 'created_by' => creatorId(), + ]); + } + + // Check for late arrival if methods exist + if (method_exists($record, 'checkLateArrival')) { + $record->checkLateArrival(); + $record->save(); + } + + return redirect()->back()->with('success', __('Clocked in successfully.')); + } catch (\Exception $e) { + \Log::error('Clock in failed: '.$e->getMessage()); + + return redirect()->back()->with('error', __('Failed to clock in. Please try again.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function clockOut(Request $request) + { + if (Auth::user()->can('clock-in-out')) { + try { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + ]); + + $today = Carbon::today(); + $now = Carbon::now(); + + $record = AttendanceRecord::where('employee_id', $validated['employee_id']) + ->where('date', $today) + ->first(); + + if (! $record || ! $record->clock_in) { + return redirect()->back()->with('error', __('Must clock in first.')); + } + + if ($record->clock_out) { + return redirect()->back()->with('error', __('Already clocked out today.')); + } + + $record->update([ + 'clock_out' => $now->format('H:i:s'), + ]); + + // Process complete attendance calculation if method exists + if (method_exists($record, 'processAttendance')) { + $record->processAttendance(); + } + + return redirect()->back()->with('success', __('Clocked out successfully.')); + } catch (\Exception $e) { + \Log::error('Clock out failed: '.$e->getMessage()); + + return redirect()->back()->with('error', __('Failed to clock out. Please try again.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + } + + public function getTodayAttendance(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + ]); + + $today = Carbon::today(); + $attendance = AttendanceRecord::where('employee_id', $validated['employee_id']) + ->where('date', $today) + ->first(); + + return Inertia::render('employee-dashboard', [ + 'attendance' => $attendance, + ]); + } + + public function export() + { + if (Auth::user()->can('export-attendance-record')) { + try { + $attendanceRecords = AttendanceRecord::with(['employee', 'shift', 'attendancePolicy']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-attendance-records')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-attendance-records')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + })->orderBy('date', 'desc')->get(); + + $fileName = 'attendance_records_'.date('Y-m-d_His').'.csv'; + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="'.$fileName.'"', + ]; + + $callback = function () use ($attendanceRecords) { + $file = fopen('php://output', 'w'); + fputcsv($file, [ + 'Employee', + 'Date', + 'Shift', + 'Attedance Policy', + 'Clock In', + 'Clock Out', + 'Break Hours', + 'Total Hours', + 'Overtime Hours', + 'Status', + 'Is Late', + 'Is Early Departure', + 'Notes' + ]); + + foreach ($attendanceRecords as $record) { + fputcsv($file, [ + $record->employee->name ?? '', + $record->date ? date('Y-m-d', strtotime($record->date)) : '', + $record->shift->name ?? '', + $record->attendancePolicy->name ?? '', + $record->clock_in ?? '', + $record->clock_out ?? '', + $record->break_hours ?? '', + $record->total_hours ?? '', + $record->overtime_hours ?? '', + $record->status ?? '', + $record->is_late ? 'Yes' : 'No', + $record->is_early_departure ? 'Yes' : 'No', + $record->notes ?? '' + ]); + } + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to export attendance records: :message', ['message' => $e->getMessage()])], 500); + } + } else { + return response()->json(['message' => __('Permission Denied.')], 403); + } + } + + public function downloadTemplate() + { + $filePath = storage_path('uploads/sample/sample-attendance-record.xlsx'); + if (! file_exists($filePath)) { + return response()->json(['error' => __('Template file not available')], 404); + } + + return response()->download($filePath, 'sample-attendance-record.xlsx'); + } + + public function parseFile(Request $request) + { + if (Auth::user()->can('import-attendance-record')) { + $rules = ['file' => 'required|mimes:csv,txt,xlsx,xls']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return response()->json(['message' => $validator->getMessageBag()->first()]); + } + + try { + $file = $request->file('file'); + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file->getRealPath()); + $worksheet = $spreadsheet->getActiveSheet(); + $highestColumn = $worksheet->getHighestColumn(); + $highestRow = $worksheet->getHighestRow(); + $headers = []; + + for ($col = 'A'; $col <= $highestColumn; $col++) { + $value = $worksheet->getCell($col.'1')->getValue(); + if ($value) { + $headers[] = (string) $value; + } + } + + $previewData = []; + for ($row = 2; $row <= $highestRow; $row++) { + $rowData = []; + $colIndex = 0; + for ($col = 'A'; $col <= $highestColumn; $col++) { + if ($colIndex < count($headers)) { + $rowData[$headers[$colIndex]] = (string) $worksheet->getCell($col.$row)->getValue(); + } + $colIndex++; + } + $previewData[] = $rowData; + } + + return response()->json(['excelColumns' => $headers, 'previewData' => $previewData]); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to parse file: :error', ['error' => $e->getMessage()])]); + } + } else { + return response()->json(['message' => __('Permission denied.')], 403); + } + } + + public function fileImport(Request $request) + { + if (Auth::user()->can('import-attendance-record')) { + $rules = ['data' => 'required|array']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return redirect()->back()->with('error', $validator->getMessageBag()->first()); + } + + try { + $data = $request->data; + $imported = 0; + $skipped = 0; + + foreach ($data as $row) { + try { + if (empty($row['employee']) || empty($row['date'])) { + $skipped++; + continue; + } + + $employee = User::where('name', $row['employee']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('type', 'employee') + ->first(); + + if (! $employee) { + $skipped++; + continue; + } + + // Check if attendance record already exists for this employee and date + $exists = AttendanceRecord::where('employee_id', $employee->id) + ->whereDate('date', $row['date']) + ->exists(); + + if ($exists) { + $skipped++; + continue; + } + + // Get employee with shift and policy + $employeeModel = Employee::where('user_id', $employee->id)->first(); + + $shift = $employeeModel && $employeeModel->shift_id ? + Shift::find($employeeModel->shift_id) : + Shift::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); + + $policy = $employeeModel && $employeeModel->attendance_policy_id ? + AttendancePolicy::find($employeeModel->attendance_policy_id) : + AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('status', 'active')->first(); + + if (! $shift || ! $policy) { + $skipped++; + continue; + } + + $record = AttendanceRecord::create([ + 'employee_id' => $employee->id, + 'date' => $row['date'], + 'shift_id' => $shift->id, + 'attendance_policy_id' => $policy->id, + 'clock_in' => $row['clock_in'] ?? null, + 'clock_out' => $row['clock_out'] ?? null, + 'created_by' => creatorId(), + ]); + + // Process attendance calculation + if (method_exists($record, 'processAttendance')) { + $record->processAttendance(); + } + $imported++; + } catch (\Exception $e) { + $skipped++; + } + } + + return redirect()->back()->with('success', __('Import completed: :added attendance records added, :skipped attendance records skipped', ['added' => $imported, 'skipped' => $skipped])); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to import: :error', ['error' => $e->getMessage()])); + } + } else { + return redirect()->back()->with('error', __('Permission denied.')); + } + } +} diff --git a/app/Http/Controllers/AttendanceRegularizationController.php b/app/Http/Controllers/AttendanceRegularizationController.php new file mode 100644 index 000000000..f22804d31 --- /dev/null +++ b/app/Http/Controllers/AttendanceRegularizationController.php @@ -0,0 +1,313 @@ +can('manage-attendance-regularizations')) { + $query = AttendanceRegularization::with(['employee', 'attendanceRecord', 'approver', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-attendance-regularizations')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-attendance-regularizations')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('reason', 'like', '%' . $request->search . '%') + ->orWhereHas('employee', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->where('date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->where('date', '<=', $request->date_to); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['date', 'created_at'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + } else { + $query->orderBy('created_at', 'desc'); + } + + $regularizations = $query->paginate($request->per_page ?? 9); + + // Load avatar dynamically — same pattern as AttendanceRecordController + $regularizations->getCollection()->transform(function ($record) { + if ($record->employee) { + $rawAvatar = $record->employee->getRawOriginal('avatar'); + $record->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $record; + }); + + $employees = User::where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name']); + + // Get attendance records for form dropdown + $attendanceRecords = AttendanceRecord::whereIn('created_by', getCompanyAndUsersId()) + ->with('employee') + ->orderBy('date', 'desc') + ->take(50) + ->get(); + + $companyUserIds = getCompanyAndUsersId(); + + $statsQuery = AttendanceRegularization::where(function ($q) { + if (Auth::user()->can('manage-any-attendance-regularizations')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-attendance-regularizations')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + $summaryStats = [ + 'total' => (clone $statsQuery)->count(), + 'pending' => (clone $statsQuery)->where('status', 'pending')->count(), + 'approved' => (clone $statsQuery)->where('status', 'approved')->count(), + 'rejected' => (clone $statsQuery)->where('status', 'rejected')->count(), + ]; + + return Inertia::render('hr/attendance-regularizations/index', [ + 'regularizations' => $regularizations, + 'employees' => $this->getFilteredEmployees(), + 'attendanceRecords' => $attendanceRecords, + 'filters' => $request->all(['search', 'employee_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + 'summaryStats' => $summaryStats, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-attendance-regularizations') && !Auth::user()->can('manage-any-attendance-regularizations')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + return $employees; + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'attendance_record_id' => 'required|exists:attendance_records,id', + 'requested_clock_in' => 'nullable|date_format:H:i', + 'requested_clock_out' => 'nullable|date_format:H:i', + 'reason' => 'required|string', + ]); + + $validated['created_by'] = creatorId(); + + // Get attendance record to populate original times and date + $attendanceRecord = AttendanceRecord::find($validated['attendance_record_id']); + if (!$attendanceRecord) { + return redirect()->back()->with('error', __('Attendance record not found.')); + } + + $validated['date'] = $attendanceRecord->date; + $validated['original_clock_in'] = $attendanceRecord->clock_in; + $validated['original_clock_out'] = $attendanceRecord->clock_out; + + // Check if regularization already exists for this record + $exists = AttendanceRegularization::where('attendance_record_id', $validated['attendance_record_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Regularization request already exists for this attendance record.')); + } + + AttendanceRegularization::create($validated); + + return redirect()->back()->with('success', __('Regularization request created successfully.')); + } + + public function update(Request $request, $regularizationId) + { + $regularization = AttendanceRegularization::where('id', $regularizationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($regularization) { + try { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'attendance_record_id' => 'required|exists:attendance_records,id', + 'requested_clock_in' => 'nullable|date_format:H:i', + 'requested_clock_out' => 'nullable|date_format:H:i', + 'reason' => 'required|string', + ]); + + // Only allow updates if status is pending + if ($regularization->status !== 'pending') { + return redirect()->back()->with('error', __('Cannot update processed regularization request.')); + } + + $regularization->update($validated); + + return redirect()->back()->with('success', __('Regularization request updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update regularization request')); + } + } else { + return redirect()->back()->with('error', __('Regularization request Not Found.')); + } + } + + public function destroy($regularizationId) + { + $regularization = AttendanceRegularization::where('id', $regularizationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($regularization) { + try { + // Only allow deletion if status is pending + if ($regularization->status !== 'pending') { + return redirect()->back()->with('error', __('Cannot delete processed regularization request.')); + } + + $regularization->delete(); + return redirect()->back()->with('success', __('Regularization request deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete regularization request')); + } + } else { + return redirect()->back()->with('error', __('Regularization request Not Found.')); + } + } + + public function updateStatus(Request $request, $regularizationId) + { + $validated = $request->validate([ + 'status' => 'required|in:approved,rejected', + 'manager_comments' => 'nullable|string', + ]); + + $regularization = AttendanceRegularization::where('id', $regularizationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($regularization) { + try { + $regularization->update([ + 'status' => $validated['status'], + 'manager_comments' => $validated['manager_comments'], + 'approved_by' => Auth::id(), + 'approved_at' => now(), + ]); + + // Apply changes to attendance record if approved + if ($validated['status'] === 'approved') { + $regularization->applyToAttendanceRecord(); + } + + return redirect()->back()->with('success', __('Regularization request status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update regularization request status')); + } + } else { + return redirect()->back()->with('error', __('Regularization request Not Found.')); + } + } + + public function getEmployeeAttendance($employeeId) + { + try { + // Get attendance records for the last 30 days + $query = AttendanceRecord::where('employee_id', $employeeId) + ->with('employee') + ->orderBy('date', 'desc'); + + if (!isDemo()) { + $query->whereDate('date', '>=', now()->subDays(30)); + } + + $attendanceRecords = $query->get([ + 'id', + 'employee_id', + 'date', + 'clock_in', + 'clock_out', + 'status', + 'is_late', + 'is_early_departure' + ]); + + $datesForDropdown = $attendanceRecords->map(function ($record) { + return [ + 'label' => $record->date->format('d/m/Y'), + 'value' => $record->id, + ]; + }); + + + return response()->json($datesForDropdown); + } catch (\Exception $e) { + return response()->json(['error' => $e->getMessage()], 500); + } + } +} diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php new file mode 100644 index 000000000..498a99162 --- /dev/null +++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php @@ -0,0 +1,160 @@ +first(); + } + + return Inertia::render('auth/login', [ + 'canResetPassword' => Route::has('password.request'), + 'status' => $request->session()->get('status'), + 'settings' => settings(), + ]); + } + + /** + * Handle an incoming authentication request. + */ + public function store(LoginRequest $request): RedirectResponse + { + try { + $request->authenticate(); + $request->session()->regenerate(); + + // Check if email verification is enabled and user is not verified + $emailVerificationEnabled = getSetting('emailVerification', false); + if ($emailVerificationEnabled && !$request->user()->hasVerifiedEmail()) { + return redirect()->route('verification.notice'); + } + + $user = Auth::user(); + + // Safely get IP address + $ip = $request->ip() ?? '127.0.0.1'; + try { + // Get location data with timeout and error handling + $context = stream_context_create([ + 'http' => [ + 'timeout' => 5, + 'ignore_errors' => true + ] + ]); + $response = @file_get_contents('http://ip-api.com/php/' . $ip, false, $context); + $query = $response ? @unserialize($response) : []; + if (!is_array($query)) { + $query = []; + } + } catch (\Exception $e) { + $query = []; + } + + try { + // Browser detection with error handling + $userAgent = $request->header('User-Agent', ''); + if (!empty($userAgent)) { + $whichbrowser = new \WhichBrowser\Parser($userAgent); + // Skip if it's a bot + if (isset($whichbrowser->device->type) && $whichbrowser->device->type == 'bot') { + return redirect()->intended(route('dashboard', absolute: false)); + } + + $query['browser_name'] = $whichbrowser->browser->name ?? null; + $query['os_name'] = $whichbrowser->os->name ?? null; + } + } catch (\Exception $e) { + // Continue without browser detection if it fails + } + + // Get referrer safely + $referrer = $request->header('Referer') ? parse_url($request->header('Referer')) : null; + + // Set additional details + $query['browser_language'] = $request->header('Accept-Language') ? mb_substr($request->header('Accept-Language'), 0, 2) : null; + $query['device_type'] = class_exists('Utility') ? getDeviceType($userAgent) : 'unknown'; + $query['referrer_host'] = !empty($referrer['host']) ? $referrer['host'] : null; + $query['referrer_path'] = !empty($referrer['path']) ? $referrer['path'] : null; + + // Set timezone safely + if (isset($query['timezone']) && !empty($query['timezone'])) { + try { + date_default_timezone_set($query['timezone']); + } catch (\Exception $e) { + // Continue with default timezone if setting fails + } + } + + // Save login details + try { + + if (isSaaS()) { + if (Auth::user()->hasRole('superadmin')) { + $createdBy = Auth::user()->id; + } else if (Auth::user()->hasRole('company')) { + $createdBy = Auth::user()->created_by; + } else { + $createdBy = getCompanyId(Auth::user()->id); + } + } else { + if (Auth::user()->hasRole('company')) { + $createdBy = Auth::user()->id; + } else { + $createdBy = getCompanyId(Auth::user()->id); + } + } + + + $loginDetail = new LoginHistory(); + $loginDetail->user_id = $user->id; + $loginDetail->ip = $ip; + $loginDetail->date = now(); + $loginDetail->Details = json_encode($query); + $loginDetail->created_by = $createdBy; + $loginDetail->save(); + + } catch (\Exception $e) { + Log::warning('Failed to save login details: ' . $e->getMessage()); + } + + return redirect()->intended(route('dashboard', absolute: false)); + + } catch (\Exception $e) { + Log::error('Login error: ' . $e->getMessage()); + return back()->withErrors(['email' => $e->getMessage()]); + } + } + + /** + * Destroy an authenticated session. + */ + public function destroy(Request $request): RedirectResponse + { + Auth::guard('web')->logout(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Http/Controllers/Auth/ConfirmablePasswordController.php b/app/Http/Controllers/Auth/ConfirmablePasswordController.php new file mode 100644 index 000000000..c729706d6 --- /dev/null +++ b/app/Http/Controllers/Auth/ConfirmablePasswordController.php @@ -0,0 +1,41 @@ +validate([ + 'email' => $request->user()->email, + 'password' => $request->password, + ])) { + throw ValidationException::withMessages([ + 'password' => __('auth.password'), + ]); + } + + $request->session()->put('auth.password_confirmed_at', time()); + + return redirect()->intended(route('dashboard', absolute: false)); + } +} diff --git a/app/Http/Controllers/Auth/EmailVerificationNotificationController.php b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php new file mode 100644 index 000000000..f64fa9ba7 --- /dev/null +++ b/app/Http/Controllers/Auth/EmailVerificationNotificationController.php @@ -0,0 +1,24 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(route('dashboard', absolute: false)); + } + + $request->user()->sendEmailVerificationNotification(); + + return back()->with('status', 'verification-link-sent'); + } +} diff --git a/app/Http/Controllers/Auth/EmailVerificationPromptController.php b/app/Http/Controllers/Auth/EmailVerificationPromptController.php new file mode 100644 index 000000000..672f7cfce --- /dev/null +++ b/app/Http/Controllers/Auth/EmailVerificationPromptController.php @@ -0,0 +1,22 @@ +user()->hasVerifiedEmail() + ? redirect()->intended(route('dashboard', absolute: false)) + : Inertia::render('auth/verify-email', ['status' => $request->session()->get('status')]); + } +} diff --git a/app/Http/Controllers/Auth/NewPasswordController.php b/app/Http/Controllers/Auth/NewPasswordController.php new file mode 100644 index 000000000..0b4c6cbd3 --- /dev/null +++ b/app/Http/Controllers/Auth/NewPasswordController.php @@ -0,0 +1,69 @@ + $request->email, + 'token' => $request->route('token'), + ]); + } + + /** + * Handle an incoming new password request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + $request->validate([ + 'token' => 'required', + 'email' => 'required|email', + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + ]); + + // Here we will attempt to reset the user's password. If it is successful we + // will update the password on an actual user model and persist it to the + // database. Otherwise we will parse the error and return the response. + $status = Password::reset( + $request->only('email', 'password', 'password_confirmation', 'token'), + function ($user) use ($request) { + $user->forceFill([ + 'password' => Hash::make($request->password), + 'remember_token' => Str::random(60), + ])->save(); + + event(new PasswordReset($user)); + } + ); + + // If the password was successfully reset, we will redirect the user back to + // the application's home authenticated view. If there is an error we can + // redirect them back to where they came from with their error message. + if ($status == Password::PasswordReset) { + return to_route('login')->with('status', __($status)); + } + + throw ValidationException::withMessages([ + 'email' => [__($status)], + ]); + } +} diff --git a/app/Http/Controllers/Auth/PasswordResetLinkController.php b/app/Http/Controllers/Auth/PasswordResetLinkController.php new file mode 100644 index 000000000..1679203fb --- /dev/null +++ b/app/Http/Controllers/Auth/PasswordResetLinkController.php @@ -0,0 +1,87 @@ + $request->session()->get('status'), + 'settings' => settings(), + ]); + } + + public function store(Request $request): RedirectResponse + { + try { + $request->validate([ + 'email' => 'required|email', + ]); + + $this->setEmailConfig($request->email); + + Password::sendResetLink( + $request->only('email') + ); + + return back()->with('status', __('A reset link will be sent if the account exists.')); + } catch (\Exception $e) { + return back()->withErrors(['email' => __('Unable to send reset link. Please try again.')]); + } + } + + private function setEmailConfig($email): void + { + try { + $user = User::where('email', $email)->first(); + if (! $user) { + return; + } + if (isSaas()) { + if ($user->type == 'company') { + $user = User::where('id', $user->created_by)->first(); + } else { + $user = User::where('id', $user->created_by)->first(); + } + } else { + $user = User::where('id', $user->created_by)->first(); + } + + $getSettings = settings($user->id); + + $settings = [ + 'driver' => $getSettings['email_driver'] ?? '', + 'host' => $getSettings['email_host'] ?? '', + 'port' => $getSettings['email_port'] ?? '', + 'username' => $getSettings['email_username'] ?? '', + 'password' => $getSettings['email_password'] ?? '', + 'encryption' => $getSettings['email_encryption'] ?? '', + 'fromAddress' => $getSettings['email_from_address'] ?? '', + 'fromName' => $getSettings['email_from_name'] ?? '', + ]; + + Config::set([ + 'mail.default' => $settings['driver'], + 'mail.mailers.smtp.host' => $settings['host'], + 'mail.mailers.smtp.port' => $settings['port'], + 'mail.mailers.smtp.encryption' => $settings['encryption'] === 'none' ? null : $settings['encryption'], + 'mail.mailers.smtp.username' => $settings['username'], + 'mail.mailers.smtp.password' => $settings['password'], + 'mail.from.address' => $settings['fromAddress'], + 'mail.from.name' => $settings['fromName'], + ]); + } catch (\Exception $e) { + throw new \Exception('Email config error: '.$e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php new file mode 100644 index 000000000..eab8c5882 --- /dev/null +++ b/app/Http/Controllers/Auth/RegisteredUserController.php @@ -0,0 +1,178 @@ +route('login'); + } + + $referralCode = $request->get('ref'); + $encryptedPlanId = $request->get('plan'); + $planId = null; + $referrer = null; + + // Decrypt and validate plan ID + if ($encryptedPlanId) { + $planId = $this->decryptPlanId($encryptedPlanId); + if ($planId && !Plan::find($planId)) { + $planId = null; // Invalid plan ID + } + } + + if ($referralCode) { + $referrer = User::where('referral_code', $referralCode) + ->where('type', 'company') + ->first(); + } + + return Inertia::render('auth/register', [ + 'referralCode' => $referralCode, + 'planId' => $planId, + 'referrer' => $referrer ? $referrer->name : null, + 'settings' => settings(), + ]); + } + + /** + * Handle an incoming registration request. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function store(Request $request): RedirectResponse + { + if (!isUserRegistrationEnabled()) { + return redirect()->route('login'); + } + + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|lowercase|email|max:255|unique:' . User::class, + 'password' => ['required', 'confirmed', Rules\Password::defaults()], + 'terms' => 'required|accepted' + ]); + + $superAdminSettings = settings(); + $userLang = isset($superAdminSettings['defaultLanguage']) ? $superAdminSettings['defaultLanguage'] : 'en'; + + $userData = [ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'type' => 'company', + 'is_active' => 1, + 'is_enable_login' => 1, + 'created_by' => 1, + 'plan_is_active' => 0, + 'lang' => $userLang, + ]; + + // Handle referral code + if ($request->referral_code) { + $referrer = User::where('referral_code', $request->referral_code) + ->where('type', 'company') + ->first(); + + if ($referrer) { + $userData['used_referral_code'] = $request->referral_code; + } + } + + $user = User::create($userData); + + // Assign role and settings to the user + defaultRoleAndSetting($user); + + // Note: Referral record will be created when user purchases a plan + // This is handled in the PlanController or payment controllers + + Auth::login($user); + + // Check if email verification is enabled + $emailVerificationEnabled = getSetting('emailVerification', false); + if ($emailVerificationEnabled) { + event(new Registered($user)); + return redirect()->route('verification.notice'); + } + + // Redirect to plans page with selected plan + $planId = $request->plan_id; + if ($planId) { + return redirect()->route('plans.index', ['selected' => $planId]); + } + return to_route('dashboard'); + } + + /** + * Decrypt plan ID from encrypted string + */ + private function decryptPlanId($encryptedPlanId) + { + try { + $key = 'vCardGo2024'; + $encrypted = base64_decode($encryptedPlanId); + $decrypted = ''; + + for ($i = 0; $i < strlen($encrypted); $i++) { + $decrypted .= chr(ord($encrypted[$i]) ^ ord($key[$i % strlen($key)])); + } + + return is_numeric($decrypted) ? (int) $decrypted : null; + } catch (\Exception $e) { + return null; + } + } + + /** + * Create referral record when user purchases a plan + */ + private function createReferralRecord(User $user) + { + $settings = ReferralSetting::current(); + + if (!$settings->is_enabled) { + return; + } + + $referrer = User::where('referral_code', $user->used_referral_code)->first(); + if (!$referrer || !$user->plan) { + return; + } + + // Calculate commission based on plan price + $planPrice = $user->plan->price ?? 0; + $commissionAmount = ($planPrice * $settings->commission_percentage) / 100; + + if ($commissionAmount > 0) { + Referral::create([ + 'user_id' => $user->id, + 'company_id' => $referrer->id, + 'commission_percentage' => $settings->commission_percentage, + 'amount' => $commissionAmount, + 'plan_id' => $user->plan_id, + ]); + } + } +} diff --git a/app/Http/Controllers/Auth/VerifyEmailController.php b/app/Http/Controllers/Auth/VerifyEmailController.php new file mode 100644 index 000000000..a300bfafe --- /dev/null +++ b/app/Http/Controllers/Auth/VerifyEmailController.php @@ -0,0 +1,30 @@ +user()->hasVerifiedEmail()) { + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + } + + if ($request->user()->markEmailAsVerified()) { + /** @var \Illuminate\Contracts\Auth\MustVerifyEmail $user */ + $user = $request->user(); + + event(new Verified($user)); + } + + return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); + } +} diff --git a/app/Http/Controllers/AuthorizeNetPaymentController.php b/app/Http/Controllers/AuthorizeNetPaymentController.php new file mode 100644 index 000000000..5810d805d --- /dev/null +++ b/app/Http/Controllers/AuthorizeNetPaymentController.php @@ -0,0 +1,359 @@ +json(['error' => 'AuthorizeNet not properly configured'], 400); + } + + // Get currency from settings or default to USD + $currency = $settings['general_settings']['currency'] ?? 'USD'; + + // Validate currency support + if (!in_array($currency, self::SUPPORTED_CURRENCIES)) { + $currency = 'USD'; + } + + return response()->json([ + 'success' => true, + 'merchant_id' => $settings['payment_settings']['authorizenet_merchant_id'], + 'amount' => number_format($pricing['final_price'], 2, '.', ''), + 'currency' => $currency, + 'is_sandbox' => $settings['payment_settings']['authorizenet_mode'] === 'sandbox', + 'supported_countries' => self::SUPPORTED_COUNTRIES, + 'supported_currencies' => self::SUPPORTED_CURRENCIES, + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment form creation failed')], 500); + } + } + + public function processPayment(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'card_number' => 'required|string', + 'expiry_month' => 'required|string|size:2', + 'expiry_year' => 'required|string|size:2', + 'cvv' => 'required|string|min:3|max:4', + 'cardholder_name' => 'required|string|min:2|max:50', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['authorizenet_merchant_id']) || + !isset($settings['payment_settings']['authorizenet_transaction_key'])) { + return back()->withErrors(['error' => __('AuthorizeNet not properly configured')]); + } + + // Validate minimum amount (AuthorizeNet requires minimum $0.50) + if ($pricing['final_price'] < 0.50) { + return back()->withErrors(['error' => __('Minimum payment amount is $0.50')]); + } + + $result = $this->createAuthorizeNetTransaction($validated, $pricing, $settings); + + if ($result['success']) { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'authorizenet', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $result['transaction_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => $result['error']]); + + } catch (\Exception $e) { + return back()->withErrors(['error' => __('Payment processing failed. Please try again.')]); + } + } + + private function createAuthorizeNetTransaction($paymentData, $pricing, $settings) + { + try { + // Set up merchant authentication + $merchantAuthentication = new AnetAPI\MerchantAuthenticationType(); + $merchantAuthentication->setName($settings['payment_settings']['authorizenet_merchant_id']); + $merchantAuthentication->setTransactionKey($settings['payment_settings']['authorizenet_transaction_key']); + + // Set up credit card information + $creditCard = new AnetAPI\CreditCardType(); + $creditCard->setCardNumber(preg_replace('/\s+/', '', $paymentData['card_number'])); + + // Fix expiration date format - AuthorizeNet expects YYYY-MM format + $expiryYear = 2000 + intval($paymentData['expiry_year']); + $expiryMonth = str_pad($paymentData['expiry_month'], 2, '0', STR_PAD_LEFT); + $creditCard->setExpirationDate($expiryYear . '-' . $expiryMonth); + $creditCard->setCardCode($paymentData['cvv']); + + // Set up payment method + $paymentOne = new AnetAPI\PaymentType(); + $paymentOne->setCreditCard($creditCard); + + // Set up order information + $order = new AnetAPI\OrderType(); + $order->setInvoiceNumber('INV-' . time()); + $order->setDescription('Plan Subscription Payment'); + + // Set up customer information + $customer = new AnetAPI\CustomerDataType(); + $customer->setType('individual'); + $customer->setId(auth()->id()); + $customer->setEmail(auth()->user()->email); + + // Set up billing information + $billTo = new AnetAPI\CustomerAddressType(); + $billTo->setFirstName(explode(' ', $paymentData['cardholder_name'])[0]); + $billTo->setLastName(implode(' ', array_slice(explode(' ', $paymentData['cardholder_name']), 1)) ?: 'Customer'); + $billTo->setCompany(auth()->user()->name ?? ''); + $billTo->setAddress('N/A'); + $billTo->setCity('N/A'); + $billTo->setState('N/A'); + $billTo->setZip('00000'); + $billTo->setCountry('US'); + + // Create transaction request + $transactionRequestType = new AnetAPI\TransactionRequestType(); + $transactionRequestType->setTransactionType('authCaptureTransaction'); + $transactionRequestType->setAmount(number_format($pricing['final_price'], 2, '.', '')); + $transactionRequestType->setPayment($paymentOne); + $transactionRequestType->setOrder($order); + $transactionRequestType->setBillTo($billTo); + $transactionRequestType->setCustomer($customer); + + // Add merchant defined fields for tracking + $merchantDefinedField1 = new AnetAPI\UserFieldType(); + $merchantDefinedField1->setName('plan_id'); + $merchantDefinedField1->setValue($paymentData['plan_id']); + + $merchantDefinedField2 = new AnetAPI\UserFieldType(); + $merchantDefinedField2->setName('user_id'); + $merchantDefinedField2->setValue(auth()->id()); + + $transactionRequestType->setUserFields([$merchantDefinedField1, $merchantDefinedField2]); + + // Create the API request + $request = new AnetAPI\CreateTransactionRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + $request->setTransactionRequest($transactionRequestType); + + // Execute the request + $controller = new AnetController\CreateTransactionController($request); + + $environment = ($settings['payment_settings']['authorizenet_mode'] === 'sandbox') + ? \net\authorize\api\constants\ANetEnvironment::SANDBOX + : \net\authorize\api\constants\ANetEnvironment::PRODUCTION; + + $response = $controller->executeWithApiResponse($environment); + + return $this->handleAuthorizeNetResponse($response); + + } catch (\Exception $e) { + return [ + 'success' => false, + 'error' => __('Transaction processing failed. Please check your card details and try again.'), + 'transaction_id' => null + ]; + } + } + + private function handleAuthorizeNetResponse($response) + { + if ($response === null) { + return [ + 'success' => false, + 'error' => __('No response received from payment gateway'), + 'transaction_id' => null + ]; + } + + $messages = $response->getMessages(); + + if ($messages->getResultCode() !== 'Ok') { + $errorMessage = __('Payment gateway error'); + if ($messages->getMessage() && count($messages->getMessage()) > 0) { + $errorMessage = $messages->getMessage()[0]->getText(); + } + + return [ + 'success' => false, + 'error' => $this->getFriendlyErrorMessage($errorMessage), + 'transaction_id' => null + ]; + } + + $tresponse = $response->getTransactionResponse(); + + if ($tresponse === null) { + return [ + 'success' => false, + 'error' => __('Invalid transaction response'), + 'transaction_id' => null + ]; + } + + $responseCode = $tresponse->getResponseCode(); + + // Response codes: 1 = Approved, 2 = Declined, 3 = Error, 4 = Held for Review + switch ($responseCode) { + case '1': // Approved + return [ + 'success' => true, + 'error' => null, + 'transaction_id' => $tresponse->getTransId() + ]; + + case '2': // Declined + $errorMessage = 'Transaction declined'; + if ($tresponse->getErrors() && count($tresponse->getErrors()) > 0) { + $errorMessage = $tresponse->getErrors()[0]->getErrorText(); + } + + return [ + 'success' => false, + 'error' => $this->getFriendlyErrorMessage($errorMessage), + 'transaction_id' => null + ]; + + case '3': // Error + $errorMessage = 'Transaction error'; + if ($tresponse->getErrors() && count($tresponse->getErrors()) > 0) { + $errorMessage = $tresponse->getErrors()[0]->getErrorText(); + } + + return [ + 'success' => false, + 'error' => $this->getFriendlyErrorMessage($errorMessage), + 'transaction_id' => null + ]; + + case '4': // Held for Review + return [ + 'success' => false, + 'error' => __('Transaction is being reviewed. Please contact support.'), + 'transaction_id' => $tresponse->getTransId() + ]; + + default: + return [ + 'success' => false, + 'error' => __('Unknown transaction response'), + 'transaction_id' => null + ]; + } + } + + private function getFriendlyErrorMessage($errorMessage) + { + $friendlyMessages = [ + __('The credit card number is invalid') => __('Please check your card number and try again.'), + __('The credit card has expired') => __('Your card has expired. Please use a different card.'), + __('The credit card expiration date is invalid') => __('Please check the expiration date and try again.'), + __('The transaction cannot be found') => __('Transaction not found. Please try again.'), + __('A duplicate transaction has been submitted') => __('This transaction was already processed.'), + __('The amount is invalid') => __('Invalid payment amount.'), + __('This transaction has been declined') => __('Your card was declined. Please try a different payment method.'), + __('Insufficient funds') => __('Insufficient funds. Please try a different card.'), + __('The merchant does not accept this type of credit card') => __('This card type is not accepted.'), + __('The transaction has been declined because of an AVS mismatch') => __('Address verification failed. Please check your billing address.'), + __('The transaction has been declined because the CVV2 value is invalid') => __('Invalid security code. Please check your CVV.'), + ]; + + foreach ($friendlyMessages as $original => $friendly) { + if (stripos($errorMessage, $original) !== false) { + return $friendly; + } + } + + return __('Payment processing failed. Please check your card details and try again.'); + } + + /** + * Test AuthorizeNet connection and credentials + */ + public function testConnection(Request $request) + { + try { + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['authorizenet_merchant_id']) || + !isset($settings['payment_settings']['authorizenet_transaction_key'])) { + return response()->json([ + 'success' => false, + 'message' => __('AuthorizeNet credentials not configured') + ]); + } + + // Test with AuthenticateTest API call + $merchantAuthentication = new AnetAPI\MerchantAuthenticationType(); + $merchantAuthentication->setName($settings['payment_settings']['authorizenet_merchant_id']); + $merchantAuthentication->setTransactionKey($settings['payment_settings']['authorizenet_transaction_key']); + + $request = new AnetAPI\AuthenticateTestRequest(); + $request->setMerchantAuthentication($merchantAuthentication); + + $controller = new AnetController\AuthenticateTestController($request); + + $environment = ($settings['payment_settings']['authorizenet_mode'] === 'sandbox') + ? \net\authorize\api\constants\ANetEnvironment::SANDBOX + : \net\authorize\api\constants\ANetEnvironment::PRODUCTION; + + $response = $controller->executeWithApiResponse($environment); + + if ($response && $response->getMessages()->getResultCode() === 'Ok') { + return response()->json([ + 'success' => true, + 'message' => __('AuthorizeNet connection successful'), + 'mode' => $settings['payment_settings']['authorizenet_mode'] + ]); + } else { + $errorMessage = __('Connection failed'); + if ($response && $response->getMessages()->getMessage()) { + $errorMessage = $response->getMessages()->getMessage()[0]->getText(); + } + + return response()->json([ + 'success' => false, + 'message' => $errorMessage + ]); + } + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => __('Connection test failed: ') . $e->getMessage() + ]); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/AwardController.php b/app/Http/Controllers/AwardController.php new file mode 100644 index 000000000..47842889d --- /dev/null +++ b/app/Http/Controllers/AwardController.php @@ -0,0 +1,360 @@ +can('manage-awards')) { + // $query = Award::withPermissionCheck()->with(['employee.employee', 'awardType']); + $query = Award::with(['employee.employee', 'awardType']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-awards')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-awards')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($query) use ($request) { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhereHas('employee', function ($empQ) use ($request) { + $empQ->where('employee_id', 'like', '%' . $request->search . '%'); + }); + }) + ->orWhereHas('awardType', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%'); + }) + ->orWhere('gift', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle award type filter + if ($request->has('award_type_id') && !empty($request->award_type_id)) { + $query->where('award_type_id', $request->award_type_id); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('award_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('award_date', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'id'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['award_date', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'id'; + } + + $query->orderBy($sortField, $sortDirection); + + $awards = $query->paginate($request->per_page ?? 10); + + $awards->getCollection()->transform(function ($award) { + if ($award->employee) { + $rawAvatar = $award->employee->getRawOriginal('avatar'); + $award->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $award; + }); + + + // Get award types for filter dropdown + $awardTypes = AwardType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + // // Get employees for filter dropdown + // $employees = User::with('employee') + // ->where('type', 'employee') + // ->whereIn('created_by', getCompanyAndUsersId()) + // ->where('status', 'active') + // ->select('id', 'name') + // ->get() + // ->map(function ($user) { + // return [ + // 'id' => $user->id, + // 'name' => $user->name, + // 'employee_id' => $user->employee->employee_id ?? '' + // ]; + // }); + + return Inertia::render('hr/awards/index', [ + 'awards' => $awards, + 'awardTypes' => $awardTypes, + 'employees' => $this->getFilteredEmployees(), + 'filters' => $request->all(['search', 'award_type_id', 'employee_id', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-awards') && !Auth::user()->can('manage-any-awards')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + return $employees; + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-awards')) { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'award_type_id' => 'required|exists:award_types,id', + 'award_date' => 'required|date', + 'gift' => 'nullable|string|max:255', + 'monetary_value' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'certificate' => 'nullable|string', + 'photo' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if award type belongs to current company + $awardType = AwardType::find($request->award_type_id); + if (!$awardType || !in_array($awardType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid award type selected')); + } + + // Check if employee belongs to current company + $user = User::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (!$user) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + $awardData = [ + 'employee_id' => $request->employee_id, + 'award_type_id' => $request->award_type_id, + 'award_date' => $request->award_date, + 'gift' => $request->gift, + 'monetary_value' => $request->monetary_value, + 'description' => $request->description, + 'created_by' => creatorId(), + ]; + + // Handle certificate from media library + if ($request->certificate) { + $awardData['certificate'] = $request->certificate; + } + + // Handle photo from media library + if ($request->photo) { + $awardData['photo'] = $request->photo; + } + + Award::create($awardData); + + return redirect()->back()->with('success', __('Award created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Award $award) + { + if (Auth::user()->can('edit-awards')) { + // Check if award belongs to current company + if (!in_array($award->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this award')); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'award_type_id' => 'required|exists:award_types,id', + 'award_date' => 'required|date', + 'gift' => 'nullable|string|max:255', + 'monetary_value' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'certificate' => 'nullable|string', + 'photo' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if award type belongs to current company + $awardType = AwardType::find($request->award_type_id); + if (!$awardType || !in_array($awardType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid award type selected')); + } + + // Check if employee belongs to current company + $user = User::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (!$user) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + $awardData = [ + 'employee_id' => $request->employee_id, + 'award_type_id' => $request->award_type_id, + 'award_date' => $request->award_date, + 'gift' => $request->gift, + 'monetary_value' => $request->monetary_value, + 'description' => $request->description, + ]; + + // Handle certificate from media library + if ($request->certificate) { + $awardData['certificate'] = $request->certificate; + } + + // Handle photo from media library + if ($request->photo) { + $awardData['photo'] = $request->photo; + } + + $award->update($awardData); + + return redirect()->back()->with('success', __('Award updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Award $award) + { + if (Auth::user()->can('delete-awards')) { + // Check if award belongs to current company + if (!in_array($award->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this award')); + } + + $award->delete(); + + return redirect()->back()->with('success', __('Award deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Download certificate file. + */ + public function downloadCertificate(Award $award) + { + if (Auth::user()->can('view-awards')) { + // Check if award belongs to current company + if (!in_array($award->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this certificate')); + } + + if (!$award->certificate) { + return redirect()->back()->with('error', __('Certificate file not found')); + } + + $filePath = getStorageFilePath($award->certificate); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Certificate file not found')); + } + + return response()->download($filePath); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Download photo file. + */ + public function downloadPhoto(Award $award) + { + if (Auth::user()->can('view-awards')) { + // Check if award belongs to current company + if (!in_array($award->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this photo')); + } + + if (!$award->photo) { + return redirect()->back()->with('error', __('Photo file not found')); + } + + $filePath = getStorageFilePath($award->photo); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Certificate file not found')); + } + + return response()->download($filePath); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/AwardTypeController.php b/app/Http/Controllers/AwardTypeController.php new file mode 100644 index 000000000..9866bbd77 --- /dev/null +++ b/app/Http/Controllers/AwardTypeController.php @@ -0,0 +1,187 @@ +can('manage-award-types')) { + $query = AwardType::where(function ($q) { + if (Auth::user()->can('manage-any-award-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-award-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $awardTypes = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/award-types/index', [ + 'awardTypes' => $awardTypes, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-award-types')) { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + AwardType::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Award type created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, $awardTypeId) + { + if (Auth::user()->can('edit-award-types')) { + $awardType = AwardType::where('id', $awardTypeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($awardType) { + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $awardType->update([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Award type updated successfully')); + } else { + return redirect()->back()->with('error', __('Award Type Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy($awardTypeId) + { + if (Auth::user()->can('delete-award-types')) { + $awardType = AwardType::where('id', $awardTypeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($awardType) { + try { + // Check if award type is being used in awards + if ($awardType->awards()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete award type as it is being used in awards')); + } + + $awardType->delete(); + return redirect()->back()->with('success', __('Award type deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete award type')); + } + } else { + return redirect()->back()->with('error', __('Award Type Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Toggle the status of the specified resource. + */ + public function toggleStatus($awardTypeId) + { + if (Auth::user()->can('edit-award-types')) { + $awardType = AwardType::where('id', $awardTypeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($awardType) { + try { + $awardType->status = $awardType->status === 'active' ? 'inactive' : 'active'; + $awardType->save(); + + return redirect()->back()->with('success', __('Award type status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update award type status')); + } + } else { + return redirect()->back()->with('error', __('Award Type Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/BankPaymentController.php b/app/Http/Controllers/BankPaymentController.php new file mode 100644 index 000000000..d0a9baa83 --- /dev/null +++ b/app/Http/Controllers/BankPaymentController.php @@ -0,0 +1,39 @@ + 'required|numeric|min:0', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + + createPlanOrder([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'bank', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => 'BANK_' . strtoupper(uniqid()), + 'status' => 'pending', + ]); + + return back()->with('success', __('Payment request submitted. Your plan will be activated after payment verification.')); + + } catch (\Exception $e) { + return handlePaymentError($e, 'bank'); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/BaseController.php b/app/Http/Controllers/BaseController.php new file mode 100644 index 000000000..e8a40776f --- /dev/null +++ b/app/Http/Controllers/BaseController.php @@ -0,0 +1,10 @@ + 'required|string', + 'transaction_id' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['benefit_secret_key']) || !isset($settings['payment_settings']['benefit_public_key'])) { + return back()->withErrors(['error' => __('Benefit payment not configured')]); + } + + // Verify payment with Benefit API + $isPaymentValid = $this->verifyBenefitPayment( + $validated['payment_id'], + $validated['transaction_id'], + $settings['payment_settings'] + ); + + if ($isPaymentValid) { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'benefit', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['payment_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment verification failed')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'benefit'); + } + } + + public function createPaymentSession(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['benefit_secret_key'])) { + return response()->json(['error' => __('Benefit payment not configured')], 400); + } + + $user = auth()->user(); + $orderID = strtoupper(str_replace('.', '', uniqid('', true))); + + $userData = [ + "amount" => $pricing['final_price'], + "currency" => "BHD", + "customer_initiated" => true, + "threeDSecure" => true, + "save_card" => false, + "description" => "Plan - " . $plan->name, + "metadata" => ["udf1" => "Plan Payment"], + "reference" => ["transaction" => $orderID, "order" => $orderID], + "receipt" => ["email" => true, "sms" => true], + "customer" => [ + "first_name" => $user->name ?? 'Customer', + "middle_name" => "", + "last_name" => "", + "email" => $user->email, + "phone" => ["country_code" => "973", "number" => "33123456"] + ], + "source" => ["id" => "src_bh.benefit"], + "post" => ["url" => route('benefit.callback')], + "redirect" => ["url" => route('benefit.success', [ + 'plan_id' => $plan->id, + 'amount' => $pricing['final_price'], + 'coupon' => $validated['coupon_code'] ?? '', + 'user_id' => $user->id, + 'billing_cycle' => $validated['billing_cycle'] + ])] + ]; + + $responseData = json_encode($userData); + $response = \Http::withHeaders([ + 'Authorization' => 'Bearer ' . $settings['payment_settings']['benefit_secret_key'], + 'accept' => 'application/json', + 'content-type' => 'application/json', + ])->post('https://api.tap.company/v2/charges', $userData); + + if ($response->successful()) { + $res = $response->json(); + if (isset($res['transaction']['url'])) { + return response()->json([ + 'success' => true, + 'payment_url' => $res['transaction']['url'], + 'transaction_id' => $orderID + ]); + } + } + + return response()->json(['error' => $response->body()], 500); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment session creation failed')], 500); + } + } + + public function callback(Request $request) + { + try { + $paymentId = $request->input('payment_id'); + $transactionId = $request->input('transaction_id'); + $status = $request->input('status'); + + $settings = getPaymentGatewaySettings(); + + if (!$paymentId || !$transactionId) { + return redirect()->route('plans.index')->withErrors(['error' => __('Invalid payment response')]); + } + + // Verify payment status with Benefit API + $paymentResult = $this->retrieveBenefitPayment($paymentId, $settings['payment_settings']); + + if ($paymentResult && $paymentResult['status'] === 'completed') { + // Extract transaction ID to find the plan and user + $parts = explode('_', $transactionId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', // Default, should be stored in session or passed + 'payment_method' => 'benefit', + 'payment_id' => $paymentId, + ]); + + return redirect()->route('plans.index')->with('success', __('Payment successful and plan activated')); + } + } + } + + return redirect()->route('plans.index')->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return redirect()->route('plans.index')->withErrors(['error' => __('Payment processing failed')]); + } + } + + public function success(Request $request) + { + try { + $planId = $request->input('plan_id'); + $userId = $request->input('user_id'); + $amount = $request->input('amount'); + $coupon = $request->input('coupon'); + $billingCycle = $request->input('billing_cycle', 'monthly'); + + if ($planId && $userId) { + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $billingCycle, + 'payment_method' => 'benefit', + 'coupon_code' => $coupon, + 'payment_id' => $request->input('tap_id', 'benefit_' . time()), + ]); + + // Log the user in if not already authenticated + if (!auth()->check()) { + auth()->login($user); + } + + return redirect()->route('plans.index')->with('success', __('Payment completed successfully and plan activated')); + } + } + + return redirect()->route('plans.index')->with('error', __('Payment verification failed')); + + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment processing failed')); + } + } + + public function webhook(Request $request) + { + try { + $payload = $request->all(); + $settings = getPaymentGatewaySettings(); + + // Verify webhook signature + if (!$this->verifyBenefitWebhook($payload, $request->header('X-Benefit-Signature'), $settings['payment_settings'])) { + return response()->json(['error' => 'Invalid signature'], 400); + } + + $paymentId = $payload['payment_id'] ?? null; + $status = $payload['status'] ?? null; + $transactionId = $payload['transaction_id'] ?? null; + + if ($paymentId && $status === 'completed' && $transactionId) { + // Process successful payment + $parts = explode('_', $transactionId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + // Check if payment already processed + $existingOrder = PlanOrder::where('payment_id', $paymentId)->first(); + + if (!$existingOrder) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', + 'payment_method' => 'benefit', + 'payment_id' => $paymentId, + ]); + } + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Webhook processing failed')], 500); + } + } + + private function verifyBenefitPayment($paymentId, $transactionId, $settings) + { + // This is a simplified verification - in production, use Benefit API + // For now, we'll assume the payment is valid if we have the required parameters + return !empty($paymentId) && !empty($transactionId); + } + + private function createBenefitSession($paymentData, $settings) + { + // This is a simplified session creation - in production, use Benefit API + // For now, return a mock session + $baseUrl = $settings['benefit_mode'] === 'live' + ? 'https://api.benefit.bh' + : 'https://sandbox-api.benefit.bh'; + + return [ + 'session_id' => 'benefit_session_' . time(), + 'payment_url' => $baseUrl . '/payment/checkout?session=' . time() + ]; + } + + private function retrieveBenefitPayment($paymentId, $settings) + { + // This is a simplified retrieval - in production, use Benefit API + // For now, return a mock successful response + return [ + 'status' => 'completed', + 'payment_id' => $paymentId, + 'amount' => '10.000', + 'currency' => 'BHD' + ]; + } + + private function verifyBenefitWebhook($payload, $signature, $settings) + { + // This is a simplified webhook verification - in production, verify the signature + // using Benefit's webhook secret and HMAC + return true; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/BiometricAttendanceController.php b/app/Http/Controllers/BiometricAttendanceController.php new file mode 100644 index 000000000..d4b1f6c4a --- /dev/null +++ b/app/Http/Controllers/BiometricAttendanceController.php @@ -0,0 +1,2924 @@ +can('manage-biometric-attendance')) { + $company_setting = settings(); + $api_urls = !empty($company_setting['zkteco_api_url']) ? $company_setting['zkteco_api_url'] : ''; + $username = !empty($company_setting['zkteco_username']) ? $company_setting['zkteco_username'] : ''; + $password = !empty($company_setting['zkteco_password']) ? $company_setting['zkteco_password'] : ''; + $token = !empty($company_setting['zkteco_auth_token']) ? $company_setting['zkteco_auth_token'] : ''; + $isZktecoSync = !empty($company_setting['isZktecoSync']) && $company_setting['isZktecoSync'] == '1'; + + $configurationMissing = empty($api_urls) || empty($username) || empty($password) || empty($token) || !$isZktecoSync; + + if (!empty($request->start_date) && !empty($request->end_date)) { + $start_date = date('Y-m-d H:i:s', strtotime($request->start_date)); + $end_date = date('Y-m-d H:i:s', strtotime($request->end_date) + 86400 - 1); + } else { + $start_date = date('Y-m-d', strtotime('-7 days')); + $end_date = date('Y-m-d'); + } + + $attendances = []; + + if (!empty($token) && !empty($api_urls)) { + $api_url = rtrim($api_urls, '/'); + $url = $api_url . '/iclock/api/transactions/?' . http_build_query([ + 'start_time' => $start_date, + 'end_time' => $end_date, + 'page_size' => 10000, + ]); + + $curl = curl_init(); + try { + curl_setopt_array($curl, array( + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 0, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_HTTPHEADER => array( + 'Content-Type: application/json', + 'Authorization: Token ' . $token + ), + )); + + $response = curl_exec($curl); + curl_close($curl); + + $json_attendance = json_decode($response, true); + $attendances = $json_attendance['data'] ?? []; + } catch (\Throwable $th) { + $attendances = []; + } + } + + + if (isDemo()) { + $attendances = isSaas() ? $this->getSaasDemoData() : $this->getNonSaasDemoData(); + } + + + // Group by employee code and date + $groupedAttendances = collect($attendances) + ->groupBy(function ($item) { + return $item['emp_code'] . '_' . date('Y-m-d', strtotime($item['punch_time'])); + }) + ->map(function ($dayEntries) { + $sorted = $dayEntries->sortBy('punch_time'); + $firstEntry = $sorted->first(); + $lastEntry = $sorted->last(); + $employee = Employee::with('user')->where('biometric_emp_id', $firstEntry['emp_code'])->first(); + + return [ + 'id' => $firstEntry['id'], + 'employee_code' => $firstEntry['emp_code'], + 'name' => $employee && $employee->user ? $employee->user->name : 'Unknown', + 'date' => date('Y-m-d', strtotime($firstEntry['punch_time'])), + 'clock_in' => date('H:i:s', strtotime($firstEntry['punch_time'])), + 'clock_out' => $sorted->count() > 1 ? date('H:i:s', strtotime($lastEntry['punch_time'])) : null, + 'total_entries' => $sorted->count(), + ]; + }); + $query = $groupedAttendances->values(); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query = $query->filter(function ($item) use ($request) { + return stripos($item['name'], $request->search) !== false || + stripos($item['employee_code'], $request->search) !== false; + }); + } + + // Handle date range filter + if ($request->has('start_date') && !empty($request->start_date)) { + $query = $query->filter(function ($item) use ($request) { + return $item['date'] = $request->start_date; + }); + } + + if ($request->has('end_date') && !empty($request->end_date)) { + $query = $query->filter(function ($item) use ($request) { + return $item['date'] <= $request->end_date; + }); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $direction = $request->sort_direction ?? 'asc'; + $query = $query->sortBy($request->sort_field, SORT_REGULAR, $direction === 'desc'); + } else { + $query = $query->sortByDesc('id'); + } + + // Manual pagination for collection + $perPage = $request->per_page ?? 10; + $currentPage = $request->page ?? 1; + $total = $query->count(); + $items = $query->forPage($currentPage, $perPage)->values(); + + $biometricData = new LengthAwarePaginator( + $items, + $total, + $perPage, + $currentPage, + [ + 'path' => $request->url(), + 'pageName' => 'page', + ] + ); + return Inertia::render('hr/biometric-attendance/index', [ + 'biometricData' => $biometricData, + 'filters' => $request->all(['search', 'start_date', 'end_date', 'sort_field', 'sort_direction', 'per_page']), + 'configurationMissing' => $configurationMissing, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function show(Request $request, $employeeCode, $date) + { + try { + if (!Auth::user()->can('view-biometric-attendance')) { + return response()->json([ + 'success' => false, + 'message' => 'Permission denied' + ], 403); + } + + if (isDemo()) { + $allDemoData = isSaas() ? $this->getSaasDemoData() : $this->getNonSaasDemoData(); + $attendances = collect($allDemoData) + ->filter(function ($item) use ($employeeCode, $date) { + return $item['emp_code'] === $employeeCode && + date('Y-m-d', strtotime($item['punch_time'])) === $date; + }) + ->values() + ->toArray(); + } else { + $company_setting = settings(); + $token = $company_setting['zkteco_auth_token'] ?? ''; + $api_urls = $company_setting['zkteco_api_url'] ?? ''; + + if (empty($token) || empty($api_urls)) { + return response()->json([ + 'success' => false, + 'message' => 'ZKTeco API configuration missing' + ], 400); + } + + $start_date = $date . ' 00:00:00'; + $end_date = $date . ' 23:59:59'; + + $api_url = rtrim($api_urls, '/'); + $url = $api_url . '/iclock/api/transactions/?' . http_build_query([ + 'emp_code' => $employeeCode, + 'start_time' => $start_date, + 'end_time' => $end_date, + 'page_size' => 1000, + ]); + + $curl = curl_init(); + curl_setopt_array($curl, [ + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'Authorization: Token ' . $token + ], + ]); + + $response = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if ($httpCode !== 200 || !$response) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch data from ZKTeco API' + ], 500); + } + + $json_attendance = json_decode($response, true); + $attendances = $json_attendance['data'] ?? []; + } + + + // Process entries (already filtered by API) + $dayEntries = collect($attendances) + ->sortBy('punch_time') + ->map(function ($item) { + return [ + 'id' => $item['id'], + 'punch_time' => $item['punch_time'], + 'time' => date('H:i:s', strtotime($item['punch_time'])), + 'punch_state_display' => $item['punch_state_display'] ?? 'Unknown', + 'verify_type_display' => $item['verify_type_display'] ?? 'Unknown', + 'terminal_alias' => $item['terminal_alias'] ?? 'Unknown' + ]; + }) + ->values(); + + if ($dayEntries->isEmpty()) { + return response()->json([ + 'success' => false, + 'message' => 'No entries found for this employee and date' + ], 404); + } + + return response()->json([ + 'success' => true, + 'data' => [ + 'entries' => $dayEntries, + 'employee_code' => $employeeCode, + 'date' => $date + ] + ]); + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Failed to fetch attendance details' + ], 500); + } + } + + public function sync(Request $request, $id) + { + if (Auth::user()->can('sync-biometric-attendance')) { + $biometricEmpId = $request->biometric_emp_id; + $biometricId = $request->biometric_id; + // $punchTime = $request->punch_time; + $attedanceDate = $request->date; + $clockInTime = $request->clock_in; + $clockOutTime = $request->clock_out; + + $company_setting = settings(); + + if (empty($company_setting['zkteco_auth_token'])) { + return redirect()->back()->with('error', __('Create the Auth Token From the Setting page.')); + } + + $employee = Employee::with('user')->whereIn('created_by', getCompanyAndUsersId())->where('biometric_emp_id', $biometricEmpId)->first(); + + if ($employee) { + + if (is_null($clockOutTime)) { + return redirect()->back()->with('error', __("Still Employee is not Clock Out. So You Can't Sync That Attedance.")); + } + + // Check Attendance Already Sync or not + $attendance = AttendanceRecord::where('biometric_id', $biometricId) + ->where('date', $attedanceDate) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($attendance) { + return redirect()->back()->with('error', __('Attendance Is Alredady Sync.')); + } + + // Check if record already exists + $exists = AttendanceRecord::where('employee_id', $employee->user_id) + ->where('date', $attedanceDate) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Attendance record already exists for this employee and date.')); + } else { + $shift = Shift::where('id', $employee->shift_id) + ->where('status', 'active') + ->first(); + + if (!$shift) { + $shift = Shift::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->first(); + } + + $policy = AttendancePolicy::where('id', $employee->attendance_policy_id) + ->where('status', 'active') + ->first(); + + if (!$policy) { + $policy = AttendancePolicy::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->first(); + } + + + $attendance = new AttendanceRecord(); + $attendance->employee_id = $employee->user_id; + $attendance->biometric_id = $biometricId; + $attendance->shift_id = $shift?->id; + $attendance->attendance_policy_id = $policy?->id; + $attendance->date = $attedanceDate; + $attendance->clock_in = $clockInTime; + $attendance->clock_out = $clockOutTime; + $attendance->created_by = creatorId(); + $attendance->save(); + + $attendance->fresh(); // Reload to get relationships + $attendance->processAttendance(); + + return redirect()->back()->with('success', __('Biometric data synced successfully.')); + } + } else { + return redirect()->back()->with('error', __('Employee not found')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function getSaasDemoData() + { + $data = [ + + // 10/01/2025 + [ + "id" => 2285, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-01 12:54:23" + ], + [ + "id" => 2286, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-01 13:05:23" + ], + [ + "id" => 2287, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-01 14:05:23" + ], + [ + "id" => 2288, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-01 18:35:23" + ], + + + [ + "id" => 2289, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-01 09:05:23" + ], + [ + "id" => 2290, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-01 18:35:23" + ], + + + [ + "id" => 2291, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-01 09:05:00" + ], + [ + "id" => 2292, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-01 18:35:00" + ], + + // 10/02/2025 + [ + "id" => 2293, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-02 09:10:23" + ], + [ + "id" => 2294, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-02 13:05:23" + ], + [ + "id" => 2295, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-02 14:05:23" + ], + [ + "id" => 2296, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-02 18:35:23" + ], + + + [ + "id" => 2297, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-02 09:05:23" + ], + [ + "id" => 2298, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-02 18:35:23" + ], + + + [ + "id" => 2299, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-02 09:05:00" + ], + [ + "id" => 2300, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-02 18:35:00" + ], + + // 10/03/2025 + [ + "id" => 2301, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-03 09:10:23" + ], + [ + "id" => 2302, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-03 13:05:23" + ], + [ + "id" => 2303, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-03 14:05:23" + ], + [ + "id" => 2304, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-03 18:35:23" + ], + + + [ + "id" => 2305, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-03 09:05:23" + ], + [ + "id" => 2306, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-03 18:35:23" + ], + + + [ + "id" => 2307, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-03 09:05:00" + ], + [ + "id" => 2308, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-03 18:35:00" + ], + + + // 10/06/2025 + [ + "id" => 2309, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-06 09:10:23" + ], + [ + "id" => 2310, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-06 13:05:23" + ], + [ + "id" => 2311, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-06 14:05:23" + ], + [ + "id" => 2312, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-06 18:35:23" + ], + + + [ + "id" => 2313, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-06 09:05:23" + ], + [ + "id" => 2314, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-06 18:35:23" + ], + + + [ + "id" => 2315, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-06 09:05:00" + ], + [ + "id" => 2316, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-06 18:35:00" + ], + + + // 10/07/2025 + [ + "id" => 2317, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-07 09:10:23" + ], + [ + "id" => 2318, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-07 13:05:23" + ], + [ + "id" => 2319, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-07 14:05:23" + ], + [ + "id" => 2320, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-07 18:35:23" + ], + + + [ + "id" => 2321, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-07 09:05:23" + ], + [ + "id" => 2322, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-07 18:35:23" + ], + + + [ + "id" => 2323, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-07 09:05:00" + ], + [ + "id" => 2324, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-07 18:35:00" + ], + + // 10/08/2025 + [ + "id" => 2325, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-08 09:10:23" + ], + [ + "id" => 2326, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-08 13:05:23" + ], + [ + "id" => 2327, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-08 14:05:23" + ], + [ + "id" => 2328, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-08 18:35:23" + ], + + + [ + "id" => 2329, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-08 09:05:23" + ], + [ + "id" => 2330, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-08 18:35:23" + ], + + + [ + "id" => 2331, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-08 09:05:00" + ], + [ + "id" => 2332, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-08 18:35:00" + ], + + + // 10/09/2025 + [ + "id" => 2333, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-09 09:10:23" + ], + [ + "id" => 2334, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-09 13:05:23" + ], + [ + "id" => 2335, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-09 14:05:23" + ], + [ + "id" => 2336, + "emp" => 10, + "emp_code" => "201", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-09 18:35:23" + ], + + + [ + "id" => 2337, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-09 09:05:23" + ], + [ + "id" => 2338, + "emp" => 10, + "emp_code" => "202", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-09 18:35:23" + ], + + + [ + "id" => 2339, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-09 09:05:00" + ], + [ + "id" => 2340, + "emp" => 10, + "emp_code" => "203", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-09 18:35:00" + ], + ]; + + return $data; + } + + public function getNonSaasDemoData() + { + $data = [ + + // 10/01/2025 + [ + "id" => 2285, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-01 12:54:23" + ], + [ + "id" => 2286, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-01 13:05:23" + ], + [ + "id" => 2287, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-01 14:05:23" + ], + [ + "id" => 2288, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-01 18:35:23" + ], + + + [ + "id" => 2289, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-01 09:05:23" + ], + [ + "id" => 2290, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-01 18:35:23" + ], + + + [ + "id" => 2291, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-01 09:05:00" + ], + [ + "id" => 2292, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-01 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-01 18:35:00" + ], + + // 10/02/2025 + [ + "id" => 2293, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-02 09:10:23" + ], + [ + "id" => 2294, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-02 13:05:23" + ], + [ + "id" => 2295, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-02 14:05:23" + ], + [ + "id" => 2296, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-02 18:35:23" + ], + + + [ + "id" => 2297, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-02 09:05:23" + ], + [ + "id" => 2298, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-02 18:35:23" + ], + + + [ + "id" => 2299, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-02 09:05:00" + ], + [ + "id" => 2300, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-02 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-02 18:35:00" + ], + + // 10/03/2025 + [ + "id" => 2301, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-03 09:10:23" + ], + [ + "id" => 2302, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-03 13:05:23" + ], + [ + "id" => 2303, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-03 14:05:23" + ], + [ + "id" => 2304, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-03 18:35:23" + ], + + + [ + "id" => 2305, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-03 09:05:23" + ], + [ + "id" => 2306, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-03 18:35:23" + ], + + + [ + "id" => 2307, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-03 09:05:00" + ], + [ + "id" => 2308, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-03 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-03 18:35:00" + ], + + + // 10/06/2025 + [ + "id" => 2309, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-06 09:10:23" + ], + [ + "id" => 2310, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-06 13:05:23" + ], + [ + "id" => 2311, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-06 14:05:23" + ], + [ + "id" => 2312, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-06 18:35:23" + ], + + + [ + "id" => 2313, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-06 09:05:23" + ], + [ + "id" => 2314, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-06 18:35:23" + ], + + + [ + "id" => 2315, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-06 09:05:00" + ], + [ + "id" => 2316, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-06 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-06 18:35:00" + ], + + + // 10/07/2025 + [ + "id" => 2317, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-07 09:10:23" + ], + [ + "id" => 2318, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-07 13:05:23" + ], + [ + "id" => 2319, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-07 14:05:23" + ], + [ + "id" => 2320, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-07 18:35:23" + ], + + + [ + "id" => 2321, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-07 09:05:23" + ], + [ + "id" => 2322, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-07 18:35:23" + ], + + + [ + "id" => 2323, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-07 09:05:00" + ], + [ + "id" => 2324, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-07 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-07 18:35:00" + ], + + // 10/08/2025 + [ + "id" => 2325, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-08 09:10:23" + ], + [ + "id" => 2326, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-08 13:05:23" + ], + [ + "id" => 2327, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-08 14:05:23" + ], + [ + "id" => 2328, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-08 18:35:23" + ], + + + [ + "id" => 2329, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-08 09:05:23" + ], + [ + "id" => 2330, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-08 18:35:23" + ], + + + [ + "id" => 2331, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-08 09:05:00" + ], + [ + "id" => 2332, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-08 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-08 18:35:00" + ], + + + // 10/09/2025 + [ + "id" => 2333, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 09:00:17", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-09 09:10:23" + ], + [ + "id" => 2334, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 13:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-09 13:05:23" + ], + [ + "id" => 2335, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 14:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-09 14:05:23" + ], + [ + "id" => 2336, + "emp" => 10, + "emp_code" => "101", + "first_name" => "Mrs. Elvie Towne IV", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2024-10-09 18:35:23" + ], + + + [ + "id" => 2337, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-09 09:05:23" + ], + [ + "id" => 2338, + "emp" => 10, + "emp_code" => "102", + "first_name" => "Effie Wilderman", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-09 18:35:23" + ], + + + [ + "id" => 2339, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 09:00:00", + "punch_state" => "255", + "punch_state_display" => "Clock In", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-09 09:05:00" + ], + [ + "id" => 2340, + "emp" => 10, + "emp_code" => "103", + "first_name" => "Tomasa Mitchell", + "last_name" => null, + "department" => "Support", + "position" => "Technical Support", + "punch_time" => "2025-10-09 18:30:00", + "punch_state" => "255", + "punch_state_display" => "Clock Out", + "verify_type" => 1, + "verify_type_display" => "Fingerprint", + "work_code" => "", + "gps_location" => null, + "area_alias" => "Operation Office", + "terminal_sn" => "COAW221061101", + "temperature" => 0.0, + "is_mask" => "-", + "terminal_alias" => "F18/ID", + "upload_time" => "2025-10-09 18:35:00" + ], + ]; + + return $data; + } +} diff --git a/app/Http/Controllers/BranchController.php b/app/Http/Controllers/BranchController.php new file mode 100644 index 000000000..0d36e7082 --- /dev/null +++ b/app/Http/Controllers/BranchController.php @@ -0,0 +1,195 @@ +can('manage-branches')) { + $query = Branch::where(function ($q) { + if (Auth::user()->can('manage-any-branches')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-branches')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where('name', 'like', '%' . $request->search . '%') + ->orWhere('email', 'like', '%' . $request->search . '%') + ->orWhere('phone', 'like', '%' . $request->search . '%'); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'id'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'id'; + } + + $query->orderBy($sortField, $sortDirection); + + $branches = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/branches/index', [ + 'branches' => $branches, + 'filters' => $request->all(['search', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + if (Auth::user()->can('create-branches')) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'address' => 'nullable|string', + 'city' => 'nullable|string|max:100', + 'state' => 'nullable|string|max:100', + 'country' => 'nullable|string|max:100', + 'zip_code' => 'nullable|string|max:20', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:255', + 'status' => 'nullable|in:active,inactive', + ]); + + $validated['created_by'] = creatorId(); + $validated['status'] = $validated['status'] ?? 'active'; + + // Check if branch with same name already exists + $exists = Branch::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Branch with this name already exists.')); + } + + Branch::create($validated); + + return redirect()->back()->with('success', __('Branch created successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to create branch')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function update(Request $request, $branchId) + { + if (Auth::user()->can('edit-branches')) { + $branch = Branch::where('id', $branchId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($branch) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'address' => 'nullable|string', + 'city' => 'nullable|string|max:100', + 'state' => 'nullable|string|max:100', + 'country' => 'nullable|string|max:100', + 'zip_code' => 'nullable|string|max:20', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:255', + 'status' => 'nullable|in:active,inactive', + ]); + + // Check if branch with same name already exists (excluding current branch) + $exists = Branch::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('id', '!=', $branchId) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Branch with this name already exists.')); + } + + $branch->update($validated); + + return redirect()->back()->with('success', __('Branch updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update branch')); + } + } else { + return redirect()->back()->with('error', __('Branch not found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function destroy($branchId) + { + if (Auth::user()->can('delete-branches')) { + $branch = Branch::where('id', $branchId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($branch) { + try { + // Check if branch has departments + if (class_exists('App\\Models\\Department')) { + $departmentCount = \App\Models\Department::where('branch_id', $branchId)->count(); + if ($departmentCount > 0) { + return redirect()->back()->with('error', __('Cannot delete branch with assigned departments')); + } + } + + $branch->delete(); + return redirect()->back()->with('success', __('Branch deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete branch')); + } + } else { + return redirect()->back()->with('error', __('Branch not found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function toggleStatus($branchId) + { + if (Auth::user()->can('toggle-status-branches')) { + $branch = Branch::where('id', $branchId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($branch) { + try { + $branch->status = $branch->status === 'active' ? 'inactive' : 'active'; + $branch->save(); + + return redirect()->back()->with('success', __('Branch status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update branch status')); + } + } else { + return redirect()->back()->with('error', __('Branch not found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/CalendarController.php b/app/Http/Controllers/CalendarController.php new file mode 100644 index 000000000..6b3fd0a80 --- /dev/null +++ b/app/Http/Controllers/CalendarController.php @@ -0,0 +1,208 @@ +user(); + + if ($user->type === 'employee') { + if (! $user->hasPermissionTo('view-calendar')) { + abort(403, 'Unauthorized'); + } + } else { + if (! $user->hasPermissionTo('manage-calendar') && ! $user->hasPermissionTo('view-calendar')) { + abort(403, 'Unauthorized'); + } + } + + $companyUserIds = getCompanyAndUsersId(); + + if (isDemo()) { + // Static data for demo mode - 12 months + $meetings = collect(); + $holidays = collect(); + $leaves = collect(); + + for ($month = 1; $month <= 12; $month++) { + $date = now()->month($month); + + // 3 meetings per month + $meetings->push([ + 'id' => 'meeting_' . $month . '_1', + 'title' => 'Team Meeting', + 'start' => $date->copy()->day(5)->format('Y-m-d').'T10:00:00', + 'end' => $date->copy()->day(5)->format('Y-m-d').'T11:00:00', + 'type' => 'meeting', + 'status' => 'scheduled', + 'backgroundColor' => '#3b82f6', + 'borderColor' => '#3b82f6', + ]); + + $meetings->push([ + 'id' => 'meeting_' . $month . '_2', + 'title' => 'Project Review', + 'start' => $date->copy()->day(12)->format('Y-m-d').'T14:00:00', + 'end' => $date->copy()->day(12)->format('Y-m-d').'T15:30:00', + 'type' => 'meeting', + 'status' => 'scheduled', + 'backgroundColor' => '#3b82f6', + 'borderColor' => '#3b82f6', + ]); + + $meetings->push([ + 'id' => 'meeting_' . $month . '_3', + 'title' => 'Client Presentation', + 'start' => $date->copy()->day(20)->format('Y-m-d').'T09:00:00', + 'end' => $date->copy()->day(20)->format('Y-m-d').'T10:30:00', + 'type' => 'meeting', + 'status' => 'scheduled', + 'backgroundColor' => '#3b82f6', + 'borderColor' => '#3b82f6', + ]); + + // 3 holidays per month + $holidays->push([ + 'id' => 'holiday_' . $month . '_1', + 'title' => 'Company Foundation Day', + 'start' => $date->copy()->day(1)->format('Y-m-d'), + 'end' => $date->copy()->day(1)->format('Y-m-d'), + 'type' => 'holiday', + 'allDay' => true, + 'backgroundColor' => '#10b77f', + 'borderColor' => '#10b77f', + ]); + + $holidays->push([ + 'id' => 'holiday_' . $month . '_2', + 'title' => 'National Holiday', + 'start' => $date->copy()->day(15)->format('Y-m-d'), + 'end' => $date->copy()->day(15)->format('Y-m-d'), + 'type' => 'holiday', + 'allDay' => true, + 'backgroundColor' => '#10b77f', + 'borderColor' => '#10b77f', + ]); + + $holidays->push([ + 'id' => 'holiday_' . $month . '_3', + 'title' => 'Festival Holiday', + 'start' => $date->copy()->day(25)->format('Y-m-d'), + 'end' => $date->copy()->day(25)->format('Y-m-d'), + 'type' => 'holiday', + 'allDay' => true, + 'backgroundColor' => '#10b77f', + 'borderColor' => '#10b77f', + ]); + + // 3 leaves per month + $leaves->push([ + 'id' => 'leave_' . $month . '_1', + 'title' => 'John Doe - Sick Leave', + 'start' => $date->copy()->day(3)->format('Y-m-d'), + 'end' => $date->copy()->day(5)->format('Y-m-d'), + 'type' => 'leave', + 'allDay' => true, + 'backgroundColor' => '#f59e0b', + 'borderColor' => '#f59e0b', + ]); + + $leaves->push([ + 'id' => 'leave_' . $month . '_2', + 'title' => 'Jane Smith - Annual Leave', + 'start' => $date->copy()->day(10)->format('Y-m-d'), + 'end' => $date->copy()->day(13)->format('Y-m-d'), + 'type' => 'leave', + 'allDay' => true, + 'backgroundColor' => '#f59e0b', + 'borderColor' => '#f59e0b', + ]); + + $leaves->push([ + 'id' => 'leave_' . $month . '_3', + 'title' => 'Mike Johnson - Casual Leave', + 'start' => $date->copy()->day(22)->format('Y-m-d'), + 'end' => $date->copy()->day(23)->format('Y-m-d'), + 'type' => 'leave', + 'allDay' => true, + 'backgroundColor' => '#f59e0b', + 'borderColor' => '#f59e0b', + ]); + } + } else { + // Get meetings + $meetings = Meeting::query() + ->when($user->hasRole('employee'), function ($query) use ($user) { + $query->where('organizer_id', $user->id) + ->orWhereHas('attendees', function ($q) use ($user) { + $q->where('user_id', $user->id); + }); + }, function ($query) use ($companyUserIds) { + $query->whereIn('created_by', $companyUserIds); + }) + ->get() + ->map(function ($meeting) { + return [ + 'id' => $meeting->id, + 'title' => $meeting->title, + 'start' => Carbon::parse($meeting->meeting_date)->format('Y-m-d').'T'.Carbon::parse($meeting->start_time)->format('H:i:s'), + 'end' => Carbon::parse($meeting->meeting_date)->format('Y-m-d').'T'.Carbon::parse($meeting->end_time)->format('H:i:s'), + 'type' => 'meeting', + 'status' => $meeting->status, + 'backgroundColor' => '#3b82f6', + 'borderColor' => '#3b82f6', + ]; + }); + + // Get holidays + $holidays = Holiday::whereIn('created_by', $companyUserIds) + ->get() + ->map(function ($holiday) { + return [ + 'id' => $holiday->id, + 'title' => $holiday->name, + 'start' => $holiday->start_date, + 'end' => $holiday->end_date ?: $holiday->start_date, + 'type' => 'holiday', + 'allDay' => true, + 'backgroundColor' => '#10b77f', + 'borderColor' => '#10b77f', + ]; + }); + + // Get leave applications + $leaves = LeaveApplication::whereIn('created_by', $companyUserIds) + ->where('status', 'approved') + ->with(['employee', 'leaveType']) + ->get() + ->map(function ($leave) { + return [ + 'id' => $leave->id, + 'title' => $leave->employee->name.' - '.$leave->leaveType->name, + 'start' => $leave->start_date, + 'end' => Carbon::parse($leave->end_date)->addDay()->format('Y-m-d'), + 'type' => 'leave', + 'allDay' => true, + 'backgroundColor' => '#f59e0b', + 'borderColor' => '#f59e0b', + ]; + }); + } + + $events = $meetings->concat($holidays)->concat($leaves); + + return Inertia::render('calendar/index', [ + 'events' => $events, + 'canManage' => $user->hasPermissionTo('manage-calendar'), + ]); + } +} diff --git a/app/Http/Controllers/CandidateAssessmentController.php b/app/Http/Controllers/CandidateAssessmentController.php new file mode 100644 index 000000000..0278960a0 --- /dev/null +++ b/app/Http/Controllers/CandidateAssessmentController.php @@ -0,0 +1,162 @@ +can('manage-candidate-assessments')) { + $query = CandidateAssessment::with(['candidate', 'conductor'])->where(function ($q) { + if (Auth::user()->can('manage-any-candidate-assessments')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-candidate-assessments')) { + $q->where('created_by', Auth::id())->orWhere('conducted_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('assessment_name', 'like', '%' . $request->search . '%') + ->orWhereHas('candidate', function ($cq) use ($request) { + $cq->where('first_name', 'like', '%' . $request->search . '%') + ->orWhere('last_name', 'like', '%' . $request->search . '%'); + }); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('pass_fail_status', $request->status); + } + + if ($request->has('candidate_id') && !empty($request->candidate_id) && $request->candidate_id !== 'all') { + $query->where('candidate_id', $request->candidate_id); + } + + // Handle sorting + $sortField = $request->get('sort_field'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['assessment_name', 'assessment_date']; + if ($sortField && in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + $assessments = $query->paginate($request->per_page ?? 10); + + $candidates = Candidate::whereIn('created_by', getCompanyAndUsersId())->where('is_employee',0)->get(); + + $employees = User::with('employee') + ->whereIn('type', ['manager', 'hr', 'employee']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + + return Inertia::render('hr/recruitment/candidate-assessments/index', [ + 'assessments' => $assessments, + 'candidates' => $candidates, + 'employees' => $employees, + 'filters' => $request->all(['search', 'status', 'candidate_id', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'candidate_id' => 'required|exists:candidates,id', + 'assessment_name' => 'required|string|max:255', + 'score' => 'nullable|integer|min:0', + 'max_score' => 'nullable|integer|min:1', + 'pass_fail_status' => 'required|in:Pass,Fail,Pending', + 'comments' => 'nullable|string', + 'conducted_by' => 'required|exists:users,id', + 'assessment_date' => 'required|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + CandidateAssessment::create([ + 'candidate_id' => $request->candidate_id, + 'assessment_name' => $request->assessment_name, + 'score' => $request->score, + 'max_score' => $request->max_score, + 'pass_fail_status' => $request->pass_fail_status, + 'comments' => $request->comments, + 'conducted_by' => $request->conducted_by, + 'assessment_date' => $request->assessment_date, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Assessment created successfully')); + } + + public function update(Request $request, CandidateAssessment $candidateAssessment) + { + if (!in_array($candidateAssessment->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this assessment')); + } + + $validator = Validator::make($request->all(), [ + 'candidate_id' => 'required|exists:candidates,id', + 'assessment_name' => 'required|string|max:255', + 'score' => 'nullable|integer|min:0', + 'max_score' => 'nullable|integer|min:1', + 'pass_fail_status' => 'required|in:Pass,Fail,Pending', + 'comments' => 'nullable|string', + 'conducted_by' => 'required|exists:users,id', + 'assessment_date' => 'required|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $candidateAssessment->update([ + 'candidate_id' => $request->candidate_id, + 'assessment_name' => $request->assessment_name, + 'score' => $request->score, + 'max_score' => $request->max_score, + 'pass_fail_status' => $request->pass_fail_status, + 'comments' => $request->comments, + 'conducted_by' => $request->conducted_by, + 'assessment_date' => $request->assessment_date, + ]); + + return redirect()->back()->with('success', __('Assessment updated successfully')); + } + + public function destroy(CandidateAssessment $candidateAssessment) + { + if (!in_array($candidateAssessment->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this assessment')); + } + + $candidateAssessment->delete(); + return redirect()->back()->with('success', __('Assessment deleted successfully')); + } +} diff --git a/app/Http/Controllers/CandidateController.php b/app/Http/Controllers/CandidateController.php new file mode 100644 index 000000000..67059b512 --- /dev/null +++ b/app/Http/Controllers/CandidateController.php @@ -0,0 +1,457 @@ +can('manage-candidates')) { + $query = Candidate::with(['job', 'source', 'referralEmployee'])->where(function ($q) { + if (Auth::user()->can('manage-any-candidates')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-candidates')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('first_name', 'like', '%' . $request->search . '%') + ->orWhere('last_name', 'like', '%' . $request->search . '%') + ->orWhere('email', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('job_id') && !empty($request->job_id) && $request->job_id !== 'all') { + $query->where('job_id', $request->job_id); + } + + if ($request->has('source_id') && !empty($request->source_id) && $request->source_id !== 'all') { + $query->where('source_id', $request->source_id); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['first_name', 'application_date', 'created_at']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + $candidates = $query->paginate($request->per_page ?? 10); + + $jobPostings = JobPosting::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'title', 'job_code') + ->get(); + + $sources = CandidateSource::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + + return Inertia::render('hr/recruitment/candidates/index', [ + 'candidates' => $candidates, + 'jobPostings' => $jobPostings, + 'sources' => $sources, + 'employees' => $employees, + 'filters' => $request->all(['search', 'status', 'job_id', 'source_id', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'job_id' => 'required|exists:job_postings,id', + 'source_id' => 'required|exists:candidate_sources,id', + 'first_name' => 'required|string|max:255', + 'last_name' => 'required|string|max:255', + 'email' => 'required|email|max:255', + 'phone' => 'nullable|string|max:20', + 'current_company' => 'nullable|string|max:255', + 'current_position' => 'nullable|string|max:255', + 'experience_years' => 'required|integer|min:0', + 'current_salary' => 'nullable|numeric|min:0', + 'expected_salary' => 'nullable|numeric|min:0', + 'notice_period' => 'nullable|string|max:255', + 'skills' => 'nullable|string', + 'education' => 'nullable|string', + 'portfolio_url' => 'nullable|string', + 'linkedin_url' => 'nullable|string', + 'referral_employee_id' => 'nullable|exists:users,id', + 'application_date' => 'required|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + Candidate::create([ + 'job_id' => $request->job_id, + 'source_id' => $request->source_id, + 'first_name' => $request->first_name, + 'last_name' => $request->last_name, + 'email' => $request->email, + 'phone' => $request->phone, + 'current_company' => $request->current_company, + 'current_position' => $request->current_position, + 'experience_years' => $request->experience_years, + 'current_salary' => $request->current_salary, + 'expected_salary' => $request->expected_salary, + 'notice_period' => $request->notice_period, + 'skills' => $request->skills, + 'education' => $request->education, + 'portfolio_url' => $request->portfolio_url ?: null, + 'linkedin_url' => $request->linkedin_url ?: null, + 'referral_employee_id' => $request->referral_employee_id ?: null, + 'application_date' => $request->application_date, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Candidate created successfully')); + } + + public function update(Request $request, Candidate $candidate) + { + if (!in_array($candidate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this candidate'); + } + + $validator = Validator::make($request->all(), [ + 'job_id' => 'required|exists:job_postings,id', + 'source_id' => 'required|exists:candidate_sources,id', + 'first_name' => 'required|string|max:255', + 'last_name' => 'required|string|max:255', + 'email' => 'required|email|max:255', + 'phone' => 'nullable|string|max:20', + 'current_company' => 'nullable|string|max:255', + 'current_position' => 'nullable|string|max:255', + 'experience_years' => 'required|integer|min:0', + 'current_salary' => 'nullable|numeric|min:0', + 'expected_salary' => 'nullable|numeric|min:0', + 'notice_period' => 'nullable|string|max:255', + 'skills' => 'nullable|string', + 'education' => 'nullable|string', + 'portfolio_url' => 'nullable|string', + 'linkedin_url' => 'nullable|string', + 'referral_employee_id' => 'nullable|exists:users,id', + 'application_date' => 'required|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $candidate->update($request->only([ + 'job_id', + 'source_id', + 'first_name', + 'last_name', + 'email', + 'phone', + 'current_company', + 'current_position', + 'experience_years', + 'current_salary', + 'expected_salary', + 'notice_period', + 'skills', + 'education', + 'portfolio_url', + 'linkedin_url', + 'referral_employee_id', + 'application_date' + ])); + + return redirect()->back()->with('success', __('Candidate updated successfully')); + } + + public function destroy(Candidate $candidate) + { + if (!in_array($candidate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this candidate'); + } + + $candidate->delete(); + return redirect()->back()->with('success', __('Candidate deleted successfully')); + } + + public function show(Candidate $candidate) + { + if (!in_array($candidate->created_by, getCompanyAndUsersId())) { + return abort(404); + } + + $candidate->load([ + 'job.location', + 'job.jobType', + 'source', + 'referralEmployee', + 'branch', + 'department' + ]); + + return Inertia::render('hr/recruitment/candidates/show', [ + 'candidate' => $candidate, + ]); + } + public function updateStatus(Request $request, Candidate $candidate) + { + if (!in_array($candidate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this candidate'); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:New,Screening,Interview,Offer,Hired,Rejected', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $candidate->update(['status' => $request->status]); + return redirect()->back()->with('success', __('Candidate status updated successfully')); + } + + public function convertToEmployee(Candidate $candidate) + { + try { + if (Auth::user()->can('convert-to-employee')) { + if (!in_array($candidate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to convert this candidate')); + } + + if ($candidate->status !== 'Hired') { + return redirect()->back()->with('error', __('Only hired candidates can be converted to employees')); + } + + if ($candidate->is_employee) { + return redirect()->back()->with('error', __('This candidate has already been converted to an employee')); + } + + // Check if candidate has an accepted offer + $acceptedOffer = Offer::where('candidate_id', $candidate->id) + ->where('status', 'Accepted') + ->first(); + + if (!$acceptedOffer) { + return redirect()->back()->with('error', __('Candidate must have an accepted offer before conversion to employee')); + } + + // Get data needed for employee creation form + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name']); + + $departments = Department::with('branch') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'branch_id']); + + $designations = Designation::with('department') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'department_id']); + + $documentTypes = DocumentType::whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name', 'is_required']); + + $shifts = Shift::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'start_time', 'end_time']); + + $attendancePolicies = AttendancePolicy::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name']); + + return Inertia::render('hr/recruitment/candidates/convert-to-employee', [ + 'candidate' => $candidate->load(['job', 'source']), + 'branches' => $branches, + 'departments' => $departments, + 'designations' => $designations, + 'documentTypes' => $documentTypes, + 'shifts' => $shifts, + 'attendancePolicies' => $attendancePolicies, + 'generatedEmployeeId' => Employee::generateEmployeeId(), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } catch (\Exception $e) { + \Log::error('Convert to employee page load failed: ' . $e->getMessage()); + return redirect()->back()->with('error', __('Failed to load conversion page: :message', ['message' => $e->getMessage()])); + } + } + + public function storeEmployee(Request $request) + { + try { + // Validate the request + $validator = Validator::make($request->all(), [ + 'candidate_id' => 'required|exists:candidates,id', + 'name' => 'required|string|max:255', + 'email' => 'required|email|max:255|unique:users,email', + 'password' => 'required|string|min:8', + 'phone' => 'required|string|max:20', + 'date_of_birth' => 'required|date', + 'gender' => 'required|in:male,female,other', + 'branch_id' => 'required|exists:branches,id', + 'department_id' => 'required|exists:departments,id', + 'designation_id' => 'required|exists:designations,id', + 'date_of_joining' => 'required|date', + 'employment_type' => 'required|string|max:50', + 'address_line_1' => 'required|string|max:255', + 'city' => 'required|string|max:100', + 'state' => 'required|string|max:100', + 'country' => 'required|string|max:100', + 'postal_code' => 'required|string|max:20', + 'emergency_contact_name' => 'required|string|max:255', + 'emergency_contact_relationship' => 'required|string|max:100', + 'emergency_contact_number' => 'required|string|max:20', + 'bank_name' => 'required|string|max:255', + 'account_holder_name' => 'required|string|max:255', + 'account_number' => 'required|string|max:50', + 'salary' => 'required|numeric|min:0', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Get candidate + $candidate = \App\Models\Candidate::findOrFail($request->candidate_id); + + // Check permissions and status + if (!in_array($candidate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Permission denied'); + } + + if ($candidate->status !== 'Hired' || $candidate->is_employee) { + return redirect()->back()->with('error', 'Invalid candidate status for conversion'); + } + + // Create User + $user = \App\Models\User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => \Illuminate\Support\Facades\Hash::make($request->password), + 'type' => 'employee', + 'lang' => 'en', + 'avatar' => $request->profile_image, + 'created_by' => creatorId(), + ]); + + // Assign Employee role + if (isSaas()) { + $employeeRole = \Spatie\Permission\Models\Role::where('created_by', createdBy())->where('name', 'employee')->first(); + } else { + $employeeRole = \Spatie\Permission\Models\Role::where('name', 'employee')->first(); + } + if ($employeeRole) { + $user->assignRole($employeeRole); + } + + // Create Employee + $employee = \App\Models\Employee::create([ + 'user_id' => $user->id, + 'employee_id' => \App\Models\Employee::generateEmployeeId(), + 'biometric_emp_id' => $request->biometric_emp_id, + 'phone' => $request->phone, + 'date_of_birth' => $request->date_of_birth, + 'gender' => $request->gender, + 'branch_id' => $request->branch_id, + 'department_id' => $request->department_id, + 'designation_id' => $request->designation_id, + 'shift_id' => $request->shift_id, + 'attendance_policy_id' => $request->attendance_policy_id, + 'date_of_joining' => $request->date_of_joining, + 'employment_type' => $request->employment_type, + 'employee_status' => $request->employee_status ?? 'active', + 'address_line_1' => $request->address_line_1, + 'address_line_2' => $request->address_line_2, + 'city' => $request->city, + 'state' => $request->state, + 'country' => $request->country, + 'postal_code' => $request->postal_code, + 'emergency_contact_name' => $request->emergency_contact_name, + 'emergency_contact_relationship' => $request->emergency_contact_relationship, + 'emergency_contact_number' => $request->emergency_contact_number, + 'bank_name' => $request->bank_name, + 'account_holder_name' => $request->account_holder_name, + 'account_number' => $request->account_number, + 'bank_identifier_code' => $request->bank_identifier_code, + 'bank_branch' => $request->bank_branch, + 'tax_payer_id' => $request->tax_payer_id, + 'created_by' => creatorId(), + ]); + + // Handle documents + if ($request->has('documents') && is_array($request->documents)) { + foreach ($request->documents as $document) { + if (isset($document['file_path']) && !empty($document['file_path'])) { + \App\Models\EmployeeDocument::create([ + 'employee_id' => $employee->user_id, + 'document_type_id' => $document['document_type_id'], + 'file_path' => $document['file_path'], + 'expiry_date' => $document['expiry_date'] ?? null, + 'verification_status' => 'pending', + 'created_by' => creatorId(), + ]); + } + } + } + + // Mark candidate as converted + $candidate->update(['is_employee' => true]); + + return redirect()->route('hr.employees.index')->with('success', __('Candidate converted to employee successfully')); + + } catch (\Exception $e) { + \Log::error('Candidate to employee conversion failed: ' . $e->getMessage()); + return redirect()->back()->with('error', __('Failed to convert candidate: :message', ['message' => $e->getMessage()]))->withInput(); + } + } +} diff --git a/app/Http/Controllers/CandidateOnboardingController.php b/app/Http/Controllers/CandidateOnboardingController.php new file mode 100644 index 000000000..bcf34a474 --- /dev/null +++ b/app/Http/Controllers/CandidateOnboardingController.php @@ -0,0 +1,226 @@ +can('manage-candidate-onboarding')) { + $query = CandidateOnboarding::with(['employee', 'checklist', 'buddyEmployee'])->where(function ($q) { + if (Auth::user()->can('manage-any-candidate-onboarding')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-candidate-onboarding')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('buddy_employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('employee_id') && !empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + // Handle sorting + $sortField = $request->get('sort_field'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['start_date', 'created_at']; + if ($sortField && in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + $candidateOnboarding = $query->paginate($request->per_page ?? 10); + + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->orderBy('id', 'desc') + ->get(); + + $checklists = OnboardingChecklist::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/recruitment/candidate-onboarding/index', [ + 'candidateOnboarding' => $candidateOnboarding, + 'employees' => $this->getFilteredEmployees(), + 'checklists' => $checklists, + 'buddyEmployees' => $employees, + 'filters' => $request->all(['search', 'status', 'employee_id', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-candidate-onboarding') && !Auth::user()->can('manage-any-candidate-onboarding')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->orderBy('id', 'desc') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + return $employees; + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'checklist_id' => 'required|exists:onboarding_checklists,id', + 'start_date' => 'required|date', + 'buddy_employee_id' => 'nullable|exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if checklist is already assigned to this employee + $exists = CandidateOnboarding::where('employee_id', $request->employee_id) + ->where('checklist_id', $request->checklist_id) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('This checklist is already assigned to the selected employee')); + } + + CandidateOnboarding::create([ + 'employee_id' => $request->employee_id, + 'checklist_id' => $request->checklist_id, + 'start_date' => $request->start_date, + 'buddy_employee_id' => $request->buddy_employee_id, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Candidate onboarding created successfully')); + } + + public function update(Request $request, CandidateOnboarding $candidateOnboarding) + { + if (!in_array($candidateOnboarding->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this onboarding')); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'checklist_id' => 'required|exists:onboarding_checklists,id', + 'start_date' => 'required|date', + 'buddy_employee_id' => 'nullable|exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if checklist is already assigned to this employee (excluding current record) + $exists = CandidateOnboarding::where('employee_id', $request->employee_id) + ->where('checklist_id', $request->checklist_id) + ->where('id', '!=', $candidateOnboarding->id) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('This checklist is already assigned to the selected employee')); + } + + $candidateOnboarding->update([ + 'employee_id' => $request->employee_id, + 'checklist_id' => $request->checklist_id, + 'start_date' => $request->start_date, + 'buddy_employee_id' => $request->buddy_employee_id, + ]); + + return redirect()->back()->with('success', __('Candidate onboarding updated successfully')); + } + + public function destroy(CandidateOnboarding $candidateOnboarding) + { + if (!in_array($candidateOnboarding->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this onboarding')); + } + + $candidateOnboarding->delete(); + return redirect()->back()->with('success', __('Candidate onboarding deleted successfully')); + } + + public function show(CandidateOnboarding $candidateOnboarding) + { + if (!in_array($candidateOnboarding->created_by, getCompanyAndUsersId())) { + return abort(404); + } + + $candidateOnboarding->load([ + 'employee', + 'checklist.checklistItems', + 'buddyEmployee', + 'creator' + ]); + + return Inertia::render('hr/recruitment/candidate-onboarding/show', [ + 'candidateOnboarding' => $candidateOnboarding, + ]); + } + + public function updateStatus(Request $request, CandidateOnboarding $candidateOnboarding) + { + if (!in_array($candidateOnboarding->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this onboarding')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:Pending,In Progress,Completed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $candidateOnboarding->update(['status' => $request->status]); + return redirect()->back()->with('success', __('Onboarding status updated successfully')); + } +} diff --git a/app/Http/Controllers/CandidateSourceController.php b/app/Http/Controllers/CandidateSourceController.php new file mode 100644 index 000000000..05458554a --- /dev/null +++ b/app/Http/Controllers/CandidateSourceController.php @@ -0,0 +1,132 @@ +can('manage-candidate-sources')) { + $query = CandidateSource::where(function ($q) { + if (Auth::user()->can('manage-any-candidate-sources')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-candidate-sources')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + $candidateSources = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/candidate-sources/index', [ + 'candidateSources' => $candidateSources, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + CandidateSource::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Candidate source created successfully')); + } + + public function update(Request $request, CandidateSource $candidateSource) + { + if (!in_array($candidateSource->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this candidate source'); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $candidateSource->update([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Candidate source updated successfully')); + } + + public function destroy(CandidateSource $candidateSource) + { + if (!in_array($candidateSource->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this candidate source'); + } + + if ($candidateSource->candidates()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete candidate source as it is being used by candidates')); + } + + $candidateSource->delete(); + return redirect()->back()->with('success', __('Candidate source deleted successfully')); + } + + public function toggleStatus(CandidateSource $candidateSource) + { + if (!in_array($candidateSource->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this candidate source'); + } + + $candidateSource->update([ + 'status' => $candidateSource->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Candidate source status updated successfully')); + } +} diff --git a/app/Http/Controllers/CareerController.php b/app/Http/Controllers/CareerController.php new file mode 100644 index 000000000..cccd5696e --- /dev/null +++ b/app/Http/Controllers/CareerController.php @@ -0,0 +1,419 @@ +get('companyId'); + $companySettings = $request->get('companySettings'); + $userSlug = $request->get('userSlug'); + + $query = JobPosting::with(['jobType', 'location', 'branch', 'department']) + ->where('is_published', true) + ->where('status', 'Published'); + + if ($companyId) { + $query->whereIn('created_by', getCompanyUsers($companyId)); + } + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('job_type') && !empty($request->job_type)) { + $jobTypeIds = explode(',', $request->job_type); + $query->whereIn('job_type_id', $jobTypeIds); + } + + if ($request->has('location') && !empty($request->location)) { + $query->where('location_id', $request->location); + } + + if ($request->has('salary_range') && !empty($request->salary_range)) { + switch ($request->salary_range) { + case '0-50k': + $query->where('max_salary', '<=', 50000); + break; + case '50k-100k': + $query->whereBetween('min_salary', [50000, 100000]); + break; + case '100k+': + $query->where('min_salary', '>=', 100000); + break; + } + } + + if ($request->has('vacancies') && !empty($request->vacancies)) { + $vacancyRanges = explode(',', $request->vacancies); + $query->where(function ($q) use ($vacancyRanges) { + foreach ($vacancyRanges as $range) { + switch ($range) { + case '1-5': + $q->orWhereBetween('positions', [1, 5]); + break; + case '6-15': + $q->orWhereBetween('positions', [6, 15]); + break; + case '16-25': + $q->orWhereBetween('positions', [16, 25]); + break; + case '25+': + $q->orWhere('positions', '>', 25); + break; + } + } + }); + } + + // $query = $query->orderBy('is_featured', 'desc'); + + if ($request->has('sort') && !empty($request->sort)) { + switch ($request->sort) { + case 'oldest': + $query = $query->orderBy('created_at', 'asc'); + break; + case 'salary-high': + $query = $query->orderBy('max_salary', 'desc'); + break; + case 'salary-low': + $query = $query->orderBy('min_salary', 'asc'); + break; + default: // newest + $query = $query->orderBy('created_at', 'desc'); + break; + } + } else { + $query = $query->orderBy('created_at', 'desc'); + } + + $jobPostings = $query->paginate(6); + + $jobTypes = JobType::where('status', 'active')->whereIn('created_by', getCompanyUsers($companyId))->get(); + $locations = JobLocation::where('status', 'active')->whereIn('created_by', getCompanyUsers($companyId))->get(); + $vacancyRanges = [ + ['value' => '1-5', 'label' => '1-5'], + ['value' => '6-15', 'label' => '6-15'], + ['value' => '16-25', 'label' => '16-25'], + ['value' => '25+', 'label' => '25+'], + ]; + + return Inertia::render('career/index', [ + 'jobPostings' => $jobPostings, + 'jobTypes' => $jobTypes, + 'locations' => $locations, + 'companyId' => $companyId, + 'userSlug' => $userSlug, + 'companySettings' => $companySettings, + 'vacancyRanges' => $vacancyRanges, + 'filters' => $request->all(keys: ['search', 'job_type', 'location', 'salary_range', 'vacancies', 'sort']), + ]); + } + + public function show(Request $request, $userSlug, $jobCode) + { + try { + // Get company data from middleware + $companyId = $request->get('companyId'); + $companySettings = $request->get('companySettings'); + + $query = JobPosting::with(['jobType', 'location', 'branch', 'department']) + ->where('code', $jobCode) + ->whereIn('created_by', getCompanyUsers($companyId)) + ->where('is_published', true) + ->where('status', 'Published'); + + $jobPosting = $query->firstOrFail(); + + $relatedQuery = JobPosting::with(['jobType', 'location', 'branch', 'department']) + ->where('code', '!=', $jobCode) + ->where('is_published', true) + ->where('status', 'Published'); + + if ($companyId) { + $relatedQuery->whereIn('created_by', getCompanyUsers($companyId)); + } + + $relatedJobs = $relatedQuery->inRandomOrder()->limit(4)->get(); + + if ($companyId) { + $companyUser = User::find($companyId); + if ($companyUser) { + $companySettings = array_merge($companySettings, [ + 'company_name' => $companyUser->name, + 'company_email' => $companyUser->email, + ]); + } + } + + return Inertia::render('career/job-details', [ + 'jobPosting' => $jobPosting, + 'relatedJobs' => $relatedJobs, + 'companyId' => $companyId, + 'userSlug' => $userSlug, + 'companySettings' => $companySettings, + ]); + } catch (\Exception $e) { + return redirect()->route('career.index', $userSlug) + ->with('error', 'Job not found or no longer available.'); + } + } + + public function showApplicationForm(Request $request, $userSlug, $jobCode) + { + try { + // Get company data from middleware + $companyId = $request->get('companyId'); + $companySettings = $request->get('companySettings'); + + $jobPosting = JobPosting::with(['jobType', 'location', 'branch', 'department']) + ->where('code', $jobCode) + ->whereIn('created_by', getCompanyUsers($companyId)) + ->where('is_published', true) + ->where('status', 'Published') + ->firstOrFail(); + + // Get custom questions based on IDs stored in job posting + $customQuestions = []; + if ($jobPosting->custom_question && is_array($jobPosting->custom_question)) { + $customQuestions = \App\Models\CustomQuestion::whereIn('id', $jobPosting->custom_question) + ->get(); + } + + // Get candidate sources + $candidateSources = CandidateSource::where('status', 'active') + ->whereIn('created_by', getCompanyUsers($companyId)) + ->get(); + + return Inertia::render('career/apply', [ + 'jobPosting' => $jobPosting, + 'customQuestions' => $customQuestions, + 'candidateSources' => $candidateSources, + 'applicantFields' => $jobPosting->applicant ?? [], + 'visibilityFields' => $jobPosting->visibility ?? [], + 'companyId' => $companyId, + 'userSlug' => $userSlug, + 'companySettings' => $companySettings, + ]); + } catch (\Exception $e) { + return redirect()->route('career.index', $userSlug) + ->with('error', 'Job not found or no longer available.'); + } + } + + public function submitApplication(Request $request, $userSlug, $jobCode) + { + try { + // Get company data from middleware + $companyId = $request->get('companyId'); + + // Find the job posting + $jobPosting = JobPosting::where('code', $jobCode) + ->whereIn('created_by', getCompanyUsers($companyId)) + ->where('is_published', true) + ->where('status', 'Published') + ->firstOrFail(); + + // Base validation rules + $rules = [ + 'first_name' => 'required|string|max:255', + 'last_name' => 'required|string|max:255', + 'email' => 'required|email|max:255', + 'phone' => 'required|string|max:20', + 'address' => 'required|string|max:500', + 'city' => 'required|string|max:100', + 'state' => 'required|string|max:100', + 'zip_code' => 'required', + 'country' => 'required', + 'current_position' => 'required', + 'current_company' => 'required', + 'experience_years' => 'required|numeric|min:0|max:50', + 'current_salary' => 'required|numeric|min:0', + 'expected_salary' => 'required|numeric|min:0', + 'source_id' => 'required|exists:candidate_sources,id', + 'custom_question' => 'nullable|json', + 'resume' => 'required', + ]; + + // Check job posting applicant fields for conditional validation + $applicantFields = $jobPosting->applicant ?? []; + $visibilityFields = $jobPosting->visibility ?? []; + + // Add conditional validation for gender + if (in_array('gender', $applicantFields)) { + $rules['gender'] = 'required|in:male,female,other'; + } else { + $rules['gender'] = 'nullable|in:male,female,other'; + } + + // Add conditional validation for date_of_birth + if (in_array('date_of_birth', $applicantFields)) { + $rules['date_of_birth'] = 'required|date'; + } else { + $rules['date_of_birth'] = 'nullable|date'; + } + + // Add conditional validation for cover letter fields + if (in_array('cover_letter', $visibilityFields)) { + $rules['coverletter_message'] = 'required|string|max:2000'; + $rules['cover_letter_file'] = 'required'; + } else { + $rules['coverletter_message'] = 'nullable|string|max:2000'; + $rules['cover_letter_file'] = 'nullable|file'; + } + + // Add conditional validation for terms and conditions + if (in_array('terms_and_conditions', $visibilityFields)) { + $rules['terms_condition_check'] = 'required|in:on,off,1,0'; + } else { + // If terms not required, accept any value or make it optional + $rules['terms_condition_check'] = 'sometimes|in:on,off,1,0'; + } + + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return redirect()->back() + ->withErrors($validator) + ->withInput(); + } + + // Check if candidate already applied for this job + $existingCandidate = Candidate::where('email', $request->email) + ->where('job_id', $jobPosting->id) + ->first(); + + if ($existingCandidate) { + return redirect()->back() + ->withErrors(['email' => 'You have already applied for this position.']) + ->withInput(); + } + + // Handle file uploads + $resumePath = null; + $coverLetterPath = null; + + if (!empty($request->resume) && $request->hasFile('resume')) { + $filenameWithExt = $request->file('resume')->getClientOriginalName(); + $filename = pathinfo($filenameWithExt, PATHINFO_FILENAME); + $extension = $request->file('resume')->getClientOriginalExtension(); + $fileNameToStore = $filename . '_' . time() . '.' . $extension; + + $upload = upload_file($request, 'resume', $fileNameToStore, 'candidates/candidate_resumes'); + if ($upload['status'] == true) { + $resumePath = $upload['url']; + } else { + return redirect()->back() + ->withErrors(['resume' => $upload['msg']]) + ->withInput(); + } + } + + if (!empty($request->cover_letter_file) && $request->hasFile('cover_letter_file')) { + $filenameWithExt = $request->file('cover_letter_file')->getClientOriginalName(); + $filename = pathinfo($filenameWithExt, PATHINFO_FILENAME); + $extension = $request->file('cover_letter_file')->getClientOriginalExtension(); + $fileNameToStore = $filename . '_' . time() . '.' . $extension; + + $upload = upload_file($request, 'cover_letter_file', $fileNameToStore, 'candidates/candidate_cover_letters'); + if ($upload['status'] == true) { + $coverLetterPath = $upload['url']; + } else { + return redirect()->back() + ->withErrors(['cover_letter_file' => $upload['msg']]) + ->withInput(); + } + } + + // Convert terms_condition_check from 1/0 to on/off + if ($request->has('terms_condition_check')) { + $termsValue = $request->terms_condition_check; + $request->merge([ + 'terms_condition_check' => ($termsValue == '1' || $termsValue === true) ? 'on' : 'off', + ]); + } + + // Get custom questions for processing answers + $customQuestions = []; + if ($jobPosting->custom_question && is_array($jobPosting->custom_question)) { + $customQuestions = \App\Models\CustomQuestion::whereIn('id', $jobPosting->custom_question) + ->get(); + } + + // Process custom questions into question-answer format + $customQuestionData = []; + if ($customQuestions && count($customQuestions) > 0) { + foreach ($customQuestions as $question) { + $fieldName = 'custom_question_' . $question->id; + if ($request->has($fieldName) && !empty($request->input($fieldName))) { + $customQuestionData[$question->question] = $request->input($fieldName); + } + } + } + + // Create candidate record + $candidate = new Candidate; + $candidate->job_id = $jobPosting->id; + $candidate->source_id = $request->source_id; + $candidate->branch_id = $jobPosting->branch_id; + $candidate->department_id = $jobPosting->department_id; + $candidate->first_name = $request->first_name; + $candidate->last_name = $request->last_name; + $candidate->email = $request->email; + $candidate->phone = $request->phone; + $candidate->gender = $request->gender; + $candidate->date_of_birth = $request->date_of_birth; + $candidate->address = $request->address; + $candidate->city = $request->city; + $candidate->state = $request->state; + $candidate->zip_code = $request->zip_code; + $candidate->country = $request->country; + $candidate->current_company = $request->current_company; + $candidate->current_position = $request->current_position; + $candidate->experience_years = $request->experience_years ?: 0; + $candidate->current_salary = $request->current_salary ? str_replace(',', '', $request->current_salary) : null; + $candidate->expected_salary = $request->expected_salary ? str_replace(',', '', $request->expected_salary) : null; + $candidate->resume_path = $resumePath; + $candidate->cover_letter_path = $coverLetterPath; + $candidate->coverletter_message = $request->coverletter_message; + $candidate->custom_question = $customQuestionData; + $candidate->terms_condition_check = $request->terms_condition_check; + $candidate->application_date = now()->toDateString(); + $candidate->created_by = $companyId; + + $candidate->save(); + + return redirect()->back() + ->with('success', 'Your application has been submitted successfully! We will review it and get back to you soon.'); + + } catch (\Exception $e) { + // Clean up uploaded files if candidate creation fails + if (isset($resumePath) && Storage::disk('public')->exists($resumePath)) { + Storage::disk('public')->delete($resumePath); + } + if (isset($coverLetterPath) && Storage::disk('public')->exists($coverLetterPath)) { + Storage::disk('public')->delete($coverLetterPath); + } + + return redirect()->back() + ->withErrors(['error' => 'An error occurred while submitting your application. Please try again.']) + ->withInput(); + } + } +} diff --git a/app/Http/Controllers/CashfreeController.php b/app/Http/Controllers/CashfreeController.php new file mode 100644 index 000000000..56e818023 --- /dev/null +++ b/app/Http/Controllers/CashfreeController.php @@ -0,0 +1,238 @@ + $settings['payment_settings']['cashfree_public_key'] ?? null, + 'secret_key' => $settings['payment_settings']['cashfree_secret_key'] ?? null, + 'mode' => $mode, + 'base_url' => $baseUrl, + 'currency' => $settings['general_settings']['defaultCurrency'] ?? 'INR' + ]; + } + + /** + * Create a Cashfree payment session + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function createPaymentSession(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + + // Get Cashfree credentials + $credentials = $this->getCashfreeCredentials(); + + if (!$credentials['app_id'] || !$credentials['secret_key']) { + throw new \Exception(__('Cashfree API credentials not found')); + } + $orderId = 'plan_' . $plan->id . '_' . time() . '_' . uniqid(); + + // Configure Cashfree SDK + $cashfree = new Cashfree( + $credentials['mode'] === 'production' ? 1 : 0, + $credentials['app_id'], + $credentials['secret_key'], + '', + '', + '', + false + ); + + // Create customer details + $customerDetails = new CustomerDetails(); + $customerDetails->setCustomerId('user_' . auth()->id()); + $customerDetails->setCustomerName(auth()->user()->name ?? 'Customer'); + $customerDetails->setCustomerEmail(auth()->user()->email ?? 'customer@example.com'); + $customerDetails->setCustomerPhone(auth()->user()->phone ?? '9999999999'); + + // Create order meta + $orderMeta = new OrderMeta(); + $orderMeta->setReturnUrl(route('dashboard')); + $orderMeta->setNotifyUrl(route('cashfree.webhook')); + + // Create order request + $orderRequest = new CreateOrderRequest(); + $orderRequest->setOrderId($orderId); + $orderRequest->setOrderAmount($pricing['final_price']); + $orderRequest->setOrderCurrency($credentials['currency']); + $orderRequest->setCustomerDetails($customerDetails); + $orderRequest->setOrderMeta($orderMeta); + $orderRequest->setOrderNote('Plan Subscription - ' . $plan->name); + $orderRequest->setOrderTags([ + 'plan_id' => (string)$plan->id, + 'billing_cycle' => $request->billing_cycle, + 'user_id' => (string)auth()->id() + ]); + + $apiResponse = $cashfree->PGCreateOrder($orderRequest); + + return response()->json([ + 'payment_session_id' => $apiResponse[0]->getPaymentSessionId(), + 'order_id' => $orderId, + 'amount' => $pricing['final_price'], + 'currency' => $credentials['currency'], + 'mode' => $credentials['mode'] + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Failed to create payment session: ') . $e->getMessage()], 500); + } + } + + /** + * Verify Cashfree payment + * + * @param \Illuminate\\Http\\Request $request + * @return \Illuminate\\Http\\JsonResponse + */ + public function verifyPayment(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'order_id' => 'required|string', + 'cf_payment_id' => 'nullable|string' + ]); + + try { + $credentials = $this->getCashfreeCredentials(); + + if (!$credentials['app_id'] || !$credentials['secret_key']) { + throw new \Exception(__('Cashfree API credentials not found')); + } + + // Configure Cashfree SDK + $cashfree = new Cashfree( + $credentials['mode'] === 'production' ? 1 : 0, + $credentials['app_id'], + $credentials['secret_key'], + '', + '', + '', + false + ); + $orderResponse = $cashfree->PGFetchOrder($validated['order_id']); + + if ($orderResponse[0]->getOrderStatus() !== 'PAID') { + throw new \Exception(__('Payment not completed successfully')); + } + + // Get payment details - response is array with payment objects + $paymentsResponse = $cashfree->PGOrderFetchPayments($validated['order_id']); + // Response structure: [payments_array, status_code, headers] + if (is_array($paymentsResponse) && isset($paymentsResponse[0]) && is_array($paymentsResponse[0])) { + $payments = $paymentsResponse[0]; // Direct array of payment objects + } else { + throw new \Exception(__('Invalid payment response structure')); + } + + $successfulPayment = null; + foreach ($payments as $payment) { + // Payment is already an object from the SDK + if ($payment->getPaymentStatus() === 'SUCCESS') { + $successfulPayment = $payment; + break; + } + } + + if (!$successfulPayment) { + throw new \Exception(__('No successful payment found for this order')); + } + + $paymentData = [ + 'user_id' => auth()->id(), + 'plan_id' => $validated['plan_id'], + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'cashfree', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $successfulPayment->getCfPaymentId(), + ]; + + $planOrder = processPaymentSuccess($paymentData); + + return response()->json(['success' => true]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment verification failed: ') . $e->getMessage()], 500); + } + } + + /** + * Handle Cashfree webhook + * + * @param \Illuminate\\Http\\Request $request + * @return \Illuminate\\Http\\JsonResponse + */ + public function webhook(Request $request) + { + try { + $credentials = $this->getCashfreeCredentials(); + + // Verify webhook signature + $signature = $request->header('x-webhook-signature'); + $timestamp = $request->header('x-webhook-timestamp'); + $rawBody = $request->getContent(); + + $expectedSignature = base64_encode(hash_hmac('sha256', $timestamp . $rawBody, $credentials['secret_key'], true)); + + if (!hash_equals($expectedSignature, $signature)) { + return response()->json(['error' => 'Invalid signature'], 400); + } + + $data = $request->json()->all(); + + if ($data['type'] === 'PAYMENT_SUCCESS_WEBHOOK') { + $paymentData = $data['data']; + + // Extract plan and user info from order tags + $orderTags = $paymentData['order']['order_tags'] ?? []; + + if (isset($orderTags['plan_id']) && isset($orderTags['user_id'])) { + processPaymentSuccess([ + 'user_id' => $orderTags['user_id'], + 'plan_id' => $orderTags['plan_id'], + 'billing_cycle' => $orderTags['billing_cycle'] ?? 'monthly', + 'payment_method' => 'cashfree', + 'payment_id' => $paymentData['cf_payment_id'], + ]); + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Webhook processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ChatGptController.php b/app/Http/Controllers/ChatGptController.php new file mode 100644 index 000000000..ace7c79ff --- /dev/null +++ b/app/Http/Controllers/ChatGptController.php @@ -0,0 +1,111 @@ +validate([ + 'prompt' => 'required|string|max:1000', + 'language' => 'string|in:en,es,ar,da,de,fr,he,it,ja,nl,pl,pt,pt-BR,ru,tr,zh', + 'creativity' => 'string|in:low,medium,high', + 'num_results' => 'integer|min:1|max:5', + 'max_length' => 'integer|min:1|max:500' + ]); + + try { + $apiKey = Setting::where('key', 'chatgptKey')->value('value'); + $model = Setting::where('key', 'chatgptModel')->value('value') ?? 'gpt-3.5-turbo'; + + if (!$apiKey) { + return response()->json([ + 'success' => false, + 'message' => __('Please set proper configuration for Api Key') + ], 400); + } + + $temperature = (float) $request->input('creativity', 0.7); + if (is_string($request->input('creativity'))) { + $temperature = match($request->input('creativity')) { + 'low' => 0.3, + 'high' => 0.9, + default => 0.7 + }; + } + + $language = $request->input('language', 'en'); + $langText = $language !== 'en' ? "Provide response in " . match($language) { + 'es' => 'Spanish', + 'ar' => 'Arabic', + 'da' => 'Danish', + 'de' => 'German', + 'fr' => 'French', + 'he' => 'Hebrew', + 'it' => 'Italian', + 'ja' => 'Japanese', + 'nl' => 'Dutch', + 'pl' => 'Polish', + 'pt' => 'Portuguese', + 'pt-BR' => 'Brazilian Portuguese', + 'ru' => 'Russian', + 'tr' => 'Turkish', + 'zh' => 'Chinese', + default => 'English' + } . " language.\n\n " : ""; + + $maxTokens = (int) $request->input('max_length', 150); + $maxResults = (int) $request->input('num_results', 1); + + $client = OpenAI::client($apiKey); + + $response = $client->chat()->create([ + 'model' => $model, + 'messages' => [ + [ + 'role' => 'user', + 'content' => $request->prompt . ' ' . $langText + ] + ], + 'max_tokens' => $maxTokens, + 'temperature' => $temperature, + 'n' => $maxResults + ]); + + if (isset($response->choices)) { + $text = ''; + $counter = 1; + + if (count($response->choices) > 1) { + foreach ($response->choices as $choice) { + $text .= $counter . '. ' . trim($choice->message->content) . "\r\n\r\n\r\n"; + $counter++; + } + } else { + $text = $response->choices[0]->message->content; + } + + return response()->json([ + 'success' => true, + 'content' => trim($text) + ]); + } else { + return response()->json([ + 'success' => false, + 'message' => __('Text was not generated, please try again') + ], 500); + } + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => 'Error: ' . $e->getMessage() + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ChecklistItemController.php b/app/Http/Controllers/ChecklistItemController.php new file mode 100644 index 000000000..7486a26f2 --- /dev/null +++ b/app/Http/Controllers/ChecklistItemController.php @@ -0,0 +1,163 @@ +can('manage-checklist-items')) { + $query = ChecklistItem::with(['checklist'])->where(function ($q) { + if (Auth::user()->can('manage-any-checklist-items')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-checklist-items')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('task_name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('category') && !empty($request->category) && $request->category !== 'all') { + $query->where('category', $request->category); + } + + if ($request->has('checklist_id') && !empty($request->checklist_id) && $request->checklist_id !== 'all') { + $query->where('checklist_id', $request->checklist_id); + } + + if ($request->has('is_required') && $request->is_required !== 'all') { + $query->where('is_required', $request->is_required === 'true'); + } + + // Handle sorting + $sortField = $request->get('sort_field'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['task_name']; + if ($sortField && in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + $checklistItems = $query->paginate($request->per_page ?? 10); + + $checklists = OnboardingChecklist::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/recruitment/checklist-items/index', [ + 'checklistItems' => $checklistItems, + 'checklists' => $checklists, + 'filters' => $request->all(['search', 'category', 'checklist_id', 'is_required', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'checklist_id' => 'required|exists:onboarding_checklists,id', + 'task_name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'category' => 'required|in:Documentation,IT Setup,Training,HR,Facilities,Other', + 'assigned_to_role' => 'nullable|string|max:255', + 'due_day' => 'nullable|integer|min:1', + 'is_required' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + ChecklistItem::create([ + 'checklist_id' => $request->checklist_id, + 'task_name' => $request->task_name, + 'description' => $request->description, + 'category' => $request->category, + 'assigned_to_role' => $request->assigned_to_role, + 'due_day' => $request->due_day ?? 0, + 'is_required' => $request->boolean('is_required'), + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Checklist item created successfully')); + } + + public function update(Request $request, ChecklistItem $checklistItem) + { + if (!in_array($checklistItem->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this item')); + } + + $validator = Validator::make($request->all(), [ + 'checklist_id' => 'required|exists:onboarding_checklists,id', + 'task_name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'category' => 'required|in:Documentation,IT Setup,Training,HR,Facilities,Other', + 'assigned_to_role' => 'nullable|string|max:255', + 'due_day' => 'nullable|integer|min:1', + 'is_required' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $checklistItem->update([ + 'checklist_id' => $request->checklist_id, + 'task_name' => $request->task_name, + 'description' => $request->description, + 'category' => $request->category, + 'assigned_to_role' => $request->assigned_to_role, + 'due_day' => $request->due_day ?? 0, + 'is_required' => $request->boolean('is_required'), + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Checklist item updated successfully')); + } + + public function destroy(ChecklistItem $checklistItem) + { + if (!in_array($checklistItem->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this item')); + } + + $checklistItem->delete(); + return redirect()->back()->with('success', __('Checklist item deleted successfully')); + } + + public function toggleStatus(ChecklistItem $checklistItem) + { + if (!in_array($checklistItem->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this item')); + } + + $checklistItem->update([ + 'status' => $checklistItem->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Item status updated successfully')); + } +} diff --git a/app/Http/Controllers/CinetPayPaymentController.php b/app/Http/Controllers/CinetPayPaymentController.php new file mode 100644 index 000000000..e21879787 --- /dev/null +++ b/app/Http/Controllers/CinetPayPaymentController.php @@ -0,0 +1,136 @@ + 'required|string', + 'cpm_result' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['cinetpay_site_id'])) { + return back()->withErrors(['error' => __('CinetPay not configured')]); + } + + if ($validated['cpm_result'] === '00') { // Success status + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'cinetpay', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['cpm_trans_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'cinetpay'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['cinetpay_site_id'])) { + return response()->json(['error' => __('CinetPay not configured')], 400); + } + + $user = auth()->user(); + $transactionId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + + $paymentData = [ + 'cpm_site_id' => $settings['payment_settings']['cinetpay_site_id'], + 'cpm_trans_id' => $transactionId, + 'cpm_amount' => $pricing['final_price'], + 'cpm_currency' => 'XOF', // West African CFA franc + 'cpm_designation' => $plan->name, + 'cpm_custom' => json_encode([ + 'plan_id' => $plan->id, + 'user_id' => $user->id, + 'billing_cycle' => $validated['billing_cycle'], + ]), + 'cpm_page_action' => 'PAYMENT', + 'cpm_version' => 'V2', + 'cpm_language' => 'fr', + 'cpm_return_url' => route('cinetpay.success'), + 'cpm_notify_url' => route('cinetpay.callback'), + 'cpm_error_url' => route('plans.index'), + ]; + + $baseUrl = 'https://www.cinetpay.com/payment/'; + + return response()->json([ + 'success' => true, + 'payment_url' => $baseUrl, + 'payment_data' => $paymentData, + 'transaction_id' => $transactionId + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + return redirect()->route('plans.index')->with('success', __('Payment completed successfully')); + } + + public function callback(Request $request) + { + try { + $transactionId = $request->input('cpm_trans_id'); + $result = $request->input('cpm_result'); + + if ($transactionId && $result === '00') { + $parts = explode('_', $transactionId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + $customData = json_decode($request->input('cpm_custom'), true); + + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $customData['billing_cycle'] ?? 'monthly', + 'payment_method' => 'cinetpay', + 'payment_id' => $transactionId, + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/CoinGatePaymentController.php b/app/Http/Controllers/CoinGatePaymentController.php new file mode 100644 index 000000000..80af1d0f5 --- /dev/null +++ b/app/Http/Controllers/CoinGatePaymentController.php @@ -0,0 +1,129 @@ +validate([ + 'plan_id' => 'required|exists:plans,id', + 'billing_cycle' => 'required|in:monthly,yearly', + 'coupon_code' => 'nullable|string' + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $user = auth()->user(); + + // Get payment settings exactly like reference project + $settings = getPaymentGatewaySettings(); + + + if (!$settings['payment_settings']['is_coingate_enabled'] || !$settings['payment_settings']['coingate_api_token']) { + return redirect()->route('plans.index')->with('error', __('CoinGate payment is not available')); + } + + if (!isset($settings['payment_settings']['coingate_api_token']) || empty($settings['payment_settings']['coingate_api_token'])) { + return redirect()->route('plans.index')->with('error', __('CoinGate API token not configured')); + } + + // Calculate price + $price = $validated['billing_cycle'] === 'yearly' ? $plan->yearly_price : $plan->price; + + // Create plan order + $orderId = time(); + $planOrder = PlanOrder::create([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'coingate', + 'coupon_code' => $validated['coupon_code'], + 'payment_id' => $orderId, + 'original_price' => $price, + 'final_price' => $price, + 'status' => 'pending' + ]); + + // Use official CoinGate package + $client = new Client( + $settings['payment_settings']['coingate_api_token'], + ($settings['payment_settings']['coingate_mode'] ?? 'sandbox') === 'sandbox' + ); + + $orderParams = [ + 'order_id' => $orderId, + 'price_amount' => $price, + 'price_currency' => $settings['general_settings']['defaultCurrency'] ?? 'USD', + 'receive_currency' => $settings['general_settings']['defaultCurrency'] ?? 'USD', + 'callback_url' => route('coingate.callback'), + 'cancel_url' => route('plans.index'), + 'success_url' => route('coingate.callback'), + 'title' => 'Plan #' . $orderId, + ]; + + $orderResponse = $client->order->create($orderParams); + + if ($orderResponse && isset($orderResponse->payment_url)) { + // Store in session like reference project + session(['coingate_data' => $orderResponse]); + + // Store gateway response + $planOrder->payment_id = $orderResponse->order_id; + $planOrder->save(); + + return redirect($orderResponse->payment_url); + } else { + $planOrder->update(['status' => 'cancelled']); + return redirect()->route('plans.index')->with('error', __('Payment initialization failed')); + } + + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment failed: ') . $e->getMessage()); + } + } + + public function callback(Request $request) + { + try { + $user = auth()->user(); + $coingateData = session('coingate_data'); + + if (!$coingateData) { + return redirect()->route('plans.index')->with('error', __('Payment session expired')); + } + + $orderId = is_object($coingateData) ? $coingateData->order_id : $coingateData['order_id']; + $planOrder = PlanOrder::where('payment_id', $orderId)->first(); + + if (!$planOrder) { + return redirect()->route('plans.index')->with('error', 'Order not found'); + } + + // Mark as successful and activate subscription + $planOrder->update([ + 'status' => 'approved', + 'processed_at' => now() + ]); + + $planOrder->activateSubscription(); + + // Clear session + session()->forget('coingate_data'); + + return redirect()->route('plans.index')->with('success', __('Plan activated successfully!')); + + } catch (\Exception $e) { + Log::error('CoinGate callback error: ' . $e->getMessage()); + return redirect()->route('plans.index')->with('error', __('Payment processing failed')); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/CompanyController.php b/app/Http/Controllers/CompanyController.php new file mode 100644 index 000000000..7809d5202 --- /dev/null +++ b/app/Http/Controllers/CompanyController.php @@ -0,0 +1,325 @@ +where('type', 'company') + ->with('plan'); + + // Apply search filter + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', "%{$request->search}%") + ->orWhere('email', 'like', "%{$request->search}%"); + }); + } + + // Apply status filter + if ($request->has('status') && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Apply date filters + if ($request->has('start_date') && !empty($request->start_date)) { + $query->whereDate('created_at', '>=', $request->start_date); + } + + if ($request->has('end_date') && !empty($request->end_date)) { + $query->whereDate('created_at', '<=', $request->end_date); + } + + // Apply sorting + $sortField = $request->input('sort_field', 'created_at'); + $sortDirection = $request->input('sort_direction', 'desc'); + $query->orderBy($sortField, $sortDirection); + + // Get paginated results + $perPage = $request->input('per_page', 10); + $companies = $query->paginate($perPage)->withQueryString(); + + // Transform data for frontend + $companies->getCollection()->transform(function ($company) { + return [ + 'id' => $company->id, + 'avatar' => check_file($company->avatar) ? get_file($company->avatar) : get_file('avatars/avatar.png'), + 'name' => $company->name, + 'email' => $company->email, + 'status' => $company->status, + 'created_at' => $company->created_at, + 'plan_name' => $company->plan ? $company->plan->name : __('No Plan'), + 'plan_expiry_date' => $company->plan_expire_date, + ]; + }); + + // Get plans for dropdown + $plans = Plan::all(['id', 'name']); + + return Inertia::render('companies/index', [ + 'companies' => $companies, + 'plans' => $plans, + 'filters' => $request->only(['search', 'status', 'start_date', 'end_date', 'sort_field', 'sort_direction', 'per_page', 'view']), + ]); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users', + 'password' => 'nullable|string|min:8', + 'status' => 'required|in:active,inactive', + ]); + + $company = new User; + $company->name = $validated['name']; + $company->email = $validated['email']; + + // Only set password if provided + if (isset($validated['password'])) { + $company->password = Hash::make($validated['password']); + } + + $company->type = 'company'; + $company->status = $validated['status']; + $company->created_by = creatorId() ?? 1; + + // Set company language same as creator (superadmin) + $creator = auth()->user(); + $superAdminSettings = settings(); + $userLang = isset($superAdminSettings['defaultLanguage']) ? $superAdminSettings['defaultLanguage'] : $creator->lang; + $company->lang = $userLang; + + // Assign default plan + $defaultPlan = Plan::where('is_default', true)->first(); + if ($defaultPlan) { + $company->plan_id = $defaultPlan->id; + + // Set plan expiry date based on plan duration + if ($defaultPlan->duration === 'yearly') { + $company->plan_expire_date = now()->addYear(); + } else { + $company->plan_expire_date = now()->addMonth(); + } + + // Set plan is active + $company->plan_is_active = 1; + } + + $company->save(); + + // Assign role and settings to the user + defaultRoleAndSetting($company); + + // Trigger email notification + event(new \App\Events\UserCreated($company, $validated['password'] ?? '')); + + // Check for email errors + if (session()->has('email_error')) { + return redirect()->back()->with('warning', __('Company created successfully, but welcome email failed: ') . session('email_error')); + } + + return redirect()->back()->with('success', __('Company created successfully')); + } + + public function update(Request $request, User $company) + { + // Ensure this is a company type user + if ($company->type !== 'company') { + return redirect()->back()->with('error', __('Invalid company record')); + } + + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|string|email|max:255|unique:users,email,' . $company->id, + ]); + + $company->name = $validated['name']; + $company->email = $validated['email']; + + $company->save(); + + return redirect()->back()->with('success', __('Company updated successfully')); + } + + public function destroy(User $company) + { + // Ensure this is a company type user + if ($company->type !== 'company') { + return redirect()->back()->with('error', __('Invalid company record')); + } + + $company->delete(); + + return redirect()->back()->with('success', __('Company deleted successfully')); + } + + public function resetPassword(Request $request, User $company) + { + // Ensure this is a company type user + if ($company->type !== 'company') { + return redirect()->back()->with('error', __('Invalid company record')); + } + + $validated = $request->validate([ + 'password' => ['required', 'string', 'min:8'], + ]); + + $company->password = Hash::make($validated['password']); + $company->save(); + + return redirect()->back()->with('success', __('Password reset successfully')); + } + + public function toggleStatus(User $company) + { + // Ensure this is a company type user + if ($company->type !== 'company') { + return redirect()->back()->with('error', __('Invalid company record')); + } + + $company->status = $company->status === 'active' ? 'inactive' : 'active'; + $company->save(); + + return redirect()->back()->with('success', __('Company status updated successfully')); + } + + /** + * Get available plans for upgrade + */ + public function getPlans(User $company) + { + // Ensure this is a company type user + if ($company->type !== 'company') { + return response()->json(['error' => __('Invalid company record')], 400); + } + + $plans = Plan::where('is_plan_enable', 'on')->get(); + + $formattedPlans = []; + + foreach ($plans as $plan) { + // Format features using same logic as PlanController + $features = []; + if ($plan->features) { + $enabledFeatures = $plan->getEnabledFeatures(); + $featureLabels = [ + 'ai_integration' => __('AI Integration'), + 'password_protection' => __('Password Protection'), + ]; + foreach ($enabledFeatures as $feature) { + if (isset($featureLabels[$feature])) { + $features[] = $featureLabels[$feature]; + } + } + } else { + if ($plan->enable_chatgpt === 'on') { + $features[] = __('AI Integration'); + } + } + + // Monthly plan + $formattedPlans[] = [ + 'id' => $plan->id, + 'name' => $plan->name, + 'price' => $plan->price, + 'duration' => 'Monthly', + 'description' => $plan->description, + 'features' => $features, + 'max_employees' => $plan->max_employees, + 'max_users' => $plan->max_users, + 'storage_limit' => $plan->storage_limit . ' ' . __('GB'), + 'is_current' => $company->plan_id === $plan->id, + 'is_default' => $plan->is_default, + ]; + + // Yearly plan (create a separate entry) + $yearlyPrice = $plan->yearly_price ?? ($plan->price * 12 * 0.8); + $formattedPlans[] = [ + 'id' => $plan->id, + 'name' => $plan->name, + 'price' => $yearlyPrice, + 'duration' => 'Yearly', + 'description' => $plan->description, + 'features' => $features, + 'max_employees' => $plan->max_employees, + 'max_users' => $plan->max_users, + 'storage_limit' => $plan->storage_limit . ' ' . __('GB'), + 'is_current' => $company->plan_id === $plan->id, + 'is_default' => $plan->is_default, + ]; + } + + return response()->json([ + 'plans' => $formattedPlans, + 'company' => [ + 'id' => $company->id, + 'name' => $company->name, + 'current_plan_id' => $company->plan_id, + ], + ]); + } + + public function upgradePlan(Request $request, User $company) + { + // Ensure this is a company type user + if ($company->type !== 'company') { + return back()->with('error', __('Invalid company record')); + } + + $validated = $request->validate([ + 'plan_id' => 'required|exists:plans,id', + 'duration' => 'required|in:yearly,monthly', + ]); + + $plan = Plan::find($validated['plan_id']); + if (!$plan) { + return back()->with('error', __('Plan not found')); + } + + $isYearly = $validated['duration'] === 'yearly'; + + // Create plan order entry for tracking + $planOrder = new PlanOrder; + $planOrder->user_id = $company->id; + $planOrder->plan_id = $plan->id; + $planOrder->billing_cycle = $request->duration === 'yearly' ? 'yearly' : 'monthly'; + $planOrder->original_price = $request->duration === 'yearly' ? ($plan->yearly_price ?? 0) : $plan->price; + $planOrder->discount_amount = 0; + $planOrder->final_price = $planOrder->original_price; + $planOrder->payment_method = 'admin_upgrade'; + $planOrder->status = 'approved'; + $planOrder->ordered_at = now(); + $planOrder->processed_at = now(); + $planOrder->processed_by = auth()->id(); + $planOrder->notes = 'Plan upgraded by super admin'; + $planOrder->save(); + // Update company plan + $company->plan_id = $plan->id; + + // Set plan expiry date based on plan duration + if ($request->duration === 'yearly') { + $company->plan_expire_date = now()->addYear(); + } else { + $company->plan_expire_date = now()->addMonth(); + } + + // Set plan is active + $company->plan_is_active = 1; + + $company->save(); + + return back()->with('success', __('Plan upgraded successfully')); + } +} diff --git a/app/Http/Controllers/ComplaintController.php b/app/Http/Controllers/ComplaintController.php new file mode 100644 index 000000000..b7752b7a5 --- /dev/null +++ b/app/Http/Controllers/ComplaintController.php @@ -0,0 +1,523 @@ +can('manage-complaints')) { + $query = Complaint::with(['employee', 'againstEmployee', 'assignedUser'])->where(function ($q) { + if (Auth::user()->can('manage-any-complaints')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-complaints')) { + $q->where('created_by', Auth::id())->orWhere('against_employee_id', Auth::id())->orWhere('assigned_to', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('subject', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhere('complaint_type', 'like', '%' . $request->search . '%') + ->orWhereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }) + ->orWhereHas('againstEmployee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle against employee filter + if ($request->has('against_employee_id') && !empty($request->against_employee_id)) { + $query->where('against_employee_id', $request->against_employee_id); + } + + // Handle complaint type filter + if ($request->has('complaint_type') && !empty($request->complaint_type)) { + $query->where('complaint_type', $request->complaint_type); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('complaint_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('complaint_date', '<=', $request->date_to); + } + + // Handle sorting + $allowedSortFields = ['id', 'employee_id', 'against_employee_id', 'complaint_type', 'subject', 'complaint_date', 'status', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field) && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + + $complaints = $query->paginate($request->per_page ?? 10); + + $complaints->getCollection()->transform(function ($complaint) { + if ($complaint->employee) { + $rawAvatar = $complaint->employee->getRawOriginal('avatar'); + $complaint->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + if ($complaint->againstEmployee) { + $rawAvatar = $complaint->againstEmployee->getRawOriginal('avatar'); + $complaint->againstEmployee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $complaint; + }); + + // Get employees for complainant dropdown + $complainants = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? $user->id + ]; + }); + + // Get employees for against dropdown + $againstEmployees = User::emp()->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? $user->id, + 'type' => $user->type, + ]; + }); + + // Get HR personnel for assignment dropdown + $hrPersonnel = User::whereIn('created_by', getCompanyAndUsersId()) + ->whereIn('type', ['hr', 'manager', 'company']) // <-- Add this line + ->select('id', 'name', 'type') + ->get(); + + // Get complaint types for filter dropdown + $complaintTypes = Complaint::whereIn('created_by', getCompanyAndUsersId()) + ->select('complaint_type') + ->distinct() + ->pluck('complaint_type') + ->toArray(); + + return Inertia::render('hr/complaints/index', [ + 'complaints' => $complaints, + 'complainants' => $this->getFilteredEmployees(), + 'againstEmployees' => $againstEmployees, + 'hrPersonnel' => $hrPersonnel, + 'complaintTypes' => $complaintTypes, + 'filters' => $request->all(['search', 'employee_id', 'against_employee_id', 'complaint_type', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-complaints') && !Auth::user()->can('manage-any-complaints')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name', 'type') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + 'type' => $user->type, + ]; + }); + return $employees; + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-complaints')) { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'against_employee_id' => 'nullable|exists:users,id|different:employee_id', + 'complaint_type' => 'required|string|max:255', + 'subject' => 'required|string|max:255', + 'complaint_date' => 'required|date', + 'description' => 'nullable|string', + 'documents' => 'nullable|string', + 'is_anonymous' => 'nullable|boolean', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Check if against_employee belongs to current company + if ($request->against_employee_id) { + $againstEmployee = User::find($request->against_employee_id); + if (!$againstEmployee || !in_array($againstEmployee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected for complaint against')); + } + } + + $complaintData = [ + 'employee_id' => $request->employee_id, + 'against_employee_id' => $request->against_employee_id, + 'complaint_type' => $request->complaint_type, + 'subject' => $request->subject, + 'complaint_date' => $request->complaint_date, + 'description' => $request->description, + 'status' => 'submitted', + 'is_anonymous' => $request->is_anonymous ?? false, + 'created_by' => creatorId(), + ]; + + // Handle document from media library + if ($request->has('documents')) { + $complaintData['documents'] = $request->documents; + } + + Complaint::create($complaintData); + + return redirect()->back()->with('success', __('Complaint created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Complaint $complaint) + { + if (Auth::user()->can('edit-complaints')) { + // Check if complaint belongs to current company + if (!in_array($complaint->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this complaint')); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'against_employee_id' => 'nullable|exists:users,id|different:employee_id', + 'complaint_type' => 'required|string|max:255', + 'subject' => 'required|string|max:255', + 'complaint_date' => 'required|date', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:submitted,under investigation,resolved,dismissed', + 'documents' => 'nullable|string', + 'is_anonymous' => 'nullable|boolean', + 'assigned_to' => 'nullable|exists:users,id', + 'resolution_deadline' => 'nullable|date|after_or_equal:complaint_date', + 'investigation_notes' => 'nullable|string', + 'resolution_action' => 'nullable|string', + 'resolution_date' => 'nullable|date|after_or_equal:complaint_date', + 'follow_up_action' => 'nullable|string', + 'follow_up_date' => 'nullable|date|after_or_equal:resolution_date', + 'feedback' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Check if against_employee belongs to current company + if ($request->against_employee_id) { + $againstEmployee = User::find($request->against_employee_id); + if (!$againstEmployee || !in_array($againstEmployee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected for complaint against')); + } + } + + // Check if assigned user belongs to current company + if ($request->assigned_to) { + $assignedUser = User::find($request->assigned_to); + if (!$assignedUser || (!in_array($assignedUser->created_by, getCompanyAndUsersId()) && !in_array($assignedUser->id, getCompanyAndUsersId()))) { + return redirect()->back()->with('error', __('Invalid user selected for assignment')); + } + } + + $complaintData = [ + 'employee_id' => $request->employee_id, + 'against_employee_id' => $request->against_employee_id, + 'complaint_type' => $request->complaint_type, + 'subject' => $request->subject, + 'complaint_date' => $request->complaint_date, + 'description' => $request->description, + 'status' => $request->status ?? $complaint->status, + 'is_anonymous' => $request->is_anonymous ?? $complaint->is_anonymous, + 'assigned_to' => $request->assigned_to, + 'resolution_deadline' => $request->resolution_deadline, + 'investigation_notes' => $request->investigation_notes, + 'resolution_action' => $request->resolution_action, + 'resolution_date' => $request->resolution_date, + 'follow_up_action' => $request->follow_up_action, + 'follow_up_date' => $request->follow_up_date, + 'feedback' => $request->feedback, + ]; + + // Handle document from media library + if ($request->has('documents')) { + $complaintData['documents'] = $request->documents; + } + + $complaint->update($complaintData); + + return redirect()->back()->with('success', __('Complaint updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Complaint $complaint) + { + if (Auth::user()->can('delete-complaints')) { + // Check if complaint belongs to current company + if (!in_array($complaint->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this complaint')); + } + + $complaint->delete(); + + return redirect()->back()->with('success', __('Complaint deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Change the status of the complaint. + */ + public function changeStatus(Request $request, Complaint $complaint) + { + if (Auth::user()->can('edit-complaints')) { + // Check if complaint belongs to current company + if (!in_array($complaint->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this complaint')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:submitted,under investigation,resolved,dismissed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $complaint->update([ + 'status' => $request->status, + ]); + + return redirect()->back()->with('success', __('Complaint status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Assign the complaint to an HR personnel. + */ + public function assignComplaint(Request $request, Complaint $complaint) + { + if (Auth::user()->can('assign-complaints')) { + // Check if complaint belongs to current company + if (!in_array($complaint->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this complaint')); + } + + $validator = Validator::make($request->all(), [ + 'assigned_to' => 'required|exists:users,id', + 'resolution_deadline' => 'nullable|date|after_or_equal:today', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if assigned user belongs to current company + $assignedUser = User::find($request->assigned_to); + if (!$assignedUser || (!in_array($assignedUser->created_by, getCompanyAndUsersId()) && !in_array($assignedUser->id, getCompanyAndUsersId()))) { + return redirect()->back()->with('error', __('Invalid user selected for assignment')); + } + + $updateData = [ + 'assigned_to' => $request->assigned_to, + 'resolution_deadline' => $request->resolution_deadline, + ]; + + // If complaint is in submitted status, change to under investigation + if ($complaint->status === 'submitted') { + $updateData['status'] = 'under investigation'; + } + + $complaint->update($updateData); + + return redirect()->back()->with('success', __('Complaint assigned successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Resolve the complaint. + */ + public function resolveComplaint(Request $request, Complaint $complaint) + { + if (Auth::user()->can('resolve-complaints')) { + // Check if complaint belongs to current company + if (!in_array($complaint->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this complaint')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:resolved,dismissed', + 'investigation_notes' => 'required|string', + 'resolution_action' => 'required|string', + 'resolution_date' => 'required|date', + 'follow_up_action' => 'nullable|string', + 'follow_up_date' => 'nullable|date|after_or_equal:resolution_date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $complaint->update([ + 'status' => $request->status, + 'investigation_notes' => $request->investigation_notes, + 'resolution_action' => $request->resolution_action, + 'resolution_date' => $request->resolution_date, + 'follow_up_action' => $request->follow_up_action, + 'follow_up_date' => $request->follow_up_date, + ]); + + return redirect()->back()->with('success', __('Complaint resolved successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update follow-up information. + */ + public function updateFollowUp(Request $request, Complaint $complaint) + { + if (Auth::user()->can('resolve-complaints')) { + // Check if complaint belongs to current company + if (!in_array($complaint->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this complaint')); + } + + $validator = Validator::make($request->all(), [ + 'follow_up_action' => 'required|string', + 'follow_up_date' => 'required|date', + 'feedback' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $complaint->update([ + 'follow_up_action' => $request->follow_up_action, + 'follow_up_date' => $request->follow_up_date, + 'feedback' => $request->feedback, + ]); + + return redirect()->back()->with('success', __('Follow-up information updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Download document file. + */ + public function downloadDocument(Complaint $complaint) + { + if (Auth::user()->can('view-complaints')) { + // Check if complaint belongs to current company + if (!in_array($complaint->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this document')); + } + + if (!$complaint->documents) { + return redirect()->back()->with('error', __('Document file not found')); + } + + $filePath = getStorageFilePath($complaint->documents); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Document file not found')); + } + + return response()->download($filePath); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/ContactController.php b/app/Http/Controllers/ContactController.php new file mode 100644 index 000000000..2a5e7764f --- /dev/null +++ b/app/Http/Controllers/ContactController.php @@ -0,0 +1,118 @@ +can('manage-contacts')) { + $query = Contact::query(); + + // Search functionality + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%") + ->orWhere('subject', 'like', "%{$search}%") + ->orWhere('message', 'like', "%{$search}%"); + }); + } + + // Sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'email', 'subject', 'created_at']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + // Pagination + $perPage = $request->get('per_page', 10); + $contacts = $query->paginate($perPage)->withQueryString(); + + return Inertia::render('contacts/index', [ + 'contacts' => $contacts, + 'filters' => $request->only(['search', 'sort_field', 'sort_direction', 'per_page']) + ]); + + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function sendReply(Request $request, Contact $contact) + { + if (Auth::user()->can('send-reply-contacts')) { + $request->validate([ + 'subject' => 'required|string|max:255', + 'message' => 'required|string' + ]); + + try { + // Send email directly without template + $config = setEmailConfigurations(); + $fromEmail = getSetting('email_from_address') ?: config('mail.from.address'); + $fromName = getSetting('email_from_name') ?: config('mail.from.name'); + + Mail::send([], [], function ($message) use ($contact, $request, $fromEmail, $fromName) { + $message->to($contact->email, $contact->name) + ->subject($request->subject) + ->html(nl2br(e($request->message))) + ->from($fromEmail, $fromName); + }); + + // Update contact status to 'Contacted' + $contact->update(['status' => 'Contacted']); + + return redirect()->back()->with('success', 'Reply sent successfully.'); + } catch (\Exception $e) { + Log::error('Failed to send reply: ' . $e->getMessage()); + return redirect()->back()->with('error', 'Failed to send reply. Please check email settings.'); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function updateStatus(Request $request, Contact $contact) + { + if (Auth::user()->can('update-contact-status')) { + $request->validate([ + 'status' => 'required|in:New,Contacted,Qualified,Converted,Closed' + ]); + + $contact->update([ + 'status' => $request->status + ]); + + return redirect()->back()->with('success', 'Contact status updated successfully.'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy(Contact $contact) + { + if (Auth::user()->can('delete-contacts')) { + $contact->delete(); + + return redirect()->back()->with('success', 'Contact deleted successfully.'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ContractRenewalController.php b/app/Http/Controllers/ContractRenewalController.php new file mode 100644 index 000000000..08973562a --- /dev/null +++ b/app/Http/Controllers/ContractRenewalController.php @@ -0,0 +1,233 @@ +with(['contract.employee', 'requester', 'approver']); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('renewal_number', 'like', '%' . $request->search . '%') + ->orWhereHas('contract.employee', function ($eq) use ($request) { + $eq->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('contract_id') && !empty($request->contract_id) && $request->contract_id !== 'all') { + $query->where('contract_id', $request->contract_id); + } + + $query->orderBy('id', 'desc'); + $contractRenewals = $query->paginate($request->per_page ?? 10); + + $contracts = EmployeeContract::with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->whereNotNull('end_date') + ->select('id', 'contract_number', 'employee_id', 'end_date') + ->get(); + + $employees = User::whereIn('created_by', getCompanyAndUsersId()) + ->whereIn('type', ['employee', 'manager','hr']) + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/contracts/contract-renewals/index', [ + 'contractRenewals' => $contractRenewals, + 'contracts' => $contracts, + 'employees' => $employees, + 'filters' => $request->all(['search', 'status', 'contract_id', 'per_page']), + ]); + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'contract_id' => 'required|exists:employee_contracts,id', + 'new_start_date' => 'required|date', + 'new_end_date' => 'required|date|after:new_start_date', + 'new_basic_salary' => 'required|numeric|min:0', + 'new_allowances' => 'nullable|array', + 'new_benefits' => 'nullable|array', + 'new_terms_conditions' => 'nullable|string', + 'changes_summary' => 'nullable|string', + 'reason' => 'nullable|string', + 'requested_by' => 'required|exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $contract = EmployeeContract::find($request->contract_id); + + // Generate renewal number + $lastRenewal = ContractRenewal::where('contract_id', $request->contract_id) + ->orderBy('id', 'desc') + ->first(); + $nextNumber = $lastRenewal ? (intval(substr($lastRenewal->renewal_number, -2)) + 1) : 1; + $renewalNumber = 'REN-' . str_pad(creatorId(), 3, '0', STR_PAD_LEFT) . '-' . str_pad($request->contract_id, 3, '0', STR_PAD_LEFT) . '-' . str_pad($nextNumber, 2, '0', STR_PAD_LEFT); + + ContractRenewal::create([ + 'contract_id' => $request->contract_id, + 'renewal_number' => $renewalNumber, + 'current_end_date' => $contract->end_date, + 'new_start_date' => $request->new_start_date, + 'new_end_date' => $request->new_end_date, + 'new_basic_salary' => $request->new_basic_salary, + 'new_allowances' => $request->new_allowances, + 'new_benefits' => $request->new_benefits, + 'new_terms_conditions' => $request->new_terms_conditions, + 'changes_summary' => $request->changes_summary, + 'reason' => $request->reason, + 'requested_by' => $request->requested_by, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Contract renewal created successfully')); + } + + public function update(Request $request, ContractRenewal $contractRenewal) + { + if (!in_array($contractRenewal->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this renewal')); + } + + if ($contractRenewal->status !== 'Pending') { + return redirect()->back()->with('error', __('Cannot update renewal that is not pending')); + } + + $validator = Validator::make($request->all(), [ + 'contract_id' => 'required|exists:employee_contracts,id', + 'new_start_date' => 'required|date', + 'new_end_date' => 'required|date|after:new_start_date', + 'new_basic_salary' => 'required|numeric|min:0', + 'new_allowances' => 'nullable|array', + 'new_benefits' => 'nullable|array', + 'new_terms_conditions' => 'nullable|string', + 'changes_summary' => 'nullable|string', + 'reason' => 'nullable|string', + 'requested_by' => 'required|exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $contractRenewal->update([ + 'new_start_date' => $request->new_start_date, + 'new_end_date' => $request->new_end_date, + 'new_basic_salary' => $request->new_basic_salary, + 'new_allowances' => $request->new_allowances, + 'new_benefits' => $request->new_benefits, + 'new_terms_conditions' => $request->new_terms_conditions, + 'changes_summary' => $request->changes_summary, + 'reason' => $request->reason, + 'requested_by' => $request->requested_by, + ]); + + return redirect()->back()->with('success', __('Contract renewal updated successfully')); + } + + public function destroy(ContractRenewal $contractRenewal) + { + if (!in_array($contractRenewal->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this renewal')); + } + + if ($contractRenewal->status === 'Processed') { + return redirect()->back()->with('error', __('Cannot delete processed renewal')); + } + + $contractRenewal->delete(); + return redirect()->back()->with('success', __('Contract renewal deleted successfully')); + } + + public function approve(Request $request, ContractRenewal $contractRenewal) + { + if (!in_array($contractRenewal->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to approve this renewal')); + } + + $validator = Validator::make($request->all(), [ + 'approval_notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $contractRenewal->update([ + 'status' => 'Approved', + 'approved_by' => creatorId(), + 'approved_at' => now(), + 'approval_notes' => $request->approval_notes, + ]); + + return redirect()->back()->with('success', __('Renewal approved successfully')); + } + + public function reject(Request $request, ContractRenewal $contractRenewal) + { + if (!in_array($contractRenewal->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to reject this renewal')); + } + + $validator = Validator::make($request->all(), [ + 'approval_notes' => 'required|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $contractRenewal->update([ + 'status' => 'Rejected', + 'approved_by' => creatorId(), + 'approved_at' => now(), + 'approval_notes' => $request->approval_notes, + ]); + + return redirect()->back()->with('success', __('Renewal rejected successfully')); + } + + public function process(ContractRenewal $contractRenewal) + { + if (!in_array($contractRenewal->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to process this renewal')); + } + + if ($contractRenewal->status !== 'Approved') { + return redirect()->back()->with('error', __('Can only process approved renewals')); + } + + // Update the original contract + $contract = $contractRenewal->contract; + $contract->update([ + 'end_date' => $contractRenewal->new_end_date, + 'basic_salary' => $contractRenewal->new_basic_salary, + 'allowances' => $contractRenewal->new_allowances, + 'benefits' => $contractRenewal->new_benefits, + 'terms_conditions' => $contractRenewal->new_terms_conditions, + 'status' => 'Renewed', + ]); + + $contractRenewal->update(['status' => 'Processed']); + + return redirect()->back()->with('success', __('Renewal processed and contract updated successfully')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ContractTemplateController.php b/app/Http/Controllers/ContractTemplateController.php new file mode 100644 index 000000000..86166ccd8 --- /dev/null +++ b/app/Http/Controllers/ContractTemplateController.php @@ -0,0 +1,314 @@ +can('manage-contract-templates')) { + $query = ContractTemplate::with(['contractType'])->where(function ($q) { + if (Auth::user()->can('manage-any-contract-templates')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-contract-templates')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('contract_type_id') && !empty($request->contract_type_id) && $request->contract_type_id !== 'all') { + $query->where('contract_type_id', $request->contract_type_id); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('is_default') && $request->is_default !== 'all') { + $query->where('is_default', $request->is_default === 'true'); + } + + $allowedSortFields = ['id', 'name', 'status', 'is_default', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field === 'template_name' ? 'name' : $request->sort_field; + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('is_default', 'desc')->orderBy('id', 'desc'); + } + } else { + $query->orderBy('is_default', 'desc')->orderBy('id', 'desc'); + } + + $contractTemplates = $query->paginate($request->per_page ?? 10); + + $contractTypes = ContractType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/contracts/contract-templates/index', [ + 'contractTemplates' => $contractTemplates, + 'contractTypes' => $contractTypes, + 'filters' => $request->all(['search', 'contract_type_id', 'status', 'is_default', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function create() + { + if (Auth::user()->can('create-contract-templates')) { + $contractTypes = ContractType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/contracts/contract-templates/create', [ + 'contractTypes' => $contractTypes, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function show(ContractTemplate $contractTemplate) + { + if (Auth::user()->can('view-contract-templates')) { + $contractTemplate->load('contractType'); + return Inertia::render('hr/contracts/contract-templates/show', [ + 'contractTemplate' => $contractTemplate, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function edit(ContractTemplate $contractTemplate) + { + if (Auth::user()->can('edit-contract-templates')) { + $contractTypes = ContractType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/contracts/contract-templates/edit', [ + 'contractTemplate' => $contractTemplate, + 'contractTypes' => $contractTypes, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + if (Auth::user()->can('create-contract-templates')) { + $variables = null; + if ($request->filled('variables') && is_string($request->variables)) { + $variables = array_values(array_filter(array_map('trim', explode(',', $request->variables)))); + } elseif (is_array($request->variables)) { + $variables = $request->variables; + } + + $clauses = null; + if ($request->filled('clauses') && is_string($request->clauses)) { + $clauses = array_values(array_filter(array_map('trim', explode(',', $request->clauses)))); + } elseif (is_array($request->clauses)) { + $clauses = $request->clauses; + } + + $validator = Validator::make(array_merge($request->all(), [ + 'variables' => $variables, + 'clauses' => $clauses, + ]), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'contract_type_id' => 'required|exists:contract_types,id', + 'template_content' => 'required|string', + 'variables' => 'required|array', + 'clauses' => 'nullable|array', + 'is_default' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + if ($request->boolean('is_default')) { + ContractTemplate::whereIn('created_by', getCompanyAndUsersId()) + ->where('contract_type_id', $request->contract_type_id) + ->where('is_default', true) + ->update(['is_default' => false]); + } + + ContractTemplate::create([ + 'name' => $request->name, + 'description' => $request->description, + 'contract_type_id' => $request->contract_type_id, + 'template_content' => $request->template_content, + 'variables' => $variables, + 'clauses' => $clauses, + 'is_default' => $request->boolean('is_default'), + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->route('hr.contracts.contract-templates.index') + ->with('success', __('Contract template created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(Request $request, ContractTemplate $contractTemplate) + { + if (Auth::user()->can('edit-contract-templates')) { + if (!in_array($contractTemplate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this template')); + } + + $variables = null; + if ($request->filled('variables') && is_string($request->variables)) { + $variables = array_values(array_filter(array_map('trim', explode(',', $request->variables)))); + } elseif (is_array($request->variables)) { + $variables = $request->variables; + } + + $clauses = null; + if ($request->filled('clauses') && is_string($request->clauses)) { + $clauses = array_values(array_filter(array_map('trim', explode(',', $request->clauses)))); + } elseif (is_array($request->clauses)) { + $clauses = $request->clauses; + } + + $validator = Validator::make(array_merge($request->all(), [ + 'variables' => $variables, + 'clauses' => $clauses, + ]), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'contract_type_id' => 'required|exists:contract_types,id', + 'template_content' => 'required|string', + 'variables' => 'required|array', + 'clauses' => 'nullable|array', + 'is_default' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + if ($request->boolean('is_default') && !$contractTemplate->is_default) { + ContractTemplate::whereIn('created_by', getCompanyAndUsersId()) + ->where('contract_type_id', $request->contract_type_id) + ->where('is_default', true) + ->update(['is_default' => false]); + } + + $contractTemplate->update([ + 'name' => $request->name, + 'description' => $request->description, + 'contract_type_id' => $request->contract_type_id, + 'template_content' => $request->template_content, + 'variables' => $variables, + 'clauses' => $clauses, + 'is_default' => $request->boolean('is_default'), + 'status' => $request->status ?? 'active', + ]); + + return redirect()->route('hr.contracts.contract-templates.index') + ->with('success', __('Contract template updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy(ContractTemplate $contractTemplate) + { + if (Auth::user()->can('delete-contract-templates')) { + if (!in_array($contractTemplate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this template')); + } + try { + $contractTemplate->delete(); + return redirect()->back()->with('success', __('Contract template deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete contract template')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function toggleStatus(ContractTemplate $contractTemplate) + { + if (Auth::user()->can('edit-contract-templates')) { + if (!in_array($contractTemplate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this template')); + } + try { + $contractTemplate->update([ + 'status' => $contractTemplate->status === 'active' ? 'inactive' : 'active', + ]); + return redirect()->back()->with('success', __('Template status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update template status')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function preview(Request $request, ContractTemplate $contractTemplate) + { + if (!in_array($contractTemplate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to preview this template')); + } + + $variables = $request->get('variables', []); + $generatedContent = $contractTemplate->generateContract($variables); + + return response()->json([ + 'content' => $generatedContent, + 'variables' => $contractTemplate->variables, + ]); + } + + public function generate(Request $request, ContractTemplate $contractTemplate) + { + + $variables = $request->variables ?? []; + + if (!is_array($variables)) { + $variables = []; + } + + $generatedContent = $contractTemplate->generateContract($variables); + $filename = $request->filename ?? ($contractTemplate->name . '_' . date('Y-m-d')); + + $html = '
' . nl2br($generatedContent) . '
'; + $pdf = Pdf::loadHTML($html); + return $pdf->download($filename . '.pdf'); + } +} diff --git a/app/Http/Controllers/ContractTypeController.php b/app/Http/Controllers/ContractTypeController.php new file mode 100644 index 000000000..dd01b09ff --- /dev/null +++ b/app/Http/Controllers/ContractTypeController.php @@ -0,0 +1,150 @@ +can('manage-contract-types')) { + $query = ContractType::withCount('contracts')->where(function ($q) { + if (Auth::user()->can('manage-any-contract-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-contract-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('is_renewable') && $request->is_renewable !== 'all') { + $query->where('is_renewable', $request->is_renewable === 'true'); + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'status', 'default_duration_months', 'probation_period_months', 'notice_period_days', 'is_renewable', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field) && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + + $contractTypes = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/contracts/contract-types/index', [ + 'contractTypes' => $contractTypes, + 'filters' => $request->all(['search', 'status', 'is_renewable', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'default_duration_months' => 'nullable|integer|min:1|max:120', + 'probation_period_months' => 'required|integer|min:0|max:12', + 'notice_period_days' => 'required|integer|min:0|max:365', + 'is_renewable' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + ContractType::create([ + 'name' => $request->name, + 'description' => $request->description, + 'default_duration_months' => $request->default_duration_months, + 'probation_period_months' => $request->probation_period_months, + 'notice_period_days' => $request->notice_period_days, + 'is_renewable' => $request->boolean('is_renewable'), + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Contract type created successfully')); + } + + public function update(Request $request, ContractType $contractType) + { + if (!in_array($contractType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this contract type')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'default_duration_months' => 'nullable|integer|min:1|max:120', + 'probation_period_months' => 'required|integer|min:0|max:12', + 'notice_period_days' => 'required|integer|min:0|max:365', + 'is_renewable' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $contractType->update([ + 'name' => $request->name, + 'description' => $request->description, + 'default_duration_months' => $request->default_duration_months, + 'probation_period_months' => $request->probation_period_months, + 'notice_period_days' => $request->notice_period_days, + 'is_renewable' => $request->boolean('is_renewable'), + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Contract type updated successfully')); + } + + public function destroy(ContractType $contractType) + { + if (!in_array($contractType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this contract type')); + } + + if ($contractType->contracts()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete contract type as it is being used in contracts')); + } + + $contractType->delete(); + return redirect()->back()->with('success', __('Contract type deleted successfully')); + } + + public function toggleStatus(ContractType $contractType) + { + if (!in_array($contractType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this contract type')); + } + + $contractType->update([ + 'status' => $contractType->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Contract type status updated successfully')); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 000000000..8677cd5ca --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,8 @@ +all(); + $csvFile = storage_path('app/cookie-consents.csv'); + + // Create headers if file doesn't exist + if (!file_exists($csvFile)) { + $headers = array_keys($data); + file_put_contents($csvFile, implode(',', $headers) . "\n"); + } + + // Append data + $values = array_map(function($value) { + return is_string($value) ? '"' . str_replace('"', '""', $value) . '"' : $value; + }, array_values($data)); + + file_put_contents($csvFile, implode(',', $values) . "\n", FILE_APPEND); + + return response()->json(['success' => true]); + } + + public function download() + { + $csvFile = storage_path('app/cookie-consents.csv'); + + if (!file_exists($csvFile)) { + abort(404, 'No cookie consent data found'); + } + + return response()->download($csvFile, 'cookie-consents-' . date('Y-m-d') . '.csv'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/CouponController.php b/app/Http/Controllers/CouponController.php new file mode 100644 index 000000000..2bf28e961 --- /dev/null +++ b/app/Http/Controllers/CouponController.php @@ -0,0 +1,241 @@ +has('search') && !empty($request->search)) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('code', 'like', "%{$search}%"); + }); + } + + // Handle type filter + if ($request->has('type') && !empty($request->type) && $request->type !== 'all') { + $query->where('type', $request->type); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('created_at', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('created_at', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['id', 'name', 'code', 'type', 'expiry_date', 'created_at']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $perPage = $request->get('per_page', 10); + $coupons = $query->paginate($perPage); + + return Inertia::render('coupons/index', [ + 'coupons' => $coupons, + 'filters' => $request->all(['search', 'type', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']) + ]); + } + + /** + * Display the specified resource. + */ + public function show(Request $request, Coupon $coupon) + { + $coupon->load('creator'); + + // Get usage history (mock data for now - you'll need to implement actual usage tracking) + $usageHistory = collect([ + // Mock usage data - replace with actual usage model query + [ + 'id' => 1, + 'user_name' => 'John Doe', + 'user_email' => 'john@example.com', + 'order_id' => 'ORD-001', + 'amount' => 100.00, + 'discount_amount' => 10.00, + 'used_at' => now()->subDays(2)->toISOString() + ], + [ + 'id' => 2, + 'user_name' => 'Jane Smith', + 'user_email' => 'jane@example.com', + 'order_id' => 'ORD-002', + 'amount' => 150.00, + 'discount_amount' => 15.00, + 'used_at' => now()->subDays(1)->toISOString() + ] + ]); + + // Paginate the usage history + $perPage = $request->get('per_page', 10); + $page = $request->get('page', 1); + $total = $usageHistory->count(); + $items = $usageHistory->forPage($page, $perPage)->values(); + + $paginatedUsage = new \Illuminate\Pagination\LengthAwarePaginator( + $items, + $total, + $perPage, + $page, + ['path' => $request->url(), 'pageName' => 'page'] + ); + + // Add used_count to coupon (mock for now) + $coupon->used_count = $usageHistory->count(); + + return Inertia::render('coupons/show', [ + 'coupon' => $coupon, + 'usage_history' => $paginatedUsage + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(CouponRequest $request) + { + + $data = $request->all(); + $data['created_by'] = Auth::id(); + + // Generate code if auto-generate is selected + if ($request->code_type === 'auto') { + do { + $data['code'] = strtoupper(Str::random(8)); + } while (Coupon::where('code', $data['code'])->exists()); + } + + $coupon = Coupon::create($data); + + return redirect()->route('coupons.index')->with('success', __('Coupon created successfully!')); + } + + /** + * Update the specified resource in storage. + */ + public function update(CouponRequest $request, Coupon $coupon) + { + + $data = $request->all(); + + // Generate new code if switching to auto-generate + if ($request->code_type === 'auto' && $coupon->code_type !== 'auto') { + do { + $data['code'] = strtoupper(Str::random(8)); + } while (Coupon::where('code', $data['code'])->where('id', '!=', $coupon->id)->exists()); + } + + $coupon->update($data); + + return redirect()->route('coupons.index')->with('success', __('Coupon updated successfully!')); + } + + /** + * Validate coupon code + */ + public function validate(Request $request) + { + $request->validate([ + 'coupon_code' => 'required|string', + 'plan_id' => 'required|integer', + 'amount' => 'required|numeric|min:0' + ]); + + $coupon = Coupon::where('code', $request->coupon_code) + ->where('status', 1) + ->first(); + + if (!$coupon) { + return response()->json([ + 'valid' => false, + 'message' => __('Invalid or inactive coupon code') + ], 400); + } + + // Check if coupon is expired + if ($coupon->expiry_date && $coupon->expiry_date < now()) { + return response()->json([ + 'valid' => false, + 'message' => __('Coupon has expired') + ], 400); + } + + // Check usage limit + if ($coupon->use_limit_per_coupon && $coupon->used_count >= $coupon->use_limit_per_coupon) { + return response()->json([ + 'valid' => false, + 'message' => __('Coupon usage limit exceeded') + ], 400); + } + + // Check minimum amount + if ($coupon->minimum_spend && $request->amount < $coupon->minimum_spend) { + return response()->json([ + 'valid' => false, + 'message' => __('Minimum spend requirement not met') + ], 400); + } + + return response()->json([ + 'valid' => true, + 'coupon' => [ + 'id' => $coupon->id, + 'code' => $coupon->code, + 'type' => $coupon->type, + 'value' => $coupon->discount_amount + ] + ]); + } + + /** + * Toggle the status of the specified coupon. + */ + public function toggleStatus(Coupon $coupon) + { + $coupon->update([ + 'status' => !$coupon->status + ]); + + return redirect()->back()->with('success', __('Coupon status updated successfully!')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Coupon $coupon) + { + $coupon->delete(); + + return redirect()->route('coupons.index')->with('success', __('Coupon deleted successfully!')); + } +} diff --git a/app/Http/Controllers/CurrencyController.php b/app/Http/Controllers/CurrencyController.php new file mode 100644 index 000000000..86976a3ea --- /dev/null +++ b/app/Http/Controllers/CurrencyController.php @@ -0,0 +1,114 @@ +has('search')) { + $searchTerm = $request->search; + $query->where(function($q) use ($searchTerm) { + $q->where('name', 'like', "%{$searchTerm}%") + ->orWhere('code', 'like', "%{$searchTerm}%") + ->orWhere('symbol', 'like', "%{$searchTerm}%"); + }); + } + + // Handle sorting + $sortField = $request->input('sort_field', 'created_at'); + $sortDirection = $request->input('sort_direction', 'desc'); + $query->orderBy($sortField, $sortDirection); + + // Pagination + $perPage = $request->input('per_page', 10); + $currencies = $query->paginate($perPage)->withQueryString(); + + return Inertia::render('currencies/index', [ + 'currencies' => $currencies, + 'filters' => $request->all(['search', 'sort_field', 'sort_direction', 'per_page']), + ]); + } + + /** + * Store a newly created currency. + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'code' => 'required|string|max:10|unique:currencies', + 'symbol' => 'required|string|max:10', + 'description' => 'nullable|string', + 'is_default' => 'boolean', + ]); + + // If this is set as default, unset all other defaults + if ($request->input('is_default')) { + Currency::where('is_default', true)->update(['is_default' => false]); + } + + Currency::create($validated); + + return redirect()->back()->with('success', __('Currency created successfully')); + } + + /** + * Update the specified currency. + */ + public function update(Request $request, Currency $currency) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'code' => 'required|string|max:10|unique:currencies,code,' . $currency->id, + 'symbol' => 'required|string|max:10', + 'description' => 'nullable|string', + 'is_default' => 'boolean', + ]); + + // If this is set as default, unset all other defaults + if ($request->input('is_default')) { + Currency::where('id', '!=', $currency->id) + ->where('is_default', true) + ->update(['is_default' => false]); + } + + $currency->update($validated); + + return redirect()->back()->with('success', __('Currency updated successfully')); + } + + /** + * Remove the specified currency. + */ + public function destroy(Currency $currency) + { + // Don't allow deleting the default currency + if ($currency->is_default) { + return redirect()->back()->with('error', __('Cannot delete the default currency.')); + } + + $currency->delete(); + + return redirect()->back()->with('success', __('Currency deleted successfully')); + } + + /** + * Get all currencies for settings page. + */ + public function getAllCurrencies() + { + $currencies = Currency::all(); + return response()->json($currencies); + } +} diff --git a/app/Http/Controllers/CustomQuestionController.php b/app/Http/Controllers/CustomQuestionController.php new file mode 100644 index 000000000..d21d40d38 --- /dev/null +++ b/app/Http/Controllers/CustomQuestionController.php @@ -0,0 +1,144 @@ +can('manage-custom-questions')) { + $query = CustomQuestion::where(function ($q) { + if (Auth::user()->can('manage-any-custom-questions')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-custom-questions')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where('question', 'like', '%'.$request->search.'%'); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'id'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['question', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'id'; + } + + $query->orderBy($sortField, $sortDirection); + + $customQuestions = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/custom-questions/index', [ + 'customQuestions' => $customQuestions, + 'filters' => $request->all(['search', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + if (Auth::user()->can('create-custom-questions')) { + try { + $validated = $request->validate([ + 'question' => 'required|string', + 'required' => 'required|integer|in:0,1', + ]); + + $validated['created_by'] = creatorId(); + + // Check if question already exists + $exists = CustomQuestion::where('question', $validated['question']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Custom question with this text already exists.')); + } + + CustomQuestion::create($validated); + + return redirect()->back()->with('success', __('Custom question created successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to create custom question')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(Request $request, $customQuestionId) + { + if (Auth::user()->can('edit-custom-questions')) { + $customQuestion = CustomQuestion::where('id', $customQuestionId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($customQuestion) { + try { + $validated = $request->validate([ + 'question' => 'required|string', + 'required' => 'required|integer|in:0,1', + ]); + + // Check if question already exists (excluding current question) + $exists = CustomQuestion::where('question', $validated['question']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('id', '!=', $customQuestionId) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Custom question with this text already exists.')); + } + + $customQuestion->update($validated); + + return redirect()->back()->with('success', __('Custom question updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update custom question')); + } + } else { + return redirect()->back()->with('error', __('Custom question not found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy($customQuestionId) + { + if (Auth::user()->can('delete-custom-questions')) { + $customQuestion = CustomQuestion::where('id', $customQuestionId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($customQuestion) { + try { + $customQuestion->delete(); + + return redirect()->back()->with('success', __('Custom question deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete custom question')); + } + } else { + return redirect()->back()->with('error', __('Custom question not found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 000000000..3984dd4bd --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,549 @@ +user(); + + // Super admin always gets dashboard + if ($user->type === 'superadmin' || $user->type === 'super admin') { + return $this->renderDashboard(); + } + + // Check if user has dashboard permission (skip if permission doesn't exist) + try { + if ($user->hasPermissionTo('manage-dashboard')) { + return $this->renderDashboard(); + } + } catch (\Exception $e) { + // Permission doesn't exist, continue to dashboard for authenticated users + return $this->renderDashboard(); + } + + // Redirect to first available page + return $this->redirectToFirstAvailablePage(); + } + + public function redirectToFirstAvailablePage() + { + $user = auth()->user(); + + // Define available routes with their permissions + $routes = [ + ['route' => 'users.index', 'permission' => 'manage-users'], + ['route' => 'roles.index', 'permission' => 'manage-roles'], + + ['route' => 'plans.index', 'permission' => 'manage-plans'], + ['route' => 'referral.index', 'permission' => 'manage-referral'], + ['route' => 'settings.index', 'permission' => 'manage-settings'], + ]; + + // Find first available route + foreach ($routes as $routeData) { + if ($user->hasPermissionTo($routeData['permission'])) { + return redirect()->route($routeData['route']); + } + } + + // If no permissions found, logout user + auth()->logout(); + + return redirect()->route('login')->with('error', __('No access permissions found.')); + } + + private function renderDashboard() + { + $user = auth()->user(); + + if ($user->type === 'superadmin' || $user->type === 'super admin') { + return $this->renderSuperAdminDashboard(); + } else { + return $this->renderCompanyDashboard(); + } + } + + private function renderSuperAdminDashboard() + { + // Get system-wide statistics + $totalCompanies = User::where('type', 'company')->count(); + $totalUsers = User::where('type', '!=', 'superadmin')->where('type', '!=', 'super admin')->count(); + $totalRevenue = PlanOrder::where('status', 'approved')->sum('final_price') ?? 0; + $activePlans = Plan::where('is_plan_enable', 'on')->count(); + + $pendingRequests = PlanRequest::where('status', 'pending')->count(); + $activeCoupons = Coupon::where('status', true)->count(); + + // Calculate monthly growth for companies + $currentMonthCompanies = User::where('type', 'company') + ->whereMonth('created_at', now()->month) + ->whereYear('created_at', now()->year) + ->count(); + $previousMonthCompanies = User::where('type', 'company') + ->whereMonth('created_at', now()->subMonth()->month) + ->whereYear('created_at', now()->subMonth()->year) + ->count(); + $monthlyGrowth = isDemo() ? 90 : ($previousMonthCompanies > 0 + ? round((($currentMonthCompanies - $previousMonthCompanies) / $previousMonthCompanies) * 100, 1) + : ($currentMonthCompanies > 0 ? 100 : 0)); + + $dashboardData = [ + 'stats' => [ + 'totalCompanies' => $totalCompanies, + 'totalUsers' => $totalUsers, + 'totalRevenue' => $totalRevenue, + 'activePlans' => $activePlans, + 'pendingRequests' => $pendingRequests, + 'monthlyGrowth' => $monthlyGrowth, + 'activeCoupons' => $activeCoupons, + ], + 'recentActivity' => User::where('type', 'company') + ->orderBy('created_at', 'desc') + ->take(5) + ->get(['id', 'name', 'email', 'created_at']) + ->map(function ($company) { + return [ + 'id' => $company->id, + 'name' => $company->name, + 'email' => $company->email, + 'registered_at' => $company->created_at->diffForHumans(), + 'status' => 'active', + ]; + }), + 'topPlans' => Plan::withCount('users') + ->orderBy('users_count', 'desc') + ->take(5) + ->get() + ->map(function ($plan) { + return [ + 'name' => $plan->name, + 'subscribers' => $plan->users_count, + 'revenue' => $plan->users_count * $plan->price, + ]; + }), + ]; + + return Inertia::render('superadmin/dashboard', props: [ + 'dashboardData' => $dashboardData, + ]); + } + + private function renderCompanyDashboard() + { + $user = auth()->user(); + + // If user is employee, show limited dashboard + if ($user->type === 'employee') { + return $this->renderEmployeeDashboard(); + } + + $companyUserIds = $this->getCompanyUserIds(); + + // Core HR Statistics + $totalEmployees = User::where('type', 'employee')->whereIn('created_by', $companyUserIds)->count(); + $totalBranches = Branch::whereIn('created_by', $companyUserIds)->count(); + $totalDepartments = Department::whereIn('created_by', $companyUserIds)->count(); + + // Monthly Statistics + if (isDemo()) { + $newEmployeesThisMonth = Employee::whereIn('created_by', $companyUserIds)->count(); + $jobPostsThisMonth = JobPosting::whereIn('created_by', $companyUserIds)->count(); + $candidatesThisMonth = Candidate::whereIn('created_by', $companyUserIds)->count(); + } else { + $newEmployeesThisMonth = Employee::whereIn('created_by', $companyUserIds) + ->whereMonth('created_at', now()->month)->count(); + $jobPostsThisMonth = JobPosting::whereIn('created_by', $companyUserIds) + ->whereMonth('created_at', now()->month)->count(); + $candidatesThisMonth = Candidate::whereIn('created_by', $companyUserIds) + ->whereMonth('created_at', now()->month)->count(); + } + + // Attendance Statistics + if (isDemo()) { + $presentToday = 45; + $attendanceRate = 85.5; + } else { + $presentToday = AttendanceRecord::whereIn('created_by', $companyUserIds) + ->whereDate('date', today())->where('status', 'present')->count(); + $attendanceRate = $totalEmployees > 0 ? round(($presentToday / $totalEmployees) * 100, 1) : 0; + } + + // Leave Statistics + $pendingLeaves = LeaveApplication::whereIn('created_by', $companyUserIds) + ->where('status', 'pending')->count(); + + $onLeaveToday = LeaveApplication::whereIn('created_by', $companyUserIds) + ->where('status', 'approved'); + + $onLeaveToday = $onLeaveToday->whereDate('start_date', '<=', today()) + ->whereDate('end_date', '>=', today())->count(); + + // Recruitment Statistics + $activeJobPostings = JobPosting::whereIn('created_by', $companyUserIds) + ->where('status', 'Published')->count(); + $totalCandidates = Candidate::whereIn('created_by', $companyUserIds)->count(); + + // Department Distribution for Chart + // $predefinedColors = ['#4F46E5', '#10b77f', '#F59E0B', '#EF4444', '#3B82F6', '#D946EF']; + $predefinedColors = ['#0EA5E9', '#14B8A6', '#6366F1', '#0D9488', '#7C3AED', '#0369A1']; + + $departmentStats = Department::whereIn('created_by', $companyUserIds) + ->withCount('employees') + ->with('branch') + ->orderBy('employees_count', 'desc') + ->when(config('app.is_demo') == true, function ($query) { + return $query->take(6); + }) + ->get() + ->map(function ($dept, $index) use ($predefinedColors) { + $displayName = $dept->name . ' (' . $dept->branch->name . ')'; + + return [ + 'name' => $displayName, + 'value' => $dept->employees_count, + 'color' => config('app.is_demo') == true + ? ($predefinedColors[$index] ?? '#' . substr(md5($displayName), 0, 6)) + : '#' . substr(md5($displayName), 0, 6), + ]; + }); + + // Monthly Hiring Trend for Chart (last 6 months) + if (isDemo()) { + $hiringTrend = [ + ['month' => now()->subMonths(5)->format('M Y'), 'hires' => 8], + ['month' => now()->subMonths(4)->format('M Y'), 'hires' => 12], + ['month' => now()->subMonths(3)->format('M Y'), 'hires' => 15], + ['month' => now()->subMonths(2)->format('M Y'), 'hires' => 10], + ['month' => now()->subMonths(1)->format('M Y'), 'hires' => 18], + ['month' => now()->format('M Y'), 'hires' => 14], + ]; + } else { + $hiringTrend = []; + for ($i = 5; $i >= 0; $i--) { + $month = now()->subMonths($i); + $count = Employee::whereIn('created_by', $companyUserIds) + ->whereMonth('created_at', $month->month) + ->whereYear('created_at', $month->year) + ->count(); + $hiringTrend[] = [ + 'month' => $month->format('M Y'), + 'hires' => $count, + ]; + } + } + + // Candidate Status Distribution for Chart + $candidateStatusStats = Candidate::whereIn('created_by', $companyUserIds) + ->selectRaw('status, COUNT(*) as count') + ->groupBy('status') + ->get() + ->map(function ($item) { + $colors = [ + 'New' => '#0EA5E9', + 'Screening' => '#F59E0B', + 'Interview' => '#8B5CF6', + 'Offer' => '#14B8A6', + 'Hired' => '#10B981', + 'Rejected' => '#EF4444', + ]; + + return [ + 'name' => $item->status, + 'value' => $item->count, + 'color' => $colors[$item->status] ?? '#6b7280', + ]; + }); + + // Leave Types for Chart + $leaveTypesStats = LeaveType::whereIn('created_by', $companyUserIds) + ->get() + ->map(function ($leaveType) { + return [ + 'name' => $leaveType->name, + 'value' => $leaveType->max_days_per_year, + 'color' => $leaveType->color ?: '#' . substr(md5($leaveType->name), 0, 6), + ]; + }); + + // Employee Growth Chart (Monthly for current year) + if (isDemo()) { + $employeeGrowthChart = [ + ['month' => 'January', 'employees' => 15], + ['month' => 'February', 'employees' => 5], + ['month' => 'March', 'employees' => 22], + ['month' => 'April', 'employees' => 10], + ['month' => 'May', 'employees' => 28], + ['month' => 'June', 'employees' => 32], + ['month' => 'July', 'employees' => 35], + ['month' => 'August', 'employees' => 50], + ['month' => 'September', 'employees' => 42], + ['month' => 'October', 'employees' => 45], + ['month' => 'November', 'employees' => 48], + ['month' => 'December', 'employees' => 52], + ]; + } else { + $employeeGrowthChart = []; + for ($month = 1; $month <= 12; $month++) { + $count = User::where('type', 'employee') + ->whereIn('created_by', $companyUserIds) + ->whereMonth('created_at', $month) + ->whereYear('created_at', now()->year) + ->count(); + $employeeGrowthChart[] = [ + 'month' => date('F', mktime(0, 0, 0, $month, 1)), + 'employees' => $count, + ]; + } + } + + // Recent Activities + $recentLeaves = LeaveApplication::whereIn('created_by', $companyUserIds) + ->with(['employee', 'leaveType']); + if (config('app.is_demo') == true) { + $recentLeaves = $recentLeaves->whereIn('status', ['approved', 'absent'])->get(); + } else { + $recentLeaves = $recentLeaves->whereIn('status', ['approved', 'absent']) + ->whereDate('start_date', '<=', today()) + ->whereDate('end_date', '>=', today()) + ->get(); + } + + $recentCandidates = Candidate::whereIn('created_by', $companyUserIds) + ->with(['job']) + ->orderBy('created_at', 'desc') + ->take(5) + ->get(); + + // Recent Announcements + $recentAnnouncements = Announcement::whereIn('created_by', $companyUserIds) + ->orderBy('created_at', 'desc') + ->take(5) + ->get(); + + // Recent Meetings + $recentMeetings = Meeting::whereIn('created_by', $companyUserIds) + ->orderBy('created_at', 'desc') + ->take(5) + ->get(); + + $dashboardData = [ + 'stats' => [ + 'totalEmployees' => $totalEmployees, + 'totalBranches' => $totalBranches, + 'totalDepartments' => $totalDepartments, + 'newEmployeesThisMonth' => $newEmployeesThisMonth, + 'jobPostsThisMonth' => $jobPostsThisMonth, + 'candidatesThisMonth' => $candidatesThisMonth, + 'attendanceRate' => $attendanceRate, + 'presentToday' => $presentToday, + 'pendingLeaves' => $pendingLeaves, + 'onLeaveToday' => $onLeaveToday, + 'activeJobPostings' => $activeJobPostings, + 'totalCandidates' => $totalCandidates, + ], + 'charts' => [ + 'departmentStats' => $departmentStats, + 'hiringTrend' => $hiringTrend, + 'candidateStatusStats' => $candidateStatusStats, + 'leaveTypesStats' => $leaveTypesStats, + 'employeeGrowthChart' => $employeeGrowthChart, + ], + 'recentActivities' => [ + 'leaves' => $recentLeaves, + 'candidates' => $recentCandidates, + 'announcements' => $recentAnnouncements, + 'meetings' => $recentMeetings, + ], + 'userType' => $user->type, + ]; + + return Inertia::render('dashboard', [ + 'dashboardData' => $dashboardData, + ]); + } + + private function renderEmployeeDashboard() + { + $user = auth()->user(); + $companyUserIds = $this->getCompanyUserIds(); + + // Recent Announcements + $recentAnnouncements = \App\Models\Announcement::whereIn('created_by', $companyUserIds) + ->orderBy('created_at', 'desc') + ->take(5) + ->get(); + + // Recent Meetings - get meetings where user is organizer + $recentMeetings = \App\Models\Meeting::with('attendees') + ->whereIn('created_by', $companyUserIds) + ->where('organizer_id', $user->id) + ->orderBy('created_at', 'desc') + ->get(); + + // Get meetings where user is attendee + $meetingAttendee = \App\Models\MeetingAttendee::with('meeting') + ->where('user_id', $user->id) + ->get(); + + // Extract meetings from attendee records + $attendeeMeetings = $meetingAttendee->pluck(value: 'meeting')->filter(); + + // Merge and remove duplicates + $recentMeetings = $recentMeetings->merge($attendeeMeetings) + ->unique('id') + ->filter(function ($meeting) { + return $meeting->meeting_date >= today(); + }) + ->sortByDesc('created_at') + ->values(); + + // Employee Stats + $totalAwards = \App\Models\Award::where('employee_id', $user->id)->count(); + $totalWarnings = \App\Models\Warning::where('employee_id', $user->id)->count(); + $totalComplaints = \App\Models\Complaint::where('against_employee_id', $user->id)->count(); + + // Get shifts and attendance policies for clock in functionality + $shifts = \App\Models\Shift::whereIn('created_by', $companyUserIds) + ->where('status', 'active') + ->get(['id', 'name', 'start_time', 'end_time']); + + $attendancePolicies = \App\Models\AttendancePolicy::whereIn('created_by', $companyUserIds) + ->where('status', 'active') + ->get(['id', 'name']); + + // Get today's attendance for the employee + $todayAttendance = AttendanceRecord::where('employee_id', $user->id) + ->where('date', \Carbon\Carbon::today()) + ->first(); + + // Get employee's assigned shift + $employeeShift = null; + $employee = Employee::where('user_id', $user->id)->first(); + if ($employee && $employee->shift_id) { + $employeeShift = Shift::find($employee->shift_id); + } + + // Auto clock out previous days like yesterday and alll thing if not clocked out + $previousAttendance = AttendanceRecord::where('employee_id', $user->id) + ->where('date', '<', \Carbon\Carbon::today()) + ->whereNotNull('clock_in') + ->whereNull('clock_out') + ->get(); + + foreach ($previousAttendance as $record) { + $recordDate = \Carbon\Carbon::parse($record->date); + $shift = Shift::find($record->shift_id) ?? $employeeShift; + + if ($shift) { + $record->update([ + 'clock_out' => $shift->end_time, + ]); + + if (method_exists($record, 'processAttendance')) { + $record->processAttendance(); + } + } + } + + // Auto clock out if shift end time has passed for today + // if ($todayAttendance && $todayAttendance->clock_in && !$todayAttendance->clock_out && $employeeShift) { + // $now = \Carbon\Carbon::now(); + // $shiftEndTime = \Carbon\Carbon::today()->setTimeFromTimeString($employeeShift->end_time); + + // if ($now->greaterThan($shiftEndTime)) { + // $todayAttendance->update([ + // 'clock_out' => $employeeShift->end_time, + // ]); + + // if (method_exists($todayAttendance, 'processAttendance')) { + // $todayAttendance->processAttendance(); + // } + + // $todayAttendance = $todayAttendance->fresh(); + // } + // } + + $dashboardData = [ + 'stats' => [ + 'totalAwards' => $totalAwards, + 'totalWarnings' => $totalWarnings, + 'totalComplaints' => $totalComplaints, + ], + 'recentActivities' => [ + 'announcements' => $recentAnnouncements, + 'meetings' => $recentMeetings, + ], + 'shifts' => $shifts, + 'attendancePolicies' => $attendancePolicies, + 'todayAttendance' => $todayAttendance, + 'currentTime' => \Carbon\Carbon::now()->format('H:i:s'), + 'employeeShift' => $employeeShift, + 'userType' => $user->type, + ]; + + return Inertia::render('employee-dashboard', [ + 'dashboardData' => $dashboardData, + ]); + } + + // private function getCompanyUserIds() + // { + // $user = auth()->user(); + // if ($user->type === 'company') { + // $companyUserIds = User::where('created_by', $user->id)->pluck('id')->toArray(); + // $companyUserIds[] = $user->id; + // return $companyUserIds; + // } else { + // $userCreatedBy = User::where('id', $user->created_by)->value('id'); + // $companyUserIds = User::where('created_by', $userCreatedBy)->pluck('id')->toArray(); + // $companyUserIds[] = $userCreatedBy; + // return $companyUserIds; + // } + // } + + private function getCompanyUserIds() + { + $user = auth()->user(); + if ($user->type === 'company') { + $companyId = getCompanyId($user->id); + if ($companyId) { + $allUsers = getAllCompanyUsers($companyId); + $allUsers[] = $companyId; // Include company itself + + return array_unique($allUsers); + } + + return []; + } else { + $companyId = getCompanyId($user->id); + if ($companyId) { + $allUsers = getAllCompanyUsers($companyId); + $allUsers[] = $companyId; // Include company itself + + return array_unique($allUsers); + } + + return []; + } + } +} diff --git a/app/Http/Controllers/DepartmentController.php b/app/Http/Controllers/DepartmentController.php new file mode 100644 index 000000000..e8c79f41f --- /dev/null +++ b/app/Http/Controllers/DepartmentController.php @@ -0,0 +1,219 @@ +can('manage-departments')) { + + $query = Department::with(['branch', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-departments')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-departments')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle branch filter + if ($request->has('branch_id') && !empty($request->branch_id) && $request->branch_id !== 'all') { + $query->where('branch_id', $request->branch_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $departments = $query->paginate($request->per_page ?? 10); + + // Get branches for filter dropdown + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name']); + + return Inertia::render('hr/departments/index', [ + 'departments' => $departments, + 'branches' => $branches, + 'filters' => $request->all(['search', 'branch_id', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + if (Auth::user()->can('create-departments')) { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'branch_id' => 'required|exists:branches,id', + 'description' => 'nullable|string', + 'status' => 'nullable|in:active,inactive', + ]); + + $validated['created_by'] = creatorId(); + $validated['status'] = $validated['status'] ?? 'active'; + + // Check if branch belongs to the current user's company + $branch = Branch::where('id', $validated['branch_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$branch) { + return redirect()->back()->with('error', __('Invalid branch selected.')); + } + + // Check if department with same name already exists in this branch + $exists = Department::where('name', $validated['name']) + ->where('branch_id', $validated['branch_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Department with this name already exists in the selected branch.')); + } + + Department::create($validated); + + return redirect()->back()->with('success', __('Department created successfully.')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(Request $request, $departmentId) + { + if (Auth::user()->can('edit-departments')) { + $department = Department::where('id', $departmentId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($department) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'branch_id' => 'required|exists:branches,id', + 'description' => 'nullable|string', + 'status' => 'nullable|in:active,inactive', + ]); + + // Check if branch belongs to the current user's company + $branch = Branch::where('id', $validated['branch_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$branch) { + return redirect()->back()->with('error', __('Invalid branch selected.')); + } + + // Check if department with same name already exists in this branch (excluding current department) + $exists = Department::where('name', $validated['name']) + ->where('branch_id', $validated['branch_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('id', '!=', $departmentId) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Department with this name already exists in the selected branch.')); + } + + $department->update($validated); + + return redirect()->back()->with('success', __('Department updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update department')); + } + } else { + return redirect()->back()->with('error', __('Department Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy($departmentId) + { + if (Auth::user()->can('delete-departments')) { + $department = Department::where('id', $departmentId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($department) { + try { + // Check if department has employees + if (class_exists('App\\Models\\Employee')) { + $employeeCount = \App\Models\User::where('type', 'employee') + ->whereHas('employee', function ($q) use ($departmentId) { + $q->where('department_id', $departmentId); + })->count(); + if ($employeeCount > 0) { + return response()->json(['message' => __('Cannot delete department with assigned employees')], 400); + } + } + $department->delete(); + return redirect()->back()->with('success', __('Department deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete department')); + } + } else { + return redirect()->back()->with('error', __('Department Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function toggleStatus($departmentId) + { + if (Auth::user()->can('toggle-status-departments')) { + $department = Department::where('id', $departmentId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($department) { + try { + $department->status = $department->status === 'active' ? 'inactive' : 'active'; + $department->save(); + + return redirect()->back()->with('success', __('Department status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update department status')); + } + } else { + return redirect()->back()->with('error', __('Department Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/DesignationController.php b/app/Http/Controllers/DesignationController.php new file mode 100644 index 000000000..84184fad9 --- /dev/null +++ b/app/Http/Controllers/DesignationController.php @@ -0,0 +1,186 @@ +can('manage-designations')) { + $query = Designation::with(['department', 'department.branch'])->where(function ($q) { + if (Auth::user()->can('manage-any-designations')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-designations')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle department filter + if ($request->has('department') && $request->department !== 'all') { + $query->where('department_id', $request->department); + } + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $designations = $query->paginate($request->per_page ?? 10); + + // Get departments for dropdown + $departments = Department::with('branch') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(); + + return Inertia::render('hr/designations/index', [ + 'designations' => $designations, + 'departments' => $departments, + 'filters' => $request->all(['search', 'sort_field', 'sort_direction', 'per_page', 'department']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + if (Auth::user()->can('create-designations')) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'department_id' => 'required|exists:departments,id', + 'status' => 'nullable|in:active,inactive', + ]); + + $validated['created_by'] = creatorId(); + + // Check if department belongs to current company + $department = Department::where('id', $validated['department_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$department) { + return redirect()->back()->with('error', __('Selected department does not belong to your company')); + } + + Designation::create($validated); + + return redirect()->back()->with('success', __('Designation created successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to create designation')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(Request $request, $designationId) + { + if (Auth::user()->can('edit-designations')) { + $designation = Designation::where('id', $designationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($designation) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'department_id' => 'required|exists:departments,id', + 'status' => 'nullable|in:active,inactive', + ]); + + // Check if department belongs to current company + $department = Department::where('id', $validated['department_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$department) { + return redirect()->back()->with('error', __('Selected department does not belong to your company.')); + } + + $designation->update($validated); + + return redirect()->back()->with('success', __('Designation updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update designation')); + } + } else { + return redirect()->back()->with('error', __('Designation Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy($designationId) + { + if (Auth::user()->can('delete-designations')) { + $designation = Designation::where('id', $designationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($designation) { + try { + $designation->delete(); + return redirect()->back()->with('success', __('Designation deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete designation')); + } + } else { + return redirect()->back()->with('error', __('Designation Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function toggleStatus($designationId) + { + if (Auth::user()->can('toggle-status-designations')) { + $designation = Designation::where('id', $designationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($designation) { + try { + $designation->status = $designation->status === 'active' ? 'inactive' : 'active'; + $designation->save(); + return redirect()->back()->with('success', __('Designation status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update designation status')); + } + } else { + return redirect()->back()->with('error', __('Designation Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/DocumentAcknowledgmentController.php b/app/Http/Controllers/DocumentAcknowledgmentController.php new file mode 100644 index 000000000..45507fd76 --- /dev/null +++ b/app/Http/Controllers/DocumentAcknowledgmentController.php @@ -0,0 +1,254 @@ +can('manage-document-acknowledgments')) { + $query = DocumentAcknowledgment::with(['document', 'user', 'assignedBy'])->where(function ($q) { + if (Auth::user()->can('manage-any-document-acknowledgments')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-document-acknowledgments')) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id())->orWhere('assigned_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->whereHas('document', function ($dq) use ($request) { + $dq->where('title', 'like', '%' . $request->search . '%'); + }) + ->orWhereHas('user', function ($uq) use ($request) { + $uq->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + if ($request->has('document_id') && !empty($request->document_id) && $request->document_id !== 'all') { + $query->where('document_id', $request->document_id); + } + + if ($request->has('user_id') && !empty($request->user_id) && $request->user_id !== 'all') { + $query->where('user_id', $request->user_id); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Auto-update overdue acknowledgments + DocumentAcknowledgment::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'Pending') + ->where('due_date', '<', Carbon::today()) + ->update(['status' => 'Overdue']); + + // Handle sorting + $allowedSortFields = ['id', 'status', 'due_date', 'acknowledged_at', 'assigned_at', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + if ($sortField === 'acknowledge') $sortField = 'acknowledged_at'; + if ($sortField === 'assigned') $sortField = 'assigned_at'; + + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $documentAcknowledgments = $query->paginate($request->per_page ?? 10); + + $documents = HrDocument::whereIn('created_by', getCompanyAndUsersId()) + ->where('requires_acknowledgment', true) + ->select('id', 'title') + ->get(); + + $users = User::whereIn('created_by', getCompanyAndUsersId()) + ->where('type', 'employee') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/documents/document-acknowledgments/index', [ + 'documentAcknowledgments' => $documentAcknowledgments, + 'documents' => $documents, + 'users' => $users, + 'filters' => $request->all(['search', 'document_id', 'user_id', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'document_id' => 'required|exists:hr_documents,id', + 'user_id' => 'required|exists:users,id', + 'due_date' => 'nullable|date|after_or_equal:today', + 'acknowledgment_note' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if acknowledgment already exists + $existing = DocumentAcknowledgment::where('document_id', $request->document_id) + ->where('user_id', $request->user_id) + ->first(); + + if ($existing) { + return redirect()->back()->with('error', __('Acknowledgment already exists for this user and document')); + } + + DocumentAcknowledgment::create([ + 'document_id' => $request->document_id, + 'user_id' => $request->user_id, + 'due_date' => $request->due_date ?? Carbon::now()->addDays(7), + 'acknowledgment_note' => $request->acknowledgment_note, + 'assigned_by' => creatorId(), + 'assigned_at' => now(), + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Document acknowledgment assigned successfully')); + } + + public function update(Request $request, DocumentAcknowledgment $documentAcknowledgment) + { + if (!in_array($documentAcknowledgment->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this acknowledgment')); + } + + $validator = Validator::make($request->all(), [ + 'document_id' => 'required|exists:hr_documents,id', + 'user_id' => 'required|exists:users,id', + 'status' => 'required|in:Pending,Acknowledged,Overdue,Exempted', + 'due_date' => 'nullable|date', + 'acknowledgment_note' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator->errors())->withInput(); + } + + // Check if acknowledgment already exists for different document/user combination + if ($request->document_id != $documentAcknowledgment->document_id || $request->user_id != $documentAcknowledgment->user_id) { + $existing = DocumentAcknowledgment::where('document_id', $request->document_id) + ->where('user_id', $request->user_id) + ->where('id', '!=', $documentAcknowledgment->id) + ->first(); + + if ($existing) { + return redirect()->back()->with('error', __('Acknowledgment already exists for this user and document')); + } + } + + $updateData = [ + 'document_id' => $request->document_id, + 'user_id' => $request->user_id, + 'status' => $request->status, + 'due_date' => $request->due_date, + 'acknowledgment_note' => $request->acknowledgment_note, + ]; + + if ($request->status === 'Acknowledged' && !$documentAcknowledgment->acknowledged_at) { + $updateData['acknowledged_at'] = now(); + $updateData['ip_address'] = $request->ip(); + $updateData['user_agent'] = $request->userAgent(); + } + + $documentAcknowledgment->update($updateData); + + return redirect()->back()->with('success', __('Document acknowledgment updated successfully')); + } + + public function destroy(DocumentAcknowledgment $documentAcknowledgment) + { + if (!in_array($documentAcknowledgment->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this acknowledgment')); + } + + $documentAcknowledgment->delete(); + return redirect()->back()->with('success', __('Document acknowledgment deleted successfully')); + } + + public function acknowledge(Request $request, DocumentAcknowledgment $documentAcknowledgment) + { + if (!in_array($documentAcknowledgment->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to acknowledge this document')); + } + + $validator = Validator::make($request->all(), [ + 'acknowledgment_note' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $documentAcknowledgment->update([ + 'status' => 'Acknowledged', + 'acknowledged_at' => now(), + 'acknowledgment_note' => $request->acknowledgment_note ?? 'Document acknowledged', + 'ip_address' => $request->ip(), + 'user_agent' => $request->userAgent(), + ]); + + return redirect()->back()->with('success', __('Document acknowledged successfully')); + } + + public function bulkAssign(Request $request) + { + $validator = Validator::make($request->all(), [ + 'document_id' => 'required|exists:hr_documents,id', + 'user_ids' => 'required|array', + 'user_ids.*' => 'exists:users,id', + 'due_date' => 'nullable|date|after_or_equal:today', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $assignedCount = 0; + $dueDate = $request->due_date ?? Carbon::now()->addDays(7); + + foreach ($request->user_ids as $userId) { + // Check if acknowledgment already exists + $existing = DocumentAcknowledgment::where('document_id', $request->document_id) + ->where('user_id', $userId) + ->first(); + + if (!$existing) { + DocumentAcknowledgment::create([ + 'document_id' => $request->document_id, + 'user_id' => $userId, + 'due_date' => $dueDate, + 'assigned_by' => creatorId(), + 'assigned_at' => now(), + 'created_by' => creatorId(), + ]); + $assignedCount++; + } + } + + return redirect()->back()->with('success', __('Document assigned to :count users successfully', ['count' => $assignedCount])); + } +} diff --git a/app/Http/Controllers/DocumentCategoryController.php b/app/Http/Controllers/DocumentCategoryController.php new file mode 100644 index 000000000..b21714efb --- /dev/null +++ b/app/Http/Controllers/DocumentCategoryController.php @@ -0,0 +1,155 @@ +can('manage-document-categories')) { + $query = DocumentCategory::withCount('documents')->where(function ($q) { + if (Auth::user()->can('manage-any-document-categories')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-document-categories')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('is_mandatory') && $request->is_mandatory !== 'all') { + $query->where('is_mandatory', $request->is_mandatory === 'true'); + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'status', 'sort_order', 'is_mandatory', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field === 'category' ? 'name' : $request->sort_field; + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('sort_order', 'asc')->orderBy('id', 'desc'); + } + } else { + $query->orderBy('sort_order', 'asc')->orderBy('id', 'desc'); + } + + $documentCategories = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/documents/document-categories/index', [ + 'documentCategories' => $documentCategories, + 'filters' => $request->all(['search', 'status', 'is_mandatory', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'color' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/', + 'icon' => 'required|string|max:50', + 'sort_order' => 'nullable|integer|min:0', + 'is_mandatory' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + DocumentCategory::create([ + 'name' => $request->name, + 'description' => $request->description, + 'color' => $request->color, + 'icon' => $request->icon, + 'sort_order' => $request->sort_order ?? 0, + 'is_mandatory' => $request->boolean('is_mandatory'), + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Document category created successfully')); + } + + public function update(Request $request, DocumentCategory $documentCategory) + { + if (!in_array($documentCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this category')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'color' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/', + 'icon' => 'required|string|max:50', + 'sort_order' => 'nullable|integer|min:0', + 'is_mandatory' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $documentCategory->update([ + 'name' => $request->name, + 'description' => $request->description, + 'color' => $request->color, + 'icon' => $request->icon, + 'sort_order' => $request->sort_order ?? 0, + 'is_mandatory' => $request->boolean('is_mandatory'), + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Document category updated successfully')); + } + + public function destroy(DocumentCategory $documentCategory) + { + if (!in_array($documentCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this category')); + } + + if ($documentCategory->documents()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete category as it contains documents')); + } + + $documentCategory->delete(); + return redirect()->back()->with('success', __('Document category deleted successfully')); + } + + public function toggleStatus(DocumentCategory $documentCategory) + { + if (!in_array($documentCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this category')); + } + + $documentCategory->update([ + 'status' => $documentCategory->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Category status updated successfully')); + } +} diff --git a/app/Http/Controllers/DocumentTemplateController.php b/app/Http/Controllers/DocumentTemplateController.php new file mode 100644 index 000000000..26f3153cb --- /dev/null +++ b/app/Http/Controllers/DocumentTemplateController.php @@ -0,0 +1,353 @@ +can('manage-document-templates')) { + $query = DocumentTemplate::with(['category']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-document-templates')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-document-templates')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%'.$request->search.'%') + ->orWhere('description', 'like', '%'.$request->search.'%'); + }); + } + + if ($request->has('category_id') && ! empty($request->category_id) && $request->category_id !== 'all') { + $query->where('category_id', $request->category_id); + } + + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('is_default') && $request->is_default !== 'all') { + $query->where('is_default', $request->is_default === 'true'); + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'status', 'is_default', 'created_at']; + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field === 'template_name' ? 'name' : ($request->sort_field === 'created' ? 'created_at' : $request->sort_field); + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('is_default', 'desc')->orderBy('created_at', 'desc'); + } + } else { + $query->orderBy('is_default', 'desc')->orderBy('created_at', 'desc'); + } + + $documentTemplates = $query->paginate($request->per_page ?? 10); + + $categories = DocumentCategory::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/documents/document-templates/index', [ + 'documentTemplates' => $documentTemplates, + 'categories' => $categories, + 'filters' => $request->all(['search', 'category_id', 'status', 'is_default', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function show(DocumentTemplate $documentTemplate) + { + if (Auth::user()->can('view-document-templates')) { + $documentTemplate->load('category'); + return Inertia::render('hr/documents/document-templates/show', [ + 'documentTemplate' => $documentTemplate, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function create() + { + if (Auth::user()->can('create-document-templates')) { + $categories = DocumentCategory::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/documents/document-templates/create', [ + 'categories' => $categories, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function edit(DocumentTemplate $documentTemplate) + { + if (Auth::user()->can('edit-document-templates')) { + $categories = DocumentCategory::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/documents/document-templates/edit', [ + 'documentTemplate' => $documentTemplate, + 'categories' => $categories, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + if (Auth::user()->can('create-document-templates')) { + // Convert placeholders comma-separated string → array + $placeholders = null; + if ($request->filled('placeholders') && is_string($request->placeholders)) { + $placeholders = array_values(array_filter(array_map('trim', explode(',', $request->placeholders)))); + } elseif (is_array($request->placeholders)) { + $placeholders = $request->placeholders; + } + + // Convert default_values JSON string → array + $defaultValues = null; + if ($request->filled('default_values') && is_string($request->default_values)) { + $decoded = json_decode($request->default_values, true); + $defaultValues = is_array($decoded) ? $decoded : null; + } elseif (is_array($request->default_values)) { + $defaultValues = $request->default_values; + } + + $validator = Validator::make(array_merge($request->all(), [ + 'placeholders' => $placeholders, + 'default_values' => $defaultValues, + ]), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'category_id' => 'required|exists:document_categories,id', + 'template_content' => 'required|string', + 'placeholders' => 'nullable|array', + 'default_values' => 'nullable|array', + 'is_default' => 'boolean', + 'file_format' => 'nullable|string|in:pdf,doc,docx,txt', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return back()->withErrors($validator)->withInput(); + } + + if ($request->boolean('is_default')) { + DocumentTemplate::whereIn('created_by', getCompanyAndUsersId()) + ->where('category_id', $request->category_id) + ->where('is_default', true) + ->update(['is_default' => false]); + } + + DocumentTemplate::create([ + 'name' => $request->name, + 'description' => $request->description, + 'category_id' => $request->category_id, + 'template_content' => $request->template_content, + 'placeholders' => $placeholders, + 'default_values' => $defaultValues, + 'is_default' => $request->boolean('is_default'), + 'file_format' => $request->file_format ?? 'pdf', + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->route('hr.documents.document-templates.index') + ->with('success', __('Document template created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(Request $request, DocumentTemplate $documentTemplate) + { + if (Auth::user()->can('edit-document-templates')) { + // Convert placeholders comma-separated string → array + $placeholders = null; + if ($request->filled('placeholders') && is_string($request->placeholders)) { + $placeholders = array_values(array_filter(array_map('trim', explode(',', $request->placeholders)))); + } elseif (is_array($request->placeholders)) { + $placeholders = $request->placeholders; + } + + // Convert default_values JSON string → array + $defaultValues = null; + if ($request->filled('default_values') && is_string($request->default_values)) { + $decoded = json_decode($request->default_values, true); + $defaultValues = is_array($decoded) ? $decoded : null; + } elseif (is_array($request->default_values)) { + $defaultValues = $request->default_values; + } + + $validator = Validator::make(array_merge($request->all(), [ + 'placeholders' => $placeholders, + 'default_values' => $defaultValues, + ]), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'category_id' => 'required|exists:document_categories,id', + 'template_content' => 'required|string', + 'placeholders' => 'nullable|array', + 'default_values' => 'nullable|array', + 'is_default' => 'boolean', + 'file_format' => 'nullable|string|in:pdf,doc,docx,txt', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return back()->withErrors($validator)->withInput(); + } + + if ($request->boolean('is_default') && ! $documentTemplate->is_default) { + DocumentTemplate::whereIn('created_by', getCompanyAndUsersId()) + ->where('category_id', $request->category_id) + ->where('is_default', true) + ->update(['is_default' => false]); + } + + $documentTemplate->update([ + 'name' => $request->name, + 'description' => $request->description, + 'category_id' => $request->category_id, + 'template_content' => $request->template_content, + 'placeholders' => $placeholders, + 'default_values' => $defaultValues, + 'is_default' => $request->boolean('is_default'), + 'file_format' => $request->file_format ?? 'pdf', + 'status' => $request->status ?? 'active', + ]); + + return redirect()->route('hr.documents.document-templates.index') + ->with('success', __('Document template updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy(DocumentTemplate $documentTemplate) + { + if (Auth::user()->can('delete-document-templates')) { + try { + $documentTemplate->delete(); + return redirect()->back()->with('success', __('Document template deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete document template')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function toggleStatus(DocumentTemplate $documentTemplate) + { + if (Auth::user()->can('edit-document-templates')) { + try { + $documentTemplate->update([ + 'status' => $documentTemplate->status === 'active' ? 'inactive' : 'active', + ]); + return redirect()->back()->with('success', __('Template status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update template status')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function preview(Request $request, DocumentTemplate $documentTemplate) + { + if (Auth::user()->can('view-document-templates')) { + $values = $request->get('values', []); + $generatedContent = $documentTemplate->generateDocument($values); + + return response()->json([ + 'content' => $generatedContent, + 'placeholders' => $documentTemplate->getPlaceholderList(), + 'default_values' => $documentTemplate->default_values, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function generate(Request $request, DocumentTemplate $documentTemplate) + { + $values = $request->values ?? []; + + if (!is_array($values)) { + $values = []; + } + + $generatedContent = $documentTemplate->generateDocument($values); + $filename = $request->filename ?? ($documentTemplate->name.'_'.date('Y-m-d')); + $fileFormat = $documentTemplate->file_format ?? 'txt'; + + switch ($fileFormat) { + case 'pdf': + $html = '
'.nl2br($generatedContent).'
'; + $pdf = Pdf::loadHTML($html); + return $pdf->download($filename.'.pdf'); + + case 'doc': + case 'docx': + $phpWord = new PhpWord; + $section = $phpWord->addSection(); + + $lines = explode("\n", $generatedContent); + foreach ($lines as $line) { + if (trim($line) !== '') { + $section->addText($line); + } else { + $section->addTextBreak(); + } + } + + $writer = IOFactory::createWriter($phpWord, $fileFormat === 'docx' ? 'Word2007' : 'RTF'); + $tempFile = tempnam(sys_get_temp_dir(), 'document'); + $writer->save($tempFile); + + $contentType = $fileFormat === 'docx' + ? 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' + : 'application/msword'; + + return response()->download($tempFile, $filename.'.'.$fileFormat, [ + 'Content-Type' => $contentType, + ])->deleteFileAfterSend(true); + + default: // txt + return response($generatedContent) + ->header('Content-Type', 'text/plain') + ->header('Content-Disposition', 'attachment; filename="'.$filename.'.txt"'); + } + } +} diff --git a/app/Http/Controllers/DocumentTypeController.php b/app/Http/Controllers/DocumentTypeController.php new file mode 100644 index 000000000..700fb5fd1 --- /dev/null +++ b/app/Http/Controllers/DocumentTypeController.php @@ -0,0 +1,145 @@ +can('manage-document-types')) { + $query = DocumentType::where(function ($q) { + if (Auth::user()->can('manage-any-document-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-document-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle required filter + if ($request->has('required') && $request->required !== 'all') { + $isRequired = $request->required === 'yes'; + $query->where('is_required', $isRequired); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $documentTypes = $query->paginate($request->per_page ?? 10); + + // Cast is_required to boolean for each document type + $documentTypes->getCollection()->transform(function ($documentType) { + $documentType->is_required = (bool) $documentType->is_required; + return $documentType; + }); + + return Inertia::render('hr/document-types/index', [ + 'documentTypes' => $documentTypes, + 'filters' => $request->all(['search', 'sort_field', 'sort_direction', 'per_page', 'required']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function store(Request $request) + { + if (Auth::user()->can('create-document-types')) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'is_required' => 'boolean', + ]); + + $validated['created_by'] = creatorId(); + + DocumentType::create($validated); + return redirect()->back()->with('success', __('Document type created successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to create document type')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function update(Request $request, $documentTypeId) + { + if (Auth::user()->can('edit-document-types')) { + $documentType = DocumentType::where('id', $documentTypeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($documentType) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'is_required' => 'boolean', + ]); + + $documentType->update($validated); + return redirect()->back()->with('success', __('Document type updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update document type')); + } + } else { + return redirect()->back()->with('error', __('Document Type Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function destroy($documentTypeId) + { + if (Auth::user()->can('delete-document-types')) { + $documentType = DocumentType::where('id', $documentTypeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($documentType) { + try { + $documentType->delete(); + return redirect()->back()->with('success', __('Document type deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete document type')); + } + } else { + return redirect()->back()->with('error', __('Document Type Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/EasebuzzPaymentController.php b/app/Http/Controllers/EasebuzzPaymentController.php new file mode 100644 index 000000000..f14f8135e --- /dev/null +++ b/app/Http/Controllers/EasebuzzPaymentController.php @@ -0,0 +1,203 @@ + 'required|string', + 'status' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['easebuzz_merchant_key'])) { + return back()->withErrors(['error' => __('Easebuzz not configured')]); + } + + if ($validated['status'] === 'success') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'easebuzz', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['easepayid'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'easebuzz'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['easebuzz_merchant_key']) || !isset($settings['payment_settings']['easebuzz_salt_key'])) { + return response()->json(['error' => __('Easebuzz not configured')], 400); + } + + // Include Easebuzz library + require_once app_path('Libraries/Easebuzz/easebuzz_payment_gateway.php'); + + $user = auth()->user(); + $txnid = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + $environment = $settings['payment_settings']['easebuzz_environment'] === 'prod' ? 'prod' : 'test'; + + // Initialize Easebuzz + $easebuzz = new \Easebuzz( + $settings['payment_settings']['easebuzz_merchant_key'], + $settings['payment_settings']['easebuzz_salt_key'], + $environment + ); + + $postData = [ + 'txnid' => $txnid, + 'amount' => number_format($pricing['final_price'], 2, '.', ''), + 'productinfo' => $plan->name, + 'firstname' => $user->name ?? 'Customer', + 'email' => $user->email, + 'phone' => '9999999999', + 'surl' => route('easebuzz.success'), + 'furl' => route('plans.index'), + 'udf1' => $validated['billing_cycle'], + 'udf2' => $validated['coupon_code'] ?? '', + ]; + + // Use Easebuzz library to initiate payment + $result = $easebuzz->initiatePaymentAPI($postData, false); + + $resultArray = json_decode($result, true); + + if ($resultArray && isset($resultArray['status']) && $resultArray['status'] == 1) { + $accessKey = $resultArray['access_key'] ?? null; + if ($accessKey) { + $baseUrl = $settings['payment_settings']['easebuzz_environment'] === 'prod' + ? 'https://pay.easebuzz.in' + : 'https://testpay.easebuzz.in'; + + return response()->json([ + 'success' => true, + 'payment_url' => $baseUrl . '/pay/' . $accessKey, + 'transaction_id' => $txnid + ]); + } + } + + return response()->json(['error' => 'Payment initialization failed'], 400); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + try { + // Include Easebuzz library + require_once app_path('Libraries/Easebuzz/easebuzz_payment_gateway.php'); + + $settings = getPaymentGatewaySettings(); + $environment = $settings['payment_settings']['easebuzz_environment'] === 'prod' ? 'prod' : 'test'; + + $easebuzz = new \Easebuzz( + $settings['payment_settings']['easebuzz_merchant_key'], + $settings['payment_settings']['easebuzz_salt_key'], + $environment + ); + + // Verify payment response + $result = $easebuzz->easebuzzResponse($request->all()); + $resultArray = json_decode($result, true); + + if ($resultArray && $resultArray['status'] == 1 && $request->input('status') === 'success') { + $txnid = $request->input('txnid'); + $parts = explode('_', $txnid); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $request->input('udf1', 'monthly'), + 'payment_method' => 'easebuzz', + 'payment_id' => $request->input('easepayid'), + ]); + + // Log the user in if not already authenticated + if (!auth()->check()) { + auth()->login($user); + } + + return redirect()->route('plans.index')->with('success', __('Payment completed successfully and plan activated')); + } + } + } + + return redirect()->route('plans.index')->with('error', __('Payment verification failed')); + + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment processing failed')); + } + } + + public function callback(Request $request) + { + try { + $txnid = $request->input('txnid'); + $status = $request->input('status'); + + if ($txnid && $status === 'success') { + $parts = explode('_', $txnid); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = \App\Models\User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $request->input('udf1', 'monthly'), + 'payment_method' => 'easebuzz', + 'payment_id' => $request->input('easepayid'), + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/EmailTemplateController.php b/app/Http/Controllers/EmailTemplateController.php new file mode 100644 index 000000000..4e0544844 --- /dev/null +++ b/app/Http/Controllers/EmailTemplateController.php @@ -0,0 +1,115 @@ +has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('from', 'like', '%' . $request->search . '%'); + }); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'asc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $templates = $query->paginate($request->per_page ?? 10); + + return Inertia::render('email-templates/index', [ + 'templates' => $templates, + 'filters' => $request->all(['search', 'sort_field', 'sort_direction', 'per_page']) + ]); + } + + public function show(EmailTemplate $emailTemplate) + { + $template = $emailTemplate->load('emailTemplateLangs'); + $languages = json_decode(file_get_contents(resource_path('lang/language.json')), true); + + // Template-specific variables + $variables = []; + + if ($template->name === 'Appointment Created') { + $variables = [ + '{app_name}' => 'App Name', + '{appointment_name}' => 'Appointment Name', + '{appointment_email}' => 'Appointment Email', + '{appointment_phone}' => 'Appointment Phone', + '{appointment_date}' => 'Appointment Date', + '{appointment_time}' => 'Appointment Time' + ]; + } elseif ($template->name === 'User Created') { + $variables = [ + '{app_url}' => 'App URL', + '{user_name}' => 'User Name', + '{user_email}' => 'User Email', + '{user_password}' => 'User Password', + '{user_type}' => 'User Type' + ]; + } + + return Inertia::render('email-templates/show', [ + 'template' => $template, + 'languages' => $languages, + 'variables' => $variables + ]); + } + + public function updateSettings(EmailTemplate $emailTemplate, Request $request) + { + try { + $request->validate([ + 'from' => 'required|string|max:255' + ]); + + $emailTemplate->update([ + 'from' => $request->from + ]); + + return redirect()->back()->with('success', __('Template settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update template settings: :error', ['error' => $e->getMessage()])); + } + } + + public function updateContent(EmailTemplate $emailTemplate, Request $request) + { + try { + $request->validate([ + 'lang' => 'required|string|max:10', + 'subject' => 'required|string|max:255', + 'content' => 'required|string' + ]); + + $emailTemplate->emailTemplateLangs() + ->where('lang', $request->lang) + ->update([ + 'subject' => $request->subject, + 'content' => $request->content + ]); + + return redirect()->back()->with('success', __('Email content updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update email content: :error', ['error' => $e->getMessage()])); + } + } +} diff --git a/app/Http/Controllers/EmployeeContractController.php b/app/Http/Controllers/EmployeeContractController.php new file mode 100644 index 000000000..1524599e1 --- /dev/null +++ b/app/Http/Controllers/EmployeeContractController.php @@ -0,0 +1,281 @@ +can('manage-employee-contracts')) { + $query = EmployeeContract::with(['employee', 'contractType', 'approver'])->where(function ($q) { + if (Auth::user()->can('manage-any-awards')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employee-contracts')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('contract_number', 'like', '%'.$request->search.'%') + ->orWhereHas('employee', function ($eq) use ($request) { + $eq->where('name', 'like', '%'.$request->search.'%'); + }); + }); + } + + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('contract_type_id') && ! empty($request->contract_type_id) && $request->contract_type_id !== 'all') { + $query->where('contract_type_id', $request->contract_type_id); + } + + if ($request->has('employee_id') && ! empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + // Handle sorting + $allowedSortFields = ['id', 'contract_number', 'start_date', 'end_date', 'basic_salary', 'status', 'created_at']; + + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + + if ($sortField === 'contract' || $sortField === 'contract_type') { + $query->join('contract_types', 'employee_contracts.contract_type_id', '=', 'contract_types.id') + ->select('employee_contracts.*') + ->orderBy('contract_types.name', $sortDirection); + } elseif ($sortField === 'contract_period') { + $query->orderBy('start_date', $sortDirection); + } elseif (in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + // Auto-update expired contracts + EmployeeContract::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'Active') + ->where('end_date', '<', Carbon::today()) + ->update(['status' => 'Expired']); + $employeeContracts = $query->paginate($request->per_page ?? 10); + + $employeeContracts->getCollection()->transform(function ($contract) { + if ($contract->employee) { + $rawAvatar = $contract->employee->getRawOriginal('avatar'); + $contract->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + + return $contract; + }); + + $contractTypes = ContractType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $employees = User::whereIn('created_by', getCompanyAndUsersId()) + ->where('type', 'employee') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/contracts/employee-contracts/index', [ + 'employeeContracts' => $employeeContracts, + 'contractTypes' => $contractTypes, + 'employees' => $employees, + 'filters' => $request->all(['search', 'status', 'contract_type_id', 'employee_id', 'per_page', 'sort_field', 'sort_direction']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'contract_type_id' => 'required|exists:contract_types,id', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after:start_date', + 'basic_salary' => 'required|numeric|min:0', + 'terms_conditions' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check for duplicate contract + $existingContract = EmployeeContract::where('employee_id', $request->employee_id) + ->where('contract_type_id', $request->contract_type_id) + ->where('start_date', $request->start_date) + ->where('end_date', $request->end_date) + ->first(); + + if ($existingContract) { + return redirect()->back()->with('error', __('A contract with the same details already exists for this employee.')); + } + + // Generate contract number + $lastContract = EmployeeContract::whereIn('created_by', getCompanyAndUsersId()) + ->orderBy('id', 'desc') + ->first(); + $nextNumber = $lastContract ? (intval(substr($lastContract->contract_number, -4)) + 1) : 1; + $contractNumber = 'CON-'.str_pad(creatorId(), 3, '0', STR_PAD_LEFT).'-'.str_pad($nextNumber, 4, '0', STR_PAD_LEFT); + + EmployeeContract::create([ + 'contract_number' => $contractNumber, + 'employee_id' => $request->employee_id, + 'contract_type_id' => $request->contract_type_id, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'basic_salary' => $request->basic_salary, + 'terms_conditions' => $request->terms_conditions, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Employee contract created successfully')); + } + + public function update(Request $request, EmployeeContract $employeeContract) + { + if (! in_array($employeeContract->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this contract')); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'contract_type_id' => 'required|exists:contract_types,id', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after:start_date', + 'basic_salary' => 'required|numeric|min:0', + 'terms_conditions' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check for duplicate contract (excluding current contract) + $existingContract = EmployeeContract::where('employee_id', $request->employee_id) + ->where('contract_type_id', $request->contract_type_id) + ->where('start_date', $request->start_date) + ->where('end_date', $request->end_date) + ->where('id', '!=', $employeeContract->id) + ->first(); + + if ($existingContract) { + return redirect()->back()->with('error', __('A contract with the same details already exists for this employee.')); + } + + $employeeContract->update([ + 'employee_id' => $request->employee_id, + 'contract_type_id' => $request->contract_type_id, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'basic_salary' => $request->basic_salary, + 'terms_conditions' => $request->terms_conditions, + ]); + + return redirect()->back()->with('success', __('Employee contract updated successfully')); + } + + public function destroy(EmployeeContract $employeeContract) + { + if (! in_array($employeeContract->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this contract')); + } + + if ($employeeContract->status === 'Active') { + return redirect()->back()->with('error', __('Cannot delete active contract')); + } + + $employeeContract->delete(); + + return redirect()->back()->with('success', __('Employee contract deleted successfully')); + } + + public function updateStatus(Request $request, EmployeeContract $employeeContract) + { + if (Auth::user()->can('approve-employee-contracts') || Auth::user()->can('reject-employee-contracts')) { + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:Draft,Pending Approval,Active,Expired,Terminated,Renewed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $updateData = ['status' => $request->status]; + + if ($request->status === 'Active') { + $updateData['approved_by'] = creatorId(); + $updateData['approved_at'] = now(); + } + + $employeeContract->update($updateData); + + return redirect()->back()->with('success', __('Contract status updated successfully')); + } else { + return redirect()->back()->with('error', __('You do not have permission to update this contract')); + } + } + + public function approve(Request $request, EmployeeContract $employeeContract) + { + if (! in_array($employeeContract->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to approve this contract')); + } + + $employeeContract->update([ + 'status' => 'Active', + 'approved_by' => creatorId(), + 'approved_at' => now(), + 'approval_notes' => $request->approval_notes, + ]); + + return redirect()->back()->with('success', __('Contract approved successfully')); + } + + public function reject(Request $request, EmployeeContract $employeeContract) + { + if (! in_array($employeeContract->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to reject this contract')); + } + + $validator = Validator::make($request->all(), [ + 'rejection_reason' => 'required|string|max:1000', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $employeeContract->update([ + 'status' => 'Draft', + 'rejection_reason' => $request->rejection_reason, + 'rejected_by' => creatorId(), + 'rejected_at' => now(), + ]); + + return redirect()->back()->with('success', __('Contract rejected successfully')); + } +} diff --git a/app/Http/Controllers/EmployeeController.php b/app/Http/Controllers/EmployeeController.php new file mode 100644 index 000000000..05ad1ce15 --- /dev/null +++ b/app/Http/Controllers/EmployeeController.php @@ -0,0 +1,1393 @@ +can('manage-employees')) { + $authUser = Auth::user(); + $query = User::with(['employee.branch', 'employee.department', 'employee.designation']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-employees')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employees')) { + $q->where('created_by', Auth::id())->orWhere('id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }) + ->where('type', 'employee'); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('email', 'like', '%' . $request->search . '%') + ->orWhereHas('employee', function ($eq) use ($request) { + $eq->where('employee_id', 'like', '%' . $request->search . '%') + ->orWhere('phone', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle department filter + if ($request->has('department') && !empty($request->department) && $request->department !== 'all') { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('department_id', $request->department); + }); + } + + // Handle branch filter + if ($request->has('branch') && !empty($request->branch) && $request->branch !== 'all') { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('branch_id', $request->branch); + }); + } + + // Handle designation filter + if ($request->has('designation') && !empty($request->designation) && $request->designation !== 'all') { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('designation_id', $request->designation); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('employee_status', $request->status); + }); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + // Validate sort direction + if (!in_array($sortDirection, ['asc', 'desc'])) { + $sortDirection = 'desc'; + } + + $query->orderBy($sortField, $sortDirection); + + $employees = $query->paginate($request->per_page ?? 10); + + $employees->getCollection()->transform(function ($employee) { + $employee->avatar = check_file($employee->avatar) ? get_file($employee->avatar) : get_file('avatars/avatar.png'); + return $employee; + }); + + // Get branches, departments, and designations for filters + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name']); + + $departments = Department::with('branch') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'branch_id']); + + $designations = Designation::with('department') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'department_id']); + + // Get plan limits for company users and staff users (only in SaaS mode) + $planLimits = null; + if (isSaas()) { + if ($authUser->type === 'company' && $authUser->plan) { + $currentUserCount = User::where('type', 'employee')->whereIn('created_by', getCompanyAndUsersId())->count(); + $planLimits = [ + 'current_users' => $currentUserCount, + 'max_users' => $authUser->plan->max_employees, + 'can_create' => $currentUserCount < $authUser->plan->max_employees, + ]; + } + // Check for staff users (created by company users) + elseif ($authUser->type !== 'superadmin' && $authUser->created_by) { + $companyUser = User::find($authUser->created_by); + if ($companyUser && $companyUser->type === 'company' && $companyUser->plan) { + $currentUserCount = User::where('type', 'employee')->whereIn('created_by', getCompanyAndUsersId())->count(); + $planLimits = [ + 'current_users' => $currentUserCount, + 'max_users' => $companyUser->plan->max_employees, + 'can_create' => $currentUserCount < $companyUser->plan->max_employees, + ]; + } + } + } + + return Inertia::render('hr/employees/index', [ + 'employees' => $employees, + 'branches' => $branches, + 'planLimits' => $planLimits, + 'departments' => $departments, + 'designations' => $designations, + 'hasSampleFile' => file_exists(storage_path('uploads/sample/sample-employee.xlsx')), + 'filters' => $request->all(['search', 'department', 'branch', 'designation', 'status', 'sort_field', 'sort_direction', 'per_page', 'view']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function create() + { + if (Auth::user()->can('create-employees')) { + // Get branches, departments, designations, and document types for the form + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name']); + + $departments = Department::with('branch') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'branch_id']); + + $designations = Designation::with('department') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'department_id']); + + $documentTypes = DocumentType::whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name', 'is_required']); + + $shifts = \App\Models\Shift::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'start_time', 'end_time']); + + $attendancePolicies = \App\Models\AttendancePolicy::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name']); + + return Inertia::render('hr/employees/create', [ + 'branches' => $branches, + 'departments' => $departments, + 'designations' => $designations, + 'documentTypes' => $documentTypes, + 'shifts' => $shifts, + 'attendancePolicies' => $attendancePolicies, + 'generatedEmployeeId' => Employee::generateEmployeeId(), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function store(Request $request) + { + if (Auth::user()->can('create-employees')) { + try { + // Validate basic information + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'biometric_emp_id' => 'nullable|string|max:255|unique:employees,biometric_emp_id', + 'email' => 'required|email|max:255|unique:users,email', + 'password' => 'required|string|min:8', + 'phone' => 'required|string|max:20', + 'date_of_birth' => 'required|date', + 'gender' => 'required|in:male,female,other', + 'profile_image' => 'required|image|mimes:jpeg,png,jpg,gif|max:2048', + 'shift_id' => 'nullable|exists:shifts,id', + 'attendance_policy_id' => 'nullable|exists:attendance_policies,id', + + // Employment details + 'branch_id' => 'required|exists:branches,id', + 'department_id' => 'required|exists:departments,id', + 'designation_id' => 'required|exists:designations,id', + 'date_of_joining' => 'required|date', + 'employment_type' => 'required|string|max:50', + 'employee_status' => 'required|string|max:50', + + // Contact information + 'address_line_1' => 'required|string|max:255', + 'city' => 'required|string|max:100', + 'state' => 'required|string|max:100', + 'country' => 'required|string|max:100', + 'postal_code' => 'required|string|max:20', + 'emergency_contact_name' => 'required|string|max:255', + 'emergency_contact_relationship' => 'required|string|max:100', + 'emergency_contact_number' => 'required|string|max:20', + + // Banking information + 'bank_name' => 'required|string|max:255', + 'account_holder_name' => 'nullable|string|max:255', + 'account_number' => 'nullable|string|max:50', + 'bank_identifier_code' => 'nullable|string|max:50', + 'bank_branch' => 'nullable|string|max:255', + 'tax_payer_id' => 'nullable|string|max:50', + + // Documents + 'documents' => 'nullable|array', + 'documents.*.document_type_id' => 'required|exists:document_types,id', + 'documents.*.file' => 'required|file|mimes:jpeg,png,jpg,pdf,doc,docx|max:5120', + 'documents.*.expiry_date' => 'nullable|date', + ]); + + $validator->after(function ($validator) use ($request) { + $requiredDocTypes = DocumentType::whereIn('created_by', getCompanyAndUsersId()) + ->where('is_required', 1) + ->pluck('id')->toArray(); + + $submittedDocTypes = []; + if ($request->has('documents') && is_array($request->documents)) { + foreach ($request->documents as $index => $doc) { + if ($request->hasFile("documents.$index.file")) { + $submittedDocTypes[] = (int) $doc['document_type_id']; + } + } + } + + foreach ($requiredDocTypes as $reqDocTypeId) { + if (!in_array($reqDocTypeId, $submittedDocTypes)) { + $validator->errors()->add('documents', __('All required documents must be uploaded.')); + break; + } + } + }); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + \DB::beginTransaction(); + + // Create User model object + $user = new User; + $user->name = $request->name; + $user->email = $request->email; + $user->password = Hash::make($request->password); + $user->type = 'employee'; + $user->lang = 'en'; + $user->created_by = creatorId(); + + // Handle profile image upload for user + if ($request->hasFile('profile_image')) { + $filenameWithExt = $request->file('profile_image')->getClientOriginalName(); + $extension = $request->file('profile_image')->getClientOriginalExtension(); + $fileNameToStore = 'avatar_' . time() . '.' . $extension; + $upload = upload_file($request, 'profile_image', $fileNameToStore, 'avatars'); + + if ($upload['status'] == true) { + $user->avatar = $upload['url']; + } else { + \DB::rollBack(); + return redirect()->back() + ->withErrors(['profile_image' => $upload['msg']]) + ->withInput(); + } + } + $user->save(); + + // Assign Employee role + if (isSaaS()) { + $employeeRole = Role::where('created_by', createdBy())->where('name', 'employee')->first(); + if ($employeeRole) { + $user->assignRole($employeeRole); + } + } else { + $employeeRole = Role::where('name', 'employee')->first(); + if ($employeeRole) { + $user->assignRole($employeeRole); + } + } + + // Create Employee model object + $employee = new Employee; + $employee->user_id = $user->id; + $employee->employee_id = Employee::generateEmployeeId(); + $employee->biometric_emp_id = $request->biometric_emp_id; + $employee->phone = $request->phone; + $employee->date_of_birth = $request->date_of_birth; + $employee->gender = $request->gender; + $employee->branch_id = $request->branch_id; + $employee->department_id = $request->department_id; + $employee->designation_id = $request->designation_id; + $employee->date_of_joining = $request->date_of_joining; + $employee->employment_type = $request->employment_type; + $employee->employee_status = $request->employee_status; + $employee->address_line_1 = $request->address_line_1; + $employee->address_line_2 = $request->address_line_2; + $employee->city = $request->city; + $employee->state = $request->state; + $employee->country = $request->country; + $employee->postal_code = $request->postal_code; + $employee->emergency_contact_name = $request->emergency_contact_name; + $employee->emergency_contact_relationship = $request->emergency_contact_relationship; + $employee->emergency_contact_number = $request->emergency_contact_number; + $employee->bank_name = $request->bank_name; + $employee->account_holder_name = $request->account_holder_name; + $employee->account_number = $request->account_number; + $employee->bank_identifier_code = $request->bank_identifier_code; + $employee->bank_branch = $request->bank_branch; + $employee->tax_payer_id = $request->tax_payer_id; + $employee->base_salary = $request->salary; + $employee->created_by = creatorId(); + $employee->save(); + + if (!$employee->save()) { + throw new \Exception('Failed to save employee data'); + } + + // Handle document uploads + if ($request->has('documents') && is_array($request->documents)) { + foreach ($request->documents as $index => $document) { + if ($request->hasFile("documents.$index.file")) { + $file = $request->file("documents.$index.file"); + $extension = $file->getClientOriginalExtension(); + $fileNameToStore = 'document_' . time() . '_' . $index . '.' . $extension; + + $upload = upload_file($request, "documents.$index.file", $fileNameToStore, 'employee_document'); + + if ($upload['status'] == true) { + EmployeeDocument::create([ + 'employee_id' => $employee->user_id, + 'document_type_id' => $document['document_type_id'], + 'file_path' => $upload['url'], + 'expiry_date' => $document['expiry_date'] ?? null, + 'verification_status' => 'pending', + 'created_by' => creatorId(), + ]); + } else { + \DB::rollBack(); + return redirect()->back() + ->withErrors(["documents.$index.file" => $upload['msg']]) + ->withInput(); + } + } + } + } + + // Check if this is a candidate conversion + if ($request->has('candidate_id')) { + $candidate = Candidate::find($request->candidate_id); + if ($candidate) { + $candidate->update(['is_employee' => true]); + } + + \DB::commit(); + return redirect()->route('hr.recruitment.candidates.index')->with('success', __('Candidate converted to employee successfully')); + } + + \DB::commit(); + return redirect()->route('hr.employees.index')->with('success', __('Employee created successfully')); + } catch (\Exception $e) { + \DB::rollBack(); + \Log::error('Employee creation failed: ' . $e->getMessage()); + \Log::error('Stack trace: ' . $e->getTraceAsString()); + + return redirect()->back()->with('error', __('Failed to create employee: :message', ['message' => $e->getMessage()]))->withInput(); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function show(Employee $employee) + { + if (Auth::user()->can('view-employees')) { + // Check if employee belongs to current company + $companyUserIds = getCompanyAndUsersId(); + if (!in_array($employee->created_by, $companyUserIds)) { + return redirect()->back()->with('error', __('You do not have permission to view this employee')); + } + + // Load user with employee relationships + $user = User::with(['employee.branch', 'employee.department', 'employee.designation', 'employee.shift', 'employee.attendancePolicy', 'employee.documents.documentType']) + ->where('id', $employee->user_id) + ->first(); + + $user->avatar = check_file($user->avatar) ? get_file($user->avatar) : get_file('avatars/avatar.png'); + + if ($user->employee && $user->employee->documents) { + $user->employee->documents->transform(function ($document) { + $document->document_url = check_file($document->file_path) ? get_file($document->file_path) : get_file('default/image-not-found.jpg'); + return $document; + }); + } + + return Inertia::render('hr/employees/show', [ + 'employee' => $user, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function edit(Employee $employee) + { + if (Auth::user()->can('edit-employees')) { + // Check if employee belongs to current company + $companyUserIds = getCompanyAndUsersId(); + if (!in_array($employee->created_by, $companyUserIds)) { + return redirect()->back()->with('error', __('You do not have permission to edit this employee')); + } + + // Load user with employee relationships + $user = User::with(['employee.branch', 'employee.department', 'employee.designation', 'employee.documents.documentType']) + ->where('id', $employee->user_id) + ->first(); + + $user->avatar = check_file($user->avatar) ? get_file($user->avatar) : get_file('avatars/avatar.png'); + + if ($user->employee && $user->employee->documents) { + $user->employee->documents->transform(function ($document) { + $document->document_url = check_file($document->file_path) ? get_file($document->file_path) : get_file('default/image-not-found.jpg'); + return $document; + }); + } + + // Get branches, departments, designations, and document types for the form + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name']); + + $departments = Department::with('branch') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'branch_id']); + + $designations = Designation::with('department') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'department_id']); + + $documentTypes = DocumentType::whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name', 'is_required']); + + $shifts = \App\Models\Shift::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'start_time', 'end_time']); + + $attendancePolicies = \App\Models\AttendancePolicy::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name']); + + return Inertia::render('hr/employees/edit', [ + 'employee' => $user, + 'branches' => $branches, + 'departments' => $departments, + 'designations' => $designations, + 'documentTypes' => $documentTypes, + 'shifts' => $shifts, + 'attendancePolicies' => $attendancePolicies, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function update(Request $request, Employee $employee) + { + if (Auth::user()->can('edit-employees')) { + // Check if employee belongs to current company + $companyUserIds = getCompanyAndUsersId(); + if (!in_array($employee->created_by, $companyUserIds)) { + return redirect()->back()->with('error', __('You do not have permission to update this employee')); + } + + try { + // Validate basic information + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'biometric_emp_id' => 'nullable|string|max:255|unique:employees,biometric_emp_id,' . $employee->id, + 'email' => 'required|email|max:255|unique:users,email,' . $employee->user_id, + 'password' => 'nullable|string|min:8', + 'phone' => 'required|string|max:20', + 'date_of_birth' => 'required|date', + 'gender' => 'required|in:male,female,other', + 'profile_image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', + 'shift_id' => 'nullable|exists:shifts,id', + 'attendance_policy_id' => 'nullable|exists:attendance_policies,id', + + // Employment details + 'branch_id' => 'required|exists:branches,id', + 'department_id' => 'required|exists:departments,id', + 'designation_id' => 'required|exists:designations,id', + 'date_of_joining' => 'required|date', + 'employment_type' => 'required|string|max:50', + 'employee_status' => 'required|string|max:50', + + // Contact information + 'address_line_1' => 'required|string|max:255', + 'city' => 'required|string|max:100', + 'state' => 'required|string|max:100', + 'country' => 'required|string|max:100', + 'postal_code' => 'required|string|max:20', + 'emergency_contact_name' => 'required|string|max:255', + 'emergency_contact_relationship' => 'required|string|max:100', + 'emergency_contact_number' => 'required|string|max:20', + + // Banking information + 'bank_name' => 'required|string|max:255', + 'account_holder_name' => 'required|string|max:255', + 'account_number' => 'required|string|max:50', + 'bank_identifier_code' => 'nullable|string|max:50', + 'bank_branch' => 'nullable|string|max:255', + 'tax_payer_id' => 'nullable|string|max:50', + + // Documents + 'documents' => 'nullable|array', + 'documents.*.document_type_id' => 'required|exists:document_types,id', + 'documents.*.file' => 'required|file|mimes:jpeg,png,jpg,pdf,doc,docx|max:5120', + 'documents.*.expiry_date' => 'nullable|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + \DB::beginTransaction(); + + // Get the user + $user = $employee->user; + + // Update User model object + $user->name = $request->name; + $user->email = $request->email; + + // Hash password if provided + if ($request->has('password') && !empty($request->password)) { + $user->password = Hash::make($request->password); + } + + // Handle profile image upload for user + if ($request->hasFile('profile_image')) { + if ($user->avatar && check_file($user->avatar)) { + delete_file($user->avatar); + } + $filenameWithExt = $request->file('profile_image')->getClientOriginalName(); + $extension = $request->file('profile_image')->getClientOriginalExtension(); + $fileNameToStore = 'avatar_' . time() . '.' . $extension; + $upload = upload_file($request, 'profile_image', $fileNameToStore, 'avatars'); + + if ($upload['status'] == true) { + $user->avatar = $upload['url']; + } else { + \DB::rollBack(); + return redirect()->back() + ->withErrors(['profile_image' => $upload['msg']]) + ->withInput(); + } + } + + $user->save(); + + // Update Employee model object + // Keep existing auto-generated employee_id, don't regenerate on update + $employee->biometric_emp_id = $request->biometric_emp_id; + $employee->shift_id = $request->shift_id; + $employee->attendance_policy_id = $request->attendance_policy_id; + $employee->phone = $request->phone; + $employee->date_of_birth = $request->date_of_birth; + $employee->gender = $request->gender; + $employee->branch_id = $request->branch_id; + $employee->department_id = $request->department_id; + $employee->designation_id = $request->designation_id; + $employee->date_of_joining = $request->date_of_joining; + $employee->employment_type = $request->employment_type; + $employee->employee_status = $request->employee_status; + $employee->address_line_1 = $request->address_line_1; + $employee->address_line_2 = $request->address_line_2; + $employee->city = $request->city; + $employee->state = $request->state; + $employee->country = $request->country; + $employee->postal_code = $request->postal_code; + $employee->emergency_contact_name = $request->emergency_contact_name; + $employee->emergency_contact_relationship = $request->emergency_contact_relationship; + $employee->emergency_contact_number = $request->emergency_contact_number; + $employee->bank_name = $request->bank_name; + $employee->account_holder_name = $request->account_holder_name; + $employee->account_number = $request->account_number; + $employee->bank_identifier_code = $request->bank_identifier_code; + $employee->bank_branch = $request->bank_branch; + $employee->tax_payer_id = $request->tax_payer_id; + $employee->base_salary = $request->salary; + + $employee->save(); + + // Handle document uploads + if ($request->has('documents') && is_array($request->documents)) { + foreach ($request->documents as $index => $document) { + if ($request->hasFile("documents.$index.file")) { + $file = $request->file("documents.$index.file"); + $extension = $file->getClientOriginalExtension(); + $fileNameToStore = 'document_' . time() . '_' . $index . '.' . $extension; + + $upload = upload_file($request, "documents.$index.file", $fileNameToStore, 'employee_document'); + + if ($upload['status'] == true) { + EmployeeDocument::create([ + 'employee_id' => $employee->user_id, + 'document_type_id' => $document['document_type_id'], + 'file_path' => $upload['url'], + 'expiry_date' => $document['expiry_date'] ?? null, + 'verification_status' => 'pending', + 'created_by' => creatorId(), + ]); + } else { + \DB::rollBack(); + return redirect()->back() + ->withErrors(["documents.$index.file" => $upload['msg']]) + ->withInput(); + } + } + } + } + + \DB::commit(); + return redirect()->route('hr.employees.index')->with('success', __('Employee updated successfully')); + } catch (\Exception $e) { + \DB::rollBack(); + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update employee')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy($userId) + { + if (Auth::user()->can('delete-employees')) { + try { + $user = User::with('employee')->where('id', $userId)->whereIn('created_by', getCompanyAndUsersId())->first(); + + if (!$user || !$user->employee) { + return redirect()->back()->with('error', __('Employee not found')); + } + + $employee = $user->employee; + + // Delete documents first + $documents = EmployeeDocument::where('employee_id', $user->id)->get(); + foreach ($documents as $doc) { + if ($doc->file_path && check_file($doc->file_path)) { + delete_file($doc->file_path); + } + } + EmployeeDocument::where('employee_id', $user->id)->delete(); + + // Delete employee record + $employee->delete(); + + // Delete user record and avatar + if ($user->avatar && check_file($user->avatar)) { + delete_file($user->avatar); + } + $user->delete(); + + return redirect()->route('hr.employees.index')->with('success', __('Employee deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to delete employee: :message', ['message' => $e->getMessage()])); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function toggleStatus(Employee $employee) + { + if (Auth::user()->can('edit-employees')) { + // Check if employee belongs to current company + $companyUserIds = getCompanyAndUsersId(); + if (!in_array($employee->created_by, $companyUserIds)) { + return redirect()->back()->with('error', __('You do not have permission to update this employee')); + } + + try { + $user = $employee->user; + $newStatus = $user->status === 'active' ? 'inactive' : 'active'; + $user->update(['status' => $newStatus]); + + return redirect()->back()->with('success', __('Employee status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update employee status')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function changePassword(Request $request, Employee $employee) + { + if (Auth::user()->can('edit-employees')) { + // Check if employee belongs to current company + $companyUserIds = getCompanyAndUsersId(); + if (!in_array($employee->created_by, $companyUserIds)) { + return redirect()->back()->with('error', __('You do not have permission to change this employee password')); + } + + try { + $validated = $request->validate([ + 'password' => 'required|string|min:8|confirmed', + ]); + + $user = $employee->user; + $user->password = Hash::make($validated['password']); + $user->save(); + + return redirect()->back()->with('success', __('Employee password changed successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to change employee password')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function deleteDocument($userId, $documentId) + { + $user = User::with('employee')->find($userId); + + if (!$user || !$user->employee) { + return redirect()->back()->with('error', __('Employee not found')); + } + + $companyUserIds = getCompanyAndUsersId(); + if (!in_array($user->created_by, $companyUserIds)) { + return redirect()->back()->with('error', __('You do not have permission to access this employee')); + } + + $document = EmployeeDocument::where('id', $documentId) + ->where('employee_id', $userId) + ->first(); + + if (!$document) { + return redirect()->back()->with('error', __('Document not found')); + } + + try { + if ($document->file_path && check_file($document->file_path)) { + delete_file($document->file_path); + } + + $document->delete(); + + return redirect()->back()->with('success', __('Document deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to delete document')); + } + } + + + public function approveDocument($userId, $documentId) + { + $user = User::with('employee')->find($userId); + if (!$user || !$user->employee) { + return redirect()->back()->with('error', __('Employee not found')); + } + + $document = EmployeeDocument::where('id', $documentId) + ->where('employee_id', $userId) + ->first(); + + if (!$document) { + return redirect()->back()->with('error', __('Document not found')); + } + + try { + $document->update(['verification_status' => 'verified']); + + return redirect()->back()->with('success', __('Document approved successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to approve document')); + } + } + + + public function rejectDocument($userId, $documentId) + { + $user = User::with('employee')->find($userId); + if (!$user || !$user->employee) { + return redirect()->back()->with('error', __('Employee not found')); + } + + $document = EmployeeDocument::where('id', $documentId) + ->where('employee_id', $userId) + ->first(); + + if (!$document) { + return redirect()->back()->with('error', __('Document not found')); + } + + try { + $document->update(['verification_status' => 'rejected']); + + return redirect()->back()->with('success', __('Document rejected successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to reject document')); + } + } + + + public function downloadDocument($userId, $documentId) + { + + $user = User::with('employee')->find($userId); + if (!$user || !$user->employee) { + return redirect()->back()->with('error', __('Employee not found')); + } + + $companyUserIds = getCompanyAndUsersId(); + if (!in_array($user->created_by, $companyUserIds)) { + return redirect()->back()->with('error', __('You do not have permission to access this employee')); + } + + $document = EmployeeDocument::where('id', $documentId) + ->where('employee_id', $userId) + ->first(); + + if (!$document) { + return redirect()->back()->with('error', __('Document not found')); + } + + if (!$document->file_path) { + return redirect()->back()->with('error', __('Document file not found')); + } + + $filePath = getStorageFilePath($document->file_path); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Document file not found')); + } + + return response()->download($filePath); + } + + public function downloadJoiningLetter($employeeId, $format = 'pdf') + { + if (!Auth::user()->can('download-joining-letter')) { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + $user = User::with(['employee.branch', 'employee.department', 'employee.designation']) + ->where('id', $employeeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$user || !$user->employee) { + return redirect()->back()->with('error', __('Employee not found')); + } + + $getCompanyId = getCompanyId(Auth::user()->id); + $template = JoiningLetterTemplate::getTemplate(Auth::user()->lang ?? 'en', $getCompanyId); + if (!$template) { + return redirect()->back()->with('error', __('Template not found')); + } + + $employee = $user->employee; + $companyName = Auth::user()->name ?? 'Company Name'; + // Get variables from template or use defaults + $variables = $template->variables ? json_decode($template->variables, true) : ['date', 'company_name', 'employee_name', 'designation', 'joining_date', 'salary', 'department']; + + $placeholders = []; + foreach ($variables as $variable) { + switch ($variable) { + case 'date': + $placeholders['{date}'] = now()->format('F d, Y'); + break; + case 'company_name': + $placeholders['{company_name}'] = $companyName; + break; + case 'employee_name': + $placeholders['{employee_name}'] = $user->name; + break; + case 'designation': + $placeholders['{designation}'] = $employee->designation->name ?? ''; + break; + case 'joining_date': + $placeholders['{joining_date}'] = $employee->date_of_joining ? date('F d, Y', strtotime($employee->date_of_joining)) : ''; + break; + case 'salary': + $placeholders['{salary}'] = $employee->base_salary ?? ''; + break; + case 'department': + $placeholders['{department}'] = $employee->department->name ?? ''; + break; + case 'leaving_date': + $placeholders['{leaving_date}'] = now()->format('F d, Y'); + break; + } + } + + $content = str_replace(array_keys($placeholders), array_values($placeholders), $template->content); + $content = html_entity_decode($content, ENT_QUOTES, 'UTF-8'); + $content = str_replace('\\n', '
', $content); + + $type = 'joining_letter'; + + return view('employees.certificates.joining-letter', compact('content', 'user', 'type', 'companyName', 'format')); + } + + public function downloadExperienceCertificate($employeeId, $format = 'pdf') + { + if (!Auth::user()->can('download-experience-certificate')) { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + $user = User::with(['employee.branch', 'employee.department', 'employee.designation']) + ->where('id', $employeeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$user || !$user->employee) { + return redirect()->back()->with('error', __('Employee not found')); + } + + $termination = Termination::where('employee_id', $user->id)->where('status', 'completed')->first(); + if ($termination) { + $getCompanyId = getCompanyId(Auth::user()->id); + $template = ExperienceCertificateTemplate::getTemplate(Auth::user()->lang ?? 'en', $getCompanyId); + if (!$template) { + return redirect()->back()->with('error', __('Template not found')); + } + + $employee = $user->employee; + $companyName = Auth::user()->name ?? 'Company Name'; + + $variables = $template->variables ? json_decode($template->variables, true) : ['date', 'company_name', 'employee_name', 'designation', 'joining_date', 'leaving_date']; + + $placeholders = []; + foreach ($variables as $variable) { + switch ($variable) { + case 'date': + $placeholders['{date}'] = now()->format('F d, Y'); + break; + case 'company_name': + $placeholders['{company_name}'] = $companyName; + break; + case 'employee_name': + $placeholders['{employee_name}'] = $user->name; + break; + case 'designation': + $placeholders['{designation}'] = $employee->designation->name ?? ''; + break; + case 'joining_date': + $placeholders['{joining_date}'] = $employee->date_of_joining ? date('F d, Y', strtotime($employee->date_of_joining)) : ''; + break; + case 'leaving_date': + $placeholders['{leaving_date}'] = $termination->termination_date?->format('F d, Y') ?? now()->format('F d, Y'); + break; + case 'salary': + $placeholders['{salary}'] = $employee->base_salary ?? ''; + break; + case 'department': + $placeholders['{department}'] = $employee->department->name ?? ''; + break; + } + } + + $content = str_replace(array_keys($placeholders), array_values($placeholders), $template->content); + $content = html_entity_decode($content, ENT_QUOTES, 'UTF-8'); + $content = str_replace('\\n', '
', $content); + + $type = 'experience_certificate'; + + return view('employees.certificates.experience-certificate', compact('content', 'user', 'type', 'companyName', 'format')); + } else { + return redirect()->back()->with('error', __('Experience certificate can only be generated for employees who have been terminated.')); + } + } + + public function downloadNocCertificate($employeeId, $format = 'pdf') + { + if (!Auth::user()->can('download-noc-certificate')) { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + $user = User::with(['employee.branch', 'employee.department', 'employee.designation']) + ->where('id', $employeeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$user || !$user->employee) { + return redirect()->back()->with('error', __('Employee not found')); + } + + $getCompanyId = getCompanyId(Auth::user()->id); + $template = NocTemplate::getTemplate(Auth::user()->lang ?? 'en', $getCompanyId); + if (!$template) { + return redirect()->back()->with('error', __('Template not found')); + } + + $employee = $user->employee; + $companyName = Auth::user()->name ?? 'Company Name'; + + $variables = $template->variables ? json_decode($template->variables, true) : ['date', 'company_name', 'employee_name', 'designation', 'joining_date', 'department']; + + $placeholders = []; + foreach ($variables as $variable) { + switch ($variable) { + case 'date': + $placeholders['{date}'] = now()->format('F d, Y'); + break; + case 'company_name': + $placeholders['{company_name}'] = $companyName; + break; + case 'employee_name': + $placeholders['{employee_name}'] = $user->name; + break; + case 'designation': + $placeholders['{designation}'] = $employee->designation->name ?? ''; + break; + } + } + + $content = str_replace(array_keys($placeholders), array_values($placeholders), $template->content); + $content = html_entity_decode($content, ENT_QUOTES, 'UTF-8'); + $content = str_replace('\\n', '
', $content); + + $type = 'noc_certificate'; + + return view('employees.certificates.noc-certificate', compact('content', 'user', 'type', 'companyName', 'format')); + } + + public function export() + { + if (Auth::user()->can('export-employee')) { + try { + $employees = User::with(['employee.branch', 'employee.department', 'employee.designation', 'employee.shift', 'employee.attendancePolicy']) + ->where('type', 'employee') + ->where(function ($q) { + if (Auth::user()->can('manage-any-employees')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employees')) { + $q->where('created_by', Auth::id())->orWhere('id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + })->get(); + + $fileName = 'employees_' . date('Y-m-d_His') . '.csv'; + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="' . $fileName . '"', + ]; + + $callback = function () use ($employees) { + $file = fopen('php://output', 'w'); + fputcsv($file, [ + 'Name', + 'Email', + 'Biometric Employee Id', + 'Phone', + 'Department', + 'Designation', + 'Branch', + 'Date of Joining', + 'Date of Birth', + 'Gender', + 'Shift', + 'Basic Salary', + 'Attedance Policy', + 'Employement Type', + 'Employement Status', + 'City', + 'State', + 'Country', + 'Postal Code', + 'Address', + 'Bank Name', + 'Account Number', + 'Bank Identifier Code', + 'Bank Branch', + ]); + + foreach ($employees as $user) { + $employee = $user->employee; + if ($employee) { + fputcsv($file, [ + $user->name, + $user->email, + $employee->biometric_emp_id ?? '', + $employee->phone ?? '', + $employee->department->name ?? '', + $employee->designation->name ?? '', + $employee->branch->name ?? '', + $employee->date_of_joining ?? '', + $employee->date_of_birth ?? '', + $employee->gender ?? '', + $employee->shift->name ?? '', + $employee->base_salary ?? '', + $employee->attendancePolicy->name ?? '', + $employee->employment_type ?? '', + $employee->employee_status ?? 'active', + $employee->city ?? '', + $employee->state ?? '', + $employee->country ?? '', + $employee->postal_code ?? '', + $employee->address_line_1 ?? '', + $employee->bank_name ?? '', + $employee->account_number ?? '', + $employee->bank_identifier_code ?? '', + $employee->bank_branch ?? '', + ]); + } + } + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to export employees: :message', ['message' => $e->getMessage()])], 500); + } + } else { + return response()->json(['message' => __('Permission Denied.')], 403); + } + } + + // download sample data + public function downloadTemplate() + { + $filePath = storage_path('uploads/sample/sample-employee.xlsx'); + if (!file_exists($filePath)) { + return response()->json(['error' => __('Template file not available')], 404); + } + + return response()->download($filePath, 'sample-employee.xlsx'); + } + + public function parseFile(Request $request) + { + if (Auth::user()->can('import-employee')) { + $rules = ['file' => 'required|mimes:csv,txt,xlsx,xls']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return response()->json(['message' => $validator->getMessageBag()->first()]); + } + + try { + $file = $request->file('file'); + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file->getRealPath()); + $worksheet = $spreadsheet->getActiveSheet(); + $highestColumn = $worksheet->getHighestColumn(); + $highestRow = $worksheet->getHighestRow(); + $headers = []; + + for ($col = 'A'; $col <= $highestColumn; $col++) { + $value = $worksheet->getCell($col . '1')->getValue(); + if ($value) { + $headers[] = (string) $value; + } + } + + $previewData = []; + for ($row = 2; $row <= $highestRow; $row++) { + $rowData = []; + $colIndex = 0; + for ($col = 'A'; $col <= $highestColumn; $col++) { + if ($colIndex < count($headers)) { + $rowData[$headers[$colIndex]] = (string) $worksheet->getCell($col . $row)->getValue(); + } + $colIndex++; + } + $previewData[] = $rowData; + } + + return response()->json(['excelColumns' => $headers, 'previewData' => $previewData]); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to parse file: :error', ['error' => $e->getMessage()])]); + } + } else { + return response()->json(['message' => __('Permission denied.')], 403); + + } + } + + public function fileImport(Request $request) + { + if (Auth::user()->can('import-employee')) { + $rules = ['data' => 'required|array']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return redirect()->back()->with('error', $validator->getMessageBag()->first()); + } + + try { + $data = $request->data; + $imported = 0; + $skipped = 0; + + foreach ($data as $row) { + try { + if (empty($row['name']) || empty($row['email'])) { + $skipped++; + + continue; + } + + if (User::where('email', $row['email'])->exists()) { + $skipped++; + + continue; + } + + $password = null; + if (!empty($row['password'])) { + $password = Hash::make($row['password']); + } + + $branchId = null; + if (!empty($row['branch'])) { + $branch = Branch::whereIn('created_by', getCompanyAndUsersId())->where('name', $row['branch'])->first(); + if (is_null($branch)) { + $firstBranch = Branch::whereIn('created_by', getCompanyAndUsersId())->first(); + $branchId = $firstBranch ? $firstBranch->id : null; + } else { + $branchId = $branch ? $branch->id : null; + } + } + + $departmentId = null; + if (!empty($row['department'])) { + $department = Department::whereIn('created_by', getCompanyAndUsersId())->where('name', $row['department'])->first(); + if (is_null($department)) { + if (!is_null($branchId)) { + $department = Department::whereIn('created_by', getCompanyAndUsersId())->where('branch_id', $branchId)->first(); + $departmentId = $department ? $department->id : null; + } else { + $departmentId = null; + } + } else { + $departmentId = $department ? $department->id : null; + } + } + + $designationId = null; + if (!empty($row['designation'])) { + $designation = Designation::whereIn('created_by', getCompanyAndUsersId())->where('name', $row['designation'])->first(); + if (is_null($designation)) { + if (!is_null($departmentId)) { + $designation = Designation::whereIn('created_by', getCompanyAndUsersId())->where('department_id', $departmentId)->first(); + $designationId = $designation ? $designation->id : null; + } else { + $designationId = null; + } + } else { + $designationId = $designation ? $designation->id : null; + } + + } + + $shiftId = null; + if (!empty($row['shift'])) { + $shift = Shift::whereIn('created_by', getCompanyAndUsersId())->where('name', $row['shift'])->first(); + if (is_null($shift)) { + $shift = Shift::whereIn('created_by', getCompanyAndUsersId())->first(); + $shiftId = $shift ? $shift->id : null; + } else { + $shiftId = $shift ? $shift->id : null; + } + } + + $attendancePolicyId = null; + if (!empty($row['attendance_policy'])) { + $attendancePolicy = AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->where('name', $row['attendance_policy'])->first(); + if (is_null($attendancePolicy)) { + $attendancePolicy = AttendancePolicy::whereIn('created_by', getCompanyAndUsersId())->first(); + $attendancePolicyId = $attendancePolicy ? $attendancePolicy->id : null; + } else { + $attendancePolicyId = $attendancePolicy ? $attendancePolicy->id : null; + } + } + + $user = User::create([ + 'name' => $row['name'], + 'email' => $row['email'], + 'password' => $password, + 'type' => 'employee', + 'lang' => 'en', + 'created_by' => creatorId(), + ]); + + if (isSaaS()) { + $employeeRole = Role::whereIn('created_by', getCompanyAndUsersId())->where('name', 'employee')->first(); + } else { + $employeeRole = Role::where('name', 'employee')->first(); + } + + if ($employeeRole) { + $user->assignRole($employeeRole); + } + + Employee::create([ + 'user_id' => $user->id, + 'employee_id' => Employee::generateEmployeeId(), + 'biometric_emp_id' => $row['biometric_emp_id'] ?? null, + 'phone' => $row['phone'] ?? '', + 'date_of_birth' => !empty($row['date_of_birth']) ? $row['date_of_birth'] : null, + 'gender' => $row['gender'] ?? 'male', + 'branch_id' => $branchId, + 'department_id' => $departmentId, + 'designation_id' => $designationId, + 'base_salary' => $row['base_salary'], + 'shift_id' => $shiftId, + 'attendance_policy_id' => $attendancePolicyId, + 'date_of_joining' => !empty($row['date_of_joining']) ? $row['date_of_joining'] : now(), + 'employment_type' => $row['employment_type'] ?? 'full-time', + 'employee_status' => $row['employee_status'] ?? 'active', + 'city' => $row['city'] ?? '', + 'state' => $row['state'] ?? '', + 'country' => $row['country'] ?? '', + 'postal_code' => $row['postal_code'] ?? '', + 'address_line_1' => $row['address'] ?? '', + 'bank_name' => $row['bank_name'] ?? '', + 'account_number' => $row['account_number'] ?? '', + 'bank_identifier_code' => $row['bank_identifier_code'] ?? '', + 'bank_branch' => $row['bank_branch'] ?? '', + 'created_by' => creatorId(), + ]); + + $imported++; + } catch (\Exception $e) { + $skipped++; + } + } + + return redirect()->back()->with('success', __('Import completed: :added employees added, :skipped employees skipped', ['added' => $imported, 'skipped' => $skipped])); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to import: :error', ['error' => $e->getMessage()])); + } + } else { + return redirect()->back()->with('error', __('Permission denied.')); + } + } +} diff --git a/app/Http/Controllers/EmployeeGoalController.php b/app/Http/Controllers/EmployeeGoalController.php new file mode 100644 index 000000000..0c014ab8c --- /dev/null +++ b/app/Http/Controllers/EmployeeGoalController.php @@ -0,0 +1,284 @@ +can('manage-employee-goals')) { + $query = EmployeeGoal::with(['employee', 'goalType']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-employee-goals')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employee-goals')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhere('target', 'like', '%' . $request->search . '%') + ->orWhereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle goal type filter + if ($request->has('goal_type_id') && !empty($request->goal_type_id)) { + $query->where('goal_type_id', $request->goal_type_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['title', 'start_date', 'end_date', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $goals = $query->paginate($request->per_page ?? 10); + + + // Get goal types for filter dropdown + $goalTypes = GoalType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->orderBy('name') + ->get(['id', 'name']); + + return Inertia::render('hr/performance/employee-goals/index', [ + 'goals' => $goals, + 'employees' => $this->getFilteredEmployees(), + 'goalTypes' => $goalTypes, + 'filters' => $request->all(['search', 'employee_id', 'goal_type_id', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-employee-goals') && !Auth::user()->can('manage-any-employee-goals')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + return $employees; + } + public function store(Request $request) + { + if (Auth::user()->can('create-employee-goals')) { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'goal_type_id' => 'required|exists:goal_types,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'target' => 'nullable|string|max:255', + 'progress' => 'nullable|integer|min:0|max:100', + 'status' => 'nullable|string|in:not_started,in_progress,completed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Verify employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected'))->withInput(); + } + + // Verify goal type belongs to current company + $goalType = GoalType::find($request->goal_type_id); + if (!$goalType || !in_array($goalType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid goal type selected'))->withInput(); + } + + EmployeeGoal::create([ + 'employee_id' => $employee->id, + 'goal_type_id' => $request->goal_type_id, + 'title' => $request->title, + 'description' => $request->description, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'target' => $request->target, + 'progress' => $request->progress ?? 0, + 'status' => $request->status ?? 'not_started', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Employee goal created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, EmployeeGoal $employeeGoal) + { + if (Auth::user()->can('edit-employee-goals')) { + // Check if goal belongs to current company + if (!in_array($employeeGoal->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this goal')); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'goal_type_id' => 'required|exists:goal_types,id', + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'target' => 'nullable|string|max:255', + 'progress' => 'nullable|integer|min:0|max:100', + 'status' => 'nullable|string|in:not_started,in_progress,completed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Verify employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected'))->withInput(); + } + + // Verify goal type belongs to current company + $goalType = GoalType::find($request->goal_type_id); + if (!$goalType || !in_array($goalType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid goal type selected'))->withInput(); + } + + $employeeGoal->update([ + 'employee_id' => $employee->id, + 'goal_type_id' => $request->goal_type_id, + 'title' => $request->title, + 'description' => $request->description, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'target' => $request->target, + 'progress' => $request->progress ?? $employeeGoal->progress, + 'status' => $request->status ?? $employeeGoal->status, + ]); + + return redirect()->back()->with('success', __('Employee goal updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(EmployeeGoal $employeeGoal) + { + if (Auth::user()->can('delete-employee-goals')) { + // Check if goal belongs to current company + if (!in_array($employeeGoal->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this goal')); + } + + $employeeGoal->delete(); + + return redirect()->back()->with('success', __('Employee goal deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the progress of the specified resource. + */ + public function updateProgress(Request $request, EmployeeGoal $employeeGoal) + { + if (Auth::user()->can('edit-employee-goals')) { + // Check if goal belongs to current company + if (!in_array($employeeGoal->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this goal')); + } + + $validator = Validator::make($request->all(), [ + 'progress' => 'required|integer|min:0|max:100', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Update progress and status based on progress value + $status = $employeeGoal->status; + if ($request->progress == 100) { + $status = 'completed'; + } elseif ($request->progress > 0) { + $status = 'in_progress'; + } elseif ($request->progress == 0) { + $status = 'not_started'; + } + + $employeeGoal->update([ + 'progress' => $request->progress, + 'status' => $status, + ]); + + return redirect()->back()->with('success', __('Goal progress updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/EmployeeReviewController.php b/app/Http/Controllers/EmployeeReviewController.php new file mode 100644 index 000000000..64695d46c --- /dev/null +++ b/app/Http/Controllers/EmployeeReviewController.php @@ -0,0 +1,495 @@ +can('manage-employee-reviews')) { + $query = EmployeeReview::with(['employee', 'reviewer', 'reviewCycle']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-employee-reviews')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employee-reviews')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }) + ->orWhereHas('reviewer', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle reviewer filter + if ($request->has('reviewer_id') && !empty($request->reviewer_id)) { + $query->where('reviewer_id', $request->reviewer_id); + } + + // Handle review cycle filter + if ($request->has('review_cycle_id') && !empty($request->review_cycle_id)) { + $query->where('review_cycle_id', $request->review_cycle_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('review_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('review_date', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'review_date'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['review_date', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'review_date'; + } + + $query->orderBy($sortField, $sortDirection); + + $reviews = $query->paginate($request->per_page ?? 10); + + $reviews->getCollection()->transform(function ($review) { + if ($review->employee) { + $rawAvatar = $review->employee->getRawOriginal('avatar'); + $review->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + if ($review->reviewer) { + $rawAvatar = $review->reviewer->getRawOriginal('avatar'); + $review->reviewer->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $review; + }); + + // Get review cycles for filter dropdown + $reviewCycles = ReviewCycle::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->orderBy('name') + ->get(['id', 'name']); + + return Inertia::render('hr/performance/employee-reviews/index', [ + 'reviews' => $reviews, + 'employees' => $this->getFilteredEmployees(), + 'reviewCycles' => $reviewCycles, + 'filters' => $request->all(['search', 'employee_id', 'reviewer_id', 'review_cycle_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-employee-reviews') && !Auth::user()->can('manage-any-employee-reviews')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name', 'type') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + 'type' => $user->type, + ]; + }); + return $employees; + } + public function create() + { + if (Auth::user()->can('create-employee-reviews')) { + // Get employees for dropdown + $employees = $this->getFilteredEmployees(); + // Get review cycles for dropdown + $reviewCycles = ReviewCycle::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->orderBy('name') + ->get(['id', 'name']); + + return Inertia::render('hr/performance/employee-reviews/create', [ + 'employees' => $employees, + 'reviewCycles' => $reviewCycles, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-employee-reviews')) { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'reviewer_id' => 'required|exists:users,id', + 'review_cycle_id' => 'required|exists:review_cycles,id', + 'review_date' => 'required|date', + 'status' => 'nullable|string|in:scheduled,in_progress,completed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Verify employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected'))->withInput(); + } + + // Verify reviewer belongs to current company + $reviewer = User::find($request->reviewer_id); + if (!$reviewer || !in_array($reviewer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid reviewer selected'))->withInput(); + } + + // Verify review cycle belongs to current company + $reviewCycle = ReviewCycle::find($request->review_cycle_id); + if (!$reviewCycle || !in_array($reviewCycle->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid review cycle selected'))->withInput(); + } + + // Create the review + $review = EmployeeReview::create([ + 'employee_id' => $request->employee_id, + 'reviewer_id' => $request->reviewer_id, + 'review_cycle_id' => $request->review_cycle_id, + 'review_date' => $request->review_date, + 'status' => $request->status ?? 'scheduled', + 'created_by' => creatorId(), + ]); + + return redirect()->route('hr.performance.employee-reviews.index')->with('success', __('Employee review scheduled successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Display the specified resource. + */ + public function show(EmployeeReview $employeeReview) + { + if (Auth::user()->can('view-employee-reviews')) { + // Check if review belongs to current company + if (!in_array($employeeReview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view this review')); + } + + $employeeReview->load([ + 'employee', + 'reviewer', + 'reviewCycle', + 'ratings.indicator.category' + ]); + + return Inertia::render('hr/performance/employee-reviews/show', [ + 'review' => $employeeReview, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Show the form for conducting a review. + */ + public function conduct(EmployeeReview $employeeReview) + { + if (Auth::user()->can('edit-employee-reviews')) { + // Check if review belongs to current company + if (!in_array($employeeReview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to conduct this review')); + } + + $employeeReview->load([ + 'employee', + 'reviewer', + 'reviewCycle', + 'ratings.indicator' + ]); + + // Get all active performance indicators with their categories + $indicators = PerformanceIndicator::with('category') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get() + ->map(function ($indicator) use ($employeeReview) { + // Check if there's an existing rating for this indicator + $existingRating = $employeeReview->ratings->where('performance_indicator_id', $indicator->id)->first(); + + return [ + 'id' => $indicator->id, + 'name' => $indicator->name, + 'description' => $indicator->description, + 'measurement_unit' => $indicator->measurement_unit, + 'target_value' => $indicator->target_value, + 'category' => $indicator->category ? $indicator->category->name : 'Uncategorized', + 'weight' => 1, // Default weight since templates are removed + 'rating' => $existingRating ? $existingRating->rating : null, + 'comments' => $existingRating ? $existingRating->comments : null, + ]; + }); + + return Inertia::render('hr/performance/employee-reviews/conduct', [ + 'review' => $employeeReview, + 'indicators' => $indicators, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Submit the review ratings. + */ + public function submitRatings(Request $request, EmployeeReview $employeeReview) + { + if (Auth::user()->can('edit-employee-reviews')) { + // Check if review belongs to current company + if (!in_array($employeeReview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this review')); + } + + $validator = Validator::make($request->all(), [ + 'ratings' => 'required|array', + 'ratings.*.indicator_id' => 'required|exists:performance_indicators,id', + 'ratings.*.rating' => 'required|numeric|min:1|max:5', + 'ratings.*.comments' => 'nullable|string', + 'overall_comments' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + try { + DB::beginTransaction(); + + // Delete existing ratings + $employeeReview->ratings()->delete(); + + // Create new ratings + $totalRating = 0; + $ratingCount = 0; + + foreach ($request->ratings as $ratingData) { + // Verify indicator belongs to current company + $indicator = PerformanceIndicator::where('id', $ratingData['indicator_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$indicator) { + continue; + } + + $totalRating += $ratingData['rating']; + $ratingCount++; + + // Create the rating + EmployeeReviewRating::create([ + 'employee_review_id' => $employeeReview->id, + 'performance_indicator_id' => $ratingData['indicator_id'], + 'rating' => $ratingData['rating'], + 'comments' => $ratingData['comments'] ?? null, + ]); + } + + // Calculate overall rating + $overallRating = $ratingCount > 0 ? round($totalRating / $ratingCount, 1) : null; + + // Update the review + $employeeReview->update([ + 'overall_rating' => $overallRating, + 'comments' => $request->overall_comments, + 'status' => 'completed', + 'completion_date' => now(), + ]); + + DB::commit(); + + return redirect()->route('hr.performance.employee-reviews.show', $employeeReview->id) + ->with('success', __('Review completed successfully')); + } catch (\Exception $e) { + DB::rollBack(); + return redirect()->back()->with('error', __('An error occurred while submitting the review: :message', ['message' => $e->getMessage()])); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, EmployeeReview $employeeReview) + { + if (Auth::user()->can('edit-employee-reviews')) { + // Check if review belongs to current company + if (!in_array($employeeReview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this review')); + } + + // Only allow updates if the review is not completed + if ($employeeReview->status === 'completed') { + return redirect()->back()->with('error', __('Cannot update a completed review')); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'reviewer_id' => 'required|exists:users,id', + 'review_cycle_id' => 'required|exists:review_cycles,id', + 'review_date' => 'required|date', + 'status' => 'nullable|string|in:scheduled,in_progress,completed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Verify employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid employee selected')->withInput(); + } + + // Verify reviewer belongs to current company + $reviewer = User::find($request->reviewer_id); + if (!$reviewer || !in_array($reviewer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid reviewer selected')->withInput(); + } + + // Verify review cycle belongs to current company + $reviewCycle = ReviewCycle::find($request->review_cycle_id); + if (!$reviewCycle || !in_array($reviewCycle->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid review cycle selected')->withInput(); + } + + // Update the review + $employeeReview->update([ + 'employee_id' => $request->employee_id, + 'reviewer_id' => $request->reviewer_id, + 'review_cycle_id' => $request->review_cycle_id, + 'review_date' => $request->review_date, + 'status' => $request->status ?? $employeeReview->status, + ]); + + return redirect()->route('hr.performance.employee-reviews.index')->with('success', __('Employee review updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(EmployeeReview $employeeReview) + { + if (Auth::user()->can('delete-employee-reviews')) { + // Check if review belongs to current company + if (!in_array($employeeReview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this review')); + } + + // Only allow deletion if the review is not completed + if ($employeeReview->status === 'completed') { + return redirect()->back()->with('error', __('Cannot delete a completed review')); + } + + // Delete the review (this will also delete the ratings due to cascade) + $employeeReview->delete(); + + return redirect()->back()->with('success', __('Employee review deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the status of the specified resource. + */ + public function updateStatus(Request $request, EmployeeReview $employeeReview) + { + if (Auth::user()->can('edit-employee-reviews')) { + // Check if review belongs to current company + if (!in_array($employeeReview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this review')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:scheduled,in_progress,completed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Update the status + $employeeReview->update([ + 'status' => $request->status, + // If status is completed, set completion date + 'completion_date' => $request->status === 'completed' ? now() : $employeeReview->completion_date, + ]); + + return redirect()->back()->with('success', __('Review status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/EmployeeSalaryController.php b/app/Http/Controllers/EmployeeSalaryController.php new file mode 100644 index 000000000..9926d9bec --- /dev/null +++ b/app/Http/Controllers/EmployeeSalaryController.php @@ -0,0 +1,501 @@ +can('manage-employee-salaries')) { + // Auto-create salary records for employees who don't have one + $companyEmployees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(); + // if (Auth::user()->can('manage-any-employee-salaries')) { + // foreach ($companyEmployees as $employee) { + // $exists = EmployeeSalary::where('employee_id', $employee->id)->exists(); + // if (!$exists) { + // EmployeeSalary::create([ + // 'employee_id' => $employee->id, + // 'basic_salary' => $employee->employee?->base_salary ?? 0, + // 'components' => null, + // 'is_active' => true, + // 'created_by' => creatorId(), + // ]); + // } else { + // if (is_null($employee->employee->base_salary)) { + // // If base salary is null in employee table then it will update the employee salary in employee table + // $getEmployeeBaseSalary = EmployeeSalary::where('employee_id', $employee->employee->user_id)->first(); + // if ($getEmployeeBaseSalary) { + // $employee->employee->base_salary = $getEmployeeBaseSalary->basic_salary; + // $employee->employee->save(); + // } + // } else { + // // If salary update on employee table it will automatically affect on Employee salary table + // $getEmployeeBaseSalary = EmployeeSalary::where('employee_id', $employee->employee->user_id)->first(); + // if ($getEmployeeBaseSalary) { + // $getEmployeeBaseSalary->basic_salary = $employee->employee->base_salary; + // $getEmployeeBaseSalary->save(); + // } + // } + // } + // } + // } + + if (Auth::user()->can('manage-any-employee-salaries')) { + foreach ($companyEmployees as $employee) { + + // Safety check: employee relation must exist + if (!isset($employee->employee)) { + continue; + } + + $employeeModel = $employee->employee; + + // Fetch salary record once + $employeeSalary = EmployeeSalary::where('employee_id', $employee->id)->first(); + + // If salary record does not exist → create + if (!$employeeSalary) { + + EmployeeSalary::create([ + 'employee_id' => $employee->id, + 'basic_salary' => $employeeModel->base_salary ?? 0, + 'components' => null, + 'is_active' => true, + 'created_by' => creatorId(), + ]); + + continue; + } + + // If base_salary is NULL in employee table → update employee table + if (is_null($employeeModel->base_salary)) { + + if (!is_null($employeeSalary->basic_salary)) { + $employeeModel->base_salary = $employeeSalary->basic_salary; + $employeeModel->save(); + } + + } + // If base_salary exists → update salary table + else { + + if ($employeeSalary->basic_salary != $employeeModel->base_salary) { + $employeeSalary->basic_salary = $employeeModel->base_salary; + $employeeSalary->save(); + } + } + } + } + + $query = EmployeeSalary::with(['employee', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-employee-salaries')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employee-salaries')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->where('is_active', 1); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->whereHas('employee', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + + + // Handle active status filter + if ($request->has('is_active') && !empty($request->is_active) && $request->is_active !== 'all') { + $query->where('is_active', $request->is_active === 'active'); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'basic_salary') { + $query->orderBy('basic_salary', $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $employeeSalaries = $query->paginate($request->per_page ?? 10); + + // Load component names and types for each salary record + $employeeSalaries->getCollection()->transform(function ($salary) { + if ($salary->components) { + $components = SalaryComponent::whereIn('id', $salary->components) + ->get(['id', 'name', 'type']); + $salary->component_names = $components->pluck('name')->toArray(); + $salary->component_types = $components->pluck('type')->toArray(); + } else { + $salary->component_names = []; + $salary->component_types = []; + } + if ($salary->employee) { + $rawAvatar = $salary->employee->getRawOriginal('avatar'); + $salary->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $salary; + }); + + + // Get employees for filter dropdown + $employees = User::where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name']); + + // Get salary components for form + $salaryComponents = SalaryComponent::where('status', 'active') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name', 'type', 'calculation_type', 'default_amount', 'percentage_of_basic']); + + return Inertia::render('hr/employee-salaries/index', [ + 'employeeSalaries' => $employeeSalaries, + 'employees' => $this->getFilteredEmployees(), + 'salaryComponents' => $salaryComponents, + 'filters' => $request->all(['search', 'employee_id', 'is_active', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-employee-salaries') && !Auth::user()->can('manage-any-employee-salaries')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + return $employees; + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'basic_salary' => 'required|numeric|min:0', + 'components' => 'nullable|array', + 'components.*' => 'exists:salary_components,id', + 'notes' => 'nullable|string', + ]); + + // Check if employee already has salary + $exists = EmployeeSalary::where('employee_id', $validated['employee_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Employee already has a salary record. Please update the existing one.')); + } + + $validated['created_by'] = creatorId(); + $validated['is_active'] = true; + + EmployeeSalary::create($validated); + + return redirect()->back()->with('success', __('Employee salary created successfully.')); + } + + + + public function update(Request $request, $employeeSalaryId) + { + $employeeSalary = EmployeeSalary::where('id', $employeeSalaryId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($employeeSalary) { + try { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'basic_salary' => 'required|numeric|min:0', + 'components' => 'nullable|array', + 'components.*' => 'exists:salary_components,id', + 'is_active' => 'boolean', + 'notes' => 'nullable|string', + ]); + + $employeeSalary->update($validated); + + return redirect()->back()->with('success', __('Employee salary updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update employee salary')); + } + } else { + return redirect()->back()->with('error', __('Employee salary Not Found.')); + } + } + + public function destroy($employeeSalaryId) + { + $employeeSalary = EmployeeSalary::where('id', $employeeSalaryId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($employeeSalary) { + try { + $employeeSalary->delete(); + return redirect()->back()->with('success', __('Employee salary deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete employee salary')); + } + } else { + return redirect()->back()->with('error', __('Employee salary Not Found.')); + } + } + + public function toggleStatus($employeeSalaryId) + { + $employeeSalary = EmployeeSalary::where('id', $employeeSalaryId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($employeeSalary) { + try { + $employeeSalary->is_active = !$employeeSalary->is_active; + $employeeSalary->save(); + + return redirect()->back()->with('success', __('Employee salary status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update employee salary status')); + } + } else { + return redirect()->back()->with('error', __('Employee salary Not Found.')); + } + } + + public function showPayroll($employeeSalaryId) + { + if (Auth::user()->can('manage-employee-salaries')) { + try { + // $employeeSalary = EmployeeSalary::where('id', $employeeSalaryId) + // ->whereIn('created_by', getCompanyAndUsersId()) + // ->with('employee') + // ->first(); + + $employeeSalary = EmployeeSalary::with(['employee']) + ->where('id',$employeeSalaryId) + ->where(function ($q) { + if (Auth::user()->can('manage-any-employee-salaries')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employee-salaries')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->where('is_active', 1); + } else { + $q->whereRaw('1 = 0'); + } + })->first(); + + if (!$employeeSalary) { + return redirect()->route('hr.employee-salaries.index') + ->with('error', __('Employee salary record not found.')); + } + + // Get payroll runs for this employee + $payrollRuns = \App\Models\PayrollRun::whereIn('created_by', getCompanyAndUsersId()) + ->whereHas('payrollEntries', function ($query) use ($employeeSalary) { + $query->where('employee_id', $employeeSalary->employee_id); + }) + ->orderBy('pay_period_end', 'desc') + ->get(['id', 'title', 'pay_period_start', 'pay_period_end', 'status']); + + if ($payrollRuns->isEmpty()) { + return redirect()->route('hr.employee-salaries.index') + ->with('error', __('No payroll runs found for this employee.')); + } + + // Get the latest payroll run + $latestPayrollRun = $payrollRuns->first(); + + return Inertia::render('hr/employee-salaries/payroll-calculation', [ + 'employeeSalary' => $employeeSalary, + 'payrollRuns' => $payrollRuns, + 'selectedPayrollRun' => $latestPayrollRun, + 'payrollData' => $this->getPayrollCalculationData($employeeSalary, $latestPayrollRun) + ]); + } catch (\Exception $e) { + return redirect()->route('hr.employee-salaries.index') + ->with('error', __('Failed to load payroll calculation.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function getPayrollCalculation($employeeSalaryId, $payrollRunId) + { + try { + $employeeSalary = EmployeeSalary::where('id', $employeeSalaryId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->with('employee') + ->first(); + + $payrollRun = \App\Models\PayrollRun::where('id', $payrollRunId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$employeeSalary || !$payrollRun) { + return response()->json(['error' => 'Record not found'], 404); + } + + $payrollData = $this->getPayrollCalculationData($employeeSalary, $payrollRun); + + return response()->json($payrollData); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to calculate payroll'], 500); + } + } + + private function getPayrollCalculationData($employeeSalary, $payrollRun) + { + // Get payroll entry for this employee and payroll run + $payrollEntry = \App\Models\PayrollEntry::where('employee_id', $employeeSalary->employee_id) + ->where('payroll_run_id', $payrollRun->id) + ->first(); + + if (!$payrollEntry) { + return [ + 'payrollEntry' => null, + 'salaryBreakdown' => ['earnings' => [], 'deductions' => []], + 'attendanceSummary' => [], + 'payrollCalculation' => ['net_salary' => 0, 'total_earnings' => 0, 'total_deductions' => 0], + 'attendanceRecords' => [] + ]; + } + + // Get attendance records for the payroll period + $attendanceRecords = \App\Models\AttendanceRecord::where('employee_id', $employeeSalary->employee_id) + ->whereBetween('date', [$payrollRun->pay_period_start, $payrollRun->pay_period_end]) + ->orderBy('date') + ->get(); + + // Calculate attendance summary from payroll entry + $attendanceSummary = [ + 'total_working_days' => $payrollEntry->working_days, + 'present_days' => $payrollEntry->present_days, + 'absent_days' => $payrollEntry->absent_days, + 'half_days' => $payrollEntry->half_days, + 'leave_days' => $payrollEntry->paid_leave_days, + 'holiday_days' => $payrollEntry->holiday_days, + 'overtime_hours' => $payrollEntry->overtime_hours, + 'unpaid_leave_days' => $payrollEntry->unpaid_leave_days, + 'unpaid_leave_from_leave' => $payrollEntry->unpaid_leave_days - $payrollEntry->absent_days - ($payrollEntry->half_days * 0.5) + ]; + + // Get salary breakdown from payroll entry + $salaryBreakdown = [ + 'earnings' => is_array($payrollEntry->earnings_breakdown) ? $payrollEntry->earnings_breakdown : json_decode($payrollEntry->earnings_breakdown ?? '{}', true), + 'deductions' => is_array($payrollEntry->deductions_breakdown) ? $payrollEntry->deductions_breakdown : json_decode($payrollEntry->deductions_breakdown ?? '{}', true) + ]; + + $payrollCalculation = [ + 'net_salary' => $payrollEntry->net_pay, + 'total_earnings' => $payrollEntry->total_earnings, + 'total_deductions' => $payrollEntry->total_deductions, + 'per_day_salary' => $payrollEntry->per_day_salary ?? 0, + 'overtime_amount' => $payrollEntry->overtime_amount ?? 0 + ]; + + return [ + 'payrollEntry' => $payrollEntry, + 'salaryBreakdown' => $salaryBreakdown, + 'attendanceSummary' => $attendanceSummary, + 'payrollCalculation' => $payrollCalculation, + 'attendanceRecords' => $attendanceRecords, + 'currentMonth' => $payrollRun->pay_period_end + ]; + } + + private function calculateAttendanceSummary($attendanceRecords, $payrollRun) + { + $summary = [ + 'total_working_days' => 0, + 'present_days' => 0, + 'absent_days' => 0, + 'half_days' => 0, + 'leave_days' => 0, + 'holiday_days' => 0, + 'overtime_hours' => 0, + 'unpaid_leave_days' => 0, + 'unpaid_leave_from_leave' => 0 + ]; + + foreach ($attendanceRecords as $record) { + switch ($record->status) { + case 'present': + $summary['present_days']++; + break; + case 'absent': + $summary['absent_days']++; + break; + case 'half_day': + $summary['half_days']++; + break; + case 'on_leave': + $summary['leave_days']++; + break; + case 'holiday': + $summary['holiday_days']++; + break; + } + + if ($record->overtime_hours > 0) { + $summary['overtime_hours'] += $record->overtime_hours; + } + } + + // Calculate total working days (excluding holidays) + $summary['total_working_days'] = $summary['present_days'] + $summary['absent_days'] + $summary['half_days'] + $summary['leave_days']; + + // Calculate unpaid leave days + $summary['unpaid_leave_days'] = $summary['absent_days'] + ($summary['half_days'] * 0.5); + + return $summary; + } +} diff --git a/app/Http/Controllers/EmployeeTrainingController.php b/app/Http/Controllers/EmployeeTrainingController.php new file mode 100644 index 000000000..e1c95277d --- /dev/null +++ b/app/Http/Controllers/EmployeeTrainingController.php @@ -0,0 +1,496 @@ +can('manage-employee-trainings')) { + $query = EmployeeTraining::with(['employee.employee', 'trainingProgram.trainingType']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-employee-trainings')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employee-trainings')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%'.$request->search.'%') + ->orWhere('employee_id', 'like', '%'.$request->search.'%'); + }) + ->orWhereHas('trainingProgram', function ($q) use ($request) { + $q->where('name', 'like', '%'.$request->search.'%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && ! empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle program filter + if ($request->has('training_program_id') && ! empty($request->training_program_id)) { + $query->where('training_program_id', $request->training_program_id); + } + + // Handle status filter + if ($request->has('status') && ! empty($request->status)) { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('assigned_date_from') && ! empty($request->assigned_date_from)) { + $query->whereDate('assigned_date', '>=', $request->assigned_date_from); + } + if ($request->has('assigned_date_to') && ! empty($request->assigned_date_to)) { + $query->whereDate('assigned_date', '<=', $request->assigned_date_to); + } + + // Handle sorting + $allowedSortFields = ['id', 'status', 'assigned_date', 'completion_date', 'score', 'created_at']; + + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + + if ($sortField === 'employee_name' || $sortField === 'employee') { + $query->join('users', 'employee_trainings.employee_id', '=', 'users.id') + ->select('employee_trainings.*') + ->orderBy('users.name', $sortDirection); + } elseif ($sortField === 'program_name') { + $query->join('training_programs', 'employee_trainings.training_program_id', '=', 'training_programs.id') + ->select('employee_trainings.*') + ->orderBy('training_programs.name', $sortDirection); + } elseif (in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + // Add assessment results count + $query->withCount(['assessmentResults']); + + $employeeTrainings = $query->paginate($request->per_page ?? 10); + + $employeeTrainings->getCollection()->transform(function ($training) { + if ($training->employee) { + $rawAvatar = $training->employee->getRawOriginal('avatar'); + $training->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $training; + }); + + // Get employees for filter dropdown + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name.' ('.($user->employee->employee_id ?? '').')', + ]; + }); + + // Get training programs for filter dropdown + $trainingPrograms = TrainingProgram::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/training/employee-trainings/index', [ + 'employeeTrainings' => $employeeTrainings, + 'employees' => $employees, + 'trainingPrograms' => $trainingPrograms, + 'filters' => $request->all(['search', 'employee_id', 'training_program_id', 'status', 'assigned_date_from', 'assigned_date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Display the dashboard view. + */ + public function dashboard(Request $request) + { + // Get training statistics with proper permission check + $totalTrainings = EmployeeTraining::withPermissionCheck()->count(); + $completedTrainings = EmployeeTraining::withPermissionCheck()->where('status', 'completed')->count(); + $inProgressTrainings = EmployeeTraining::withPermissionCheck()->where('status', 'in_progress')->count(); + $assignedTrainings = EmployeeTraining::withPermissionCheck()->where('status', 'assigned')->count(); + $failedTrainings = EmployeeTraining::withPermissionCheck()->where('status', 'failed')->count(); + + // Get completion rate by program + $programStats = TrainingProgram::whereIn('created_by', getCompanyAndUsersId()) + ->withCount([ + 'employeeTrainings as total_count' => function ($q) { + $q->whereHas('employee', function ($q) { + $q->whereIn('created_by', getCompanyAndUsersId()); + }); + }, + 'employeeTrainings as completed_count' => function ($q) { + $q->where('status', 'completed') + ->whereHas('employee', function ($q) { + $q->whereIn('created_by', getCompanyAndUsersId()); + }); + }, + ]) + ->having('total_count', '>', 0) + ->get() + ->map(function ($program) { + return [ + 'name' => $program->name, + 'total' => $program->total_count, + 'completed' => $program->completed_count, + 'completion_rate' => $program->total_count > 0 + ? round(($program->completed_count / $program->total_count) * 100) + : 0, + ]; + }); + + // Get recent completions + $recentCompletions = EmployeeTraining::with(['employee', 'trainingProgram']) + ->withPermissionCheck() + ->where('status', 'completed') + ->orderBy('completion_date', 'desc') + ->take(5) + ->get(); + + // Get upcoming trainings + $upcomingTrainings = EmployeeTraining::with(['employee', 'trainingProgram']) + ->withPermissionCheck() + ->where('status', 'assigned') + ->orderBy('assigned_date', 'asc') + ->take(5) + ->get(); + + return Inertia::render('hr/training/employee-trainings/dashboard', [ + 'statistics' => [ + 'totalTrainings' => $totalTrainings, + 'completedTrainings' => $completedTrainings, + 'inProgressTrainings' => $inProgressTrainings, + 'assignedTrainings' => $assignedTrainings, + 'failedTrainings' => $failedTrainings, + 'completionRate' => $totalTrainings > 0 ? round(($completedTrainings / $totalTrainings) * 100) : 0, + ], + 'programStats' => $programStats, + 'recentCompletions' => $recentCompletions, + 'upcomingTrainings' => $upcomingTrainings, + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'training_program_id' => 'required|exists:training_programs,id', + 'status' => 'required|string|in:assigned,in_progress,completed,failed', + 'assigned_date' => 'required|date', + 'completion_date' => 'nullable|date|after_or_equal:assigned_date', + 'certification' => 'nullable|string', + 'score' => 'nullable|numeric|min:0|max:100', + 'is_passed' => 'nullable|boolean', + 'feedback' => 'nullable|string', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $user = User::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (! $user) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Check if training program belongs to current company + $trainingProgram = TrainingProgram::find($request->training_program_id); + if (! $trainingProgram || ! in_array($trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid training program selected')); + } + + $trainingData = [ + 'employee_id' => $request->employee_id, + 'training_program_id' => $request->training_program_id, + 'status' => $request->status, + 'assigned_date' => $request->assigned_date, + 'completion_date' => $request->completion_date, + 'score' => $request->score, + 'is_passed' => $request->is_passed, + 'feedback' => $request->feedback, + 'notes' => $request->notes, + 'assigned_by' => creatorId(), + 'created_by' => creatorId(), + ]; + + // Handle certification from media library + if ($request->has('certification')) { + $trainingData['certification'] = $request->certification; + } + + EmployeeTraining::create($trainingData); + + return redirect()->back()->with('success', __('Employee training assigned successfully')); + } + + /** + * Display the specified resource. + */ + public function show(EmployeeTraining $employeeTraining) + { + // Check if employee training belongs to current company + if (! in_array($employeeTraining->employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view this employee training')); + } + + // Load relationships + $employeeTraining->load([ + 'employee.employee.department', + 'employee.employee.designation', + 'trainingProgram.trainingType', + 'assessmentResults.trainingAssessment', + 'assigner', + ]); + + // Get available assessments for this training program + $availableAssessments = TrainingAssessment::where('training_program_id', $employeeTraining->training_program_id) + ->whereDoesntHave('employeeResults', function ($q) use ($employeeTraining) { + $q->where('employee_training_id', $employeeTraining->id); + }) + ->get(); + + return Inertia::render('hr/training/employee-trainings/show', [ + 'employeeTraining' => $employeeTraining, + 'availableAssessments' => $availableAssessments, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, EmployeeTraining $employeeTraining) + { + // Check if employee training belongs to current company + if (! in_array($employeeTraining->employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this employee training')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:assigned,in_progress,completed,failed', + 'completion_date' => 'nullable|date|after_or_equal:assigned_date', + 'certification' => 'nullable|string', + 'score' => 'nullable|numeric|min:0|max:100', + 'is_passed' => 'nullable|boolean', + 'feedback' => 'nullable|string', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $trainingData = [ + 'status' => $request->status, + 'completion_date' => $request->completion_date, + 'score' => $request->score, + 'is_passed' => $request->is_passed, + 'feedback' => $request->feedback, + 'notes' => $request->notes, + ]; + + // Handle certification from media library + if ($request->has('certification')) { + $trainingData['certification'] = $request->certification; + } + + $employeeTraining->update($trainingData); + + return redirect()->back()->with('success', __('Employee training updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(EmployeeTraining $employeeTraining) + { + // Check if employee training belongs to current company + if (! in_array($employeeTraining->employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this employee training')); + } + + // Delete certification if exists + if ($employeeTraining->certification) { + Storage::disk('public')->delete($employeeTraining->certification); + } + + // Delete assessment results + $employeeTraining->assessmentResults()->delete(); + + // Delete the employee training + $employeeTraining->delete(); + + return redirect()->back()->with('success', __('Employee training deleted successfully')); + } + + /** + * Download certification file. + */ + public function downloadCertification(EmployeeTraining $employeeTraining) + { + // Check if employee training belongs to current company + if (! in_array($employeeTraining->employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this certification')); + } + + if (! $employeeTraining->certification) { + return redirect()->back()->with('error', __('Certification file not found')); + } + + $filePath = getStorageFilePath($employeeTraining->certification); + + if (! file_exists($filePath)) { + return redirect()->back()->with('error', __('Certification file not found')); + } + + return response()->download($filePath); + } + + /** + * Bulk assign training to employees. + */ + public function bulkAssign(Request $request) + { + $validator = Validator::make($request->all(), [ + 'employee_ids' => 'required|array', + 'employee_ids.*' => 'exists:users,id', + 'training_program_id' => 'required|exists:training_programs,id', + 'assigned_date' => 'required|date', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employees belong to current company + $employeeIds = $request->employee_ids; + $validEmployees = User::whereIn('id', $employeeIds) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->pluck('id') + ->toArray(); + + if (count($validEmployees) !== count($employeeIds)) { + return redirect()->back()->with('error', __('Invalid employee selection')); + } + + // Check if training program belongs to current company + $trainingProgram = TrainingProgram::find($request->training_program_id); + if (! $trainingProgram || ! in_array($trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid training program selected')); + } + + // Create training assignments for each employee + foreach ($employeeIds as $employeeId) { + EmployeeTraining::create([ + 'employee_id' => $employeeId, + 'training_program_id' => $request->training_program_id, + 'status' => 'assigned', + 'assigned_date' => $request->assigned_date, + 'notes' => $request->notes, + 'assigned_by' => creatorId(), + 'created_by' => creatorId(), + ]); + } + + return redirect()->back()->with('success', __('Training assigned to '.count($employeeIds).' employees successfully')); + } + + /** + * Record assessment result. + */ + public function recordAssessment(Request $request, EmployeeTraining $employeeTraining) + { + // Check if employee training belongs to current company + if (! in_array($employeeTraining->employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to record assessment for this employee training')); + } + + $validator = Validator::make($request->all(), [ + 'training_assessment_id' => 'required|exists:training_assessments,id', + 'score' => 'required|numeric|min:0|max:100', + 'is_passed' => 'required|boolean', + 'feedback' => 'nullable|string', + 'assessment_date' => 'required|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if assessment belongs to the training program + $assessment = TrainingAssessment::find($request->training_assessment_id); + if (! $assessment || $assessment->training_program_id != $employeeTraining->training_program_id) { + return redirect()->back()->with('error', __('Invalid assessment selected')); + } + + // Create assessment result + EmployeeAssessmentResult::create([ + 'employee_training_id' => $employeeTraining->id, + 'training_assessment_id' => $request->training_assessment_id, + 'score' => $request->score, + 'is_passed' => $request->is_passed, + 'feedback' => $request->feedback, + 'assessment_date' => $request->assessment_date, + 'assessed_by' => auth()->id(), + ]); + + // Update employee training status if needed + if ($request->update_training_status) { + $employeeTraining->update([ + 'status' => $request->is_passed ? 'completed' : 'failed', + 'is_passed' => $request->is_passed, + 'score' => $request->score, + 'completion_date' => $request->assessment_date, + ]); + } + + return redirect()->back()->with('success', __('Assessment result recorded successfully')); + } +} diff --git a/app/Http/Controllers/EmployeeTransferController.php b/app/Http/Controllers/EmployeeTransferController.php new file mode 100644 index 000000000..0e734c433 --- /dev/null +++ b/app/Http/Controllers/EmployeeTransferController.php @@ -0,0 +1,529 @@ +can('manage-employee-transfers')) { + $query = EmployeeTransfer::with([ + 'employee', + 'fromBranch:id,name', + 'toBranch:id,name', + 'fromDepartment:id,name', + 'toDepartment:id,name', + 'fromDesignation:id,name', + 'toDesignation:id,name', + 'approver' + ])->where(function ($q) { + + if (Auth::user()->can('manage-any-employee-transfers')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-employee-transfers')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }) + ->orWhere('reason', 'like', '%' . $request->search . '%') + ->orWhere('notes', 'like', '%' . $request->search . '%'); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle branch filter + if ($request->has('branch_id') && !empty($request->branch_id)) { + $query->where(function ($q) use ($request) { + $q->where('from_branch_id', $request->branch_id) + ->orWhere('to_branch_id', $request->branch_id); + }); + } + + // Handle department filter + if ($request->has('department_id') && !empty($request->department_id)) { + $query->where(function ($q) use ($request) { + $q->where('from_department_id', $request->department_id) + ->orWhere('to_department_id', $request->department_id); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('transfer_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('transfer_date', '<=', $request->date_to); + } + + // Handle sorting + $allowedSortFields = ['id', 'employee_id', 'transfer_date', 'effective_date', 'status', 'from_branch_id', 'to_branch_id', 'from_department_id', 'to_department_id']; + if ($request->has('sort_field') && !empty($request->sort_field) && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + + $transfers = $query->paginate($request->per_page ?? 10); + + $transfers->getCollection()->transform(function ($transfer) { + if ($transfer->employee) { + $rawAvatar = $transfer->employee->getRawOriginal('avatar'); + $transfer->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $transfer; + }); + + // Get employees for filter dropdown + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name', 'type') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + 'type' => $user->type, + ]; + }); + + // Get branches for filter dropdown + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get departments for filter dropdown + $departments = Department::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name', 'branch_id') + ->get(); + + // Get designations for form dropdown + $designations = \App\Models\Designation::whereIn('created_by', getCompanyAndUsersId()) + ->with('department:id,name,branch_id') + ->select('id', 'name', 'department_id') + ->get(); + + return Inertia::render('hr/transfers/index', [ + 'transfers' => $transfers, + 'employees' => $this->getFilteredEmployees(), + 'branches' => $branches, + 'departments' => $departments, + 'designations' => $designations, + 'filters' => $request->all(['search', 'employee_id', 'branch_id', 'department_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-employee-transfers') && !Auth::user()->can('manage-any-employee-transfers')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + return $employees; + } + + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'to_branch_id' => 'nullable|exists:branches,id', + 'to_department_id' => 'nullable|exists:departments,id', + 'to_designation_id' => 'nullable|exists:designations,id', + 'transfer_date' => 'required|date', + 'effective_date' => 'required|date|after_or_equal:transfer_date', + 'reason' => 'nullable|string', + 'documents' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $employee = User::with('employee')->find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Ensure at least one transfer destination is specified + if (empty($request->to_branch_id) && empty($request->to_department_id) && empty($request->to_designation_id)) { + return redirect()->back()->with('error', __('At least one transfer destination (branch, department, or designation) must be specified')); + } + + // Get current employee details + $currentBranchId = $employee->employee->branch_id; + $currentDepartmentId = $employee->employee->department_id; + $currentDesignationId = $employee->employee->designation_id; + + $transferData = [ + 'employee_id' => $request->employee_id, + 'transfer_date' => $request->transfer_date, + 'effective_date' => $request->effective_date, + 'reason' => $request->reason, + 'status' => 'pending', + 'created_by' => creatorId(), + ]; + + // Set from and to branch IDs if branch transfer + if ($request->to_branch_id) { + $transferData['from_branch_id'] = $currentBranchId; + $transferData['to_branch_id'] = $request->to_branch_id; + } + + // Set from and to department IDs if department transfer + if ($request->to_department_id) { + $transferData['from_department_id'] = $currentDepartmentId; + $transferData['to_department_id'] = $request->to_department_id; + } + + // Set from and to designation IDs if designation transfer + if ($request->to_designation_id) { + $transferData['from_designation_id'] = $currentDesignationId; + $transferData['to_designation_id'] = $request->to_designation_id; + } + + // Handle document from media library + if ($request->has('documents')) { + $transferData['documents'] = $request->documents; + } + + EmployeeTransfer::create($transferData); + + return redirect()->back()->with('success', __('Transfer request created successfully')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, EmployeeTransfer $transfer) + { + // Check if transfer belongs to current company + if (!in_array($transfer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this transfer'); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'to_branch_id' => 'nullable|exists:branches,id', + 'to_department_id' => 'nullable|exists:departments,id', + 'to_designation_id' => 'nullable|exists:designations,id', + 'transfer_date' => 'required|date', + 'effective_date' => 'required|date|after_or_equal:transfer_date', + 'reason' => 'nullable|string', + 'status' => 'nullable|string|in:pending,approved,rejected', + 'documents' => 'nullable|string', + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Ensure at least one transfer destination is specified + if (empty($request->to_branch_id) && empty($request->to_department_id) && empty($request->to_designation_id)) { + return redirect()->back()->with('error', __('At least one transfer destination (branch, department, or designation) must be specified')); + } + + $transferData = [ + 'employee_id' => $request->employee_id, + 'transfer_date' => $request->transfer_date, + 'effective_date' => $request->effective_date, + 'reason' => $request->reason, + 'status' => $request->status ?? $transfer->status, + 'notes' => $request->notes, + ]; + + // Set from and to branch IDs if branch transfer + if ($request->to_branch_id) { + $transferData['to_branch_id'] = $request->to_branch_id; + } + + // Set from and to department IDs if department transfer + if ($request->to_department_id) { + $transferData['to_department_id'] = $request->to_department_id; + } + + // Set from and to designation IDs if designation transfer + if ($request->to_designation_id) { + $transferData['to_designation_id'] = $request->to_designation_id; + } + + // Handle document from media library + if ($request->has('documents')) { + $transferData['documents'] = $request->documents; + } + + // If status is being changed to approved or rejected, set approved_by and approved_at + if ($request->has('status') && in_array($request->status, ['approved', 'rejected']) && $transfer->status === 'pending') { + $transferData['approved_by'] = auth()->id(); + $transferData['approved_at'] = now(); + + // If approved and effective date has passed or is today, update employee details + if ($request->status === 'approved') { + $user = User::with('employee')->find($request->employee_id); + + if ($user && $user->employee) { + if (isset($transferData['to_branch_id'])) { + $user->employee->branch_id = $transferData['to_branch_id']; + } + if (isset($transferData['to_department_id'])) { + $user->employee->department_id = $transferData['to_department_id']; + } + if (isset($transferData['to_designation_id'])) { + $user->employee->designation_id = $transferData['to_designation_id']; + } + + $user->employee->save(); + } + } + } + + $transfer->update($transferData); + + return redirect()->back()->with('success', __('Transfer updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(EmployeeTransfer $transfer) + { + // Check if transfer belongs to current company + if (!in_array($transfer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this transfer'); + } + + // Only allow deletion of pending transfers + if ($transfer->status !== 'pending') { + return redirect()->back()->with('error', 'Only pending transfers can be deleted'); + } + + $transfer->delete(); + + return redirect()->back()->with('success', __('Transfer deleted successfully')); + } + + /** + * Approve the transfer. + */ + public function approve(Request $request, EmployeeTransfer $transfer) + { + // Check if transfer belongs to current company + if (!in_array($transfer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to approve this transfer'); + } + + // Only allow approval of pending transfers + if ($transfer->status !== 'pending') { + return redirect()->back()->with('error', 'Only pending transfers can be approved'); + } + + $validator = Validator::make($request->all(), [ + 'notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $updateData = [ + 'status' => 'approved', + 'approved_by' => auth()->id(), + 'approved_at' => now(), + 'notes' => $request->notes, + ]; + + $transfer->update($updateData); + + // If effective date has passed or is today, update employee details + + $user = User::with('employee')->find($transfer->employee_id); + + if ($user && $user->employee) { + if ($transfer->to_branch_id) { + $user->employee->branch_id = $transfer->to_branch_id; + } + if ($transfer->to_department_id) { + $user->employee->department_id = $transfer->to_department_id; + } + if ($transfer->to_designation_id) { + $user->employee->designation_id = $transfer->to_designation_id; + } + + $user->employee->save(); + } + + + return redirect()->back()->with('success', __('Transfer approved successfully')); + } + + /** + * Reject the transfer. + */ + public function reject(Request $request, EmployeeTransfer $transfer) + { + // Check if transfer belongs to current company + if (!in_array($transfer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to reject this transfer'); + } + + // Only allow rejection of pending transfers + if ($transfer->status !== 'pending') { + return redirect()->back()->with('error', 'Only pending transfers can be rejected'); + } + + $validator = Validator::make($request->all(), [ + 'notes' => 'required|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $transfer->update([ + 'status' => 'rejected', + 'approved_by' => auth()->id(), + 'approved_at' => now(), + 'notes' => $request->notes, + ]); + + return redirect()->back()->with('success', __('Transfer rejected successfully')); + } + + /** + * Download document file. + */ + public function downloadDocument(EmployeeTransfer $transfer) + { + // Check if transfer belongs to current company + if (!in_array($transfer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this document')); + } + + if (!$transfer->documents) { + return redirect()->back()->with('error', __('Document file not found')); + } + + $filePath = getStorageFilePath($transfer->documents); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Document file not found')); + } + + return response()->download($filePath); + } + + public function getDepartment($branchId) + { + try { + $branch = Branch::with('departments')->find($branchId); + + if (!$branch) { + return response()->json(['error' => 'Branch not found'], 404); + } + + // Map departments into dropdown-friendly format + $departmentsForDropdown = $branch->departments->map(function ($department) { + return [ + 'label' => $department->name, + 'value' => $department->id, + ]; + }); + + return response()->json($departmentsForDropdown); + } catch (\Exception $e) { + return response()->json(['error' => $e->getMessage()], 500); + } + } + + public function getDesignation($departmentId) + { + try { + $department = Department::with('desginations')->find($departmentId); + + if (!$department) { + return response()->json(['error' => 'Department not found'], 404); + } + + // Map departments into dropdown-friendly format + $designationDropdown = $department->desginations->map(function ($designation) { + return [ + 'label' => $designation->name, + 'value' => $designation->id, + ]; + }); + + return response()->json($designationDropdown); + } catch (\Exception $e) { + return response()->json(['error' => $e->getMessage()], 500); + } + } +} diff --git a/app/Http/Controllers/ExperienceCertificateTemplateController.php b/app/Http/Controllers/ExperienceCertificateTemplateController.php new file mode 100644 index 000000000..a44b17ae9 --- /dev/null +++ b/app/Http/Controllers/ExperienceCertificateTemplateController.php @@ -0,0 +1,43 @@ +can('update-experience-certificate')) { + $request->validate([ + 'content' => 'required|string' + ]); + + if ($request->templateId) { + // Update existing template + $template = ExperienceCertificateTemplate::where('id', $request->templateId) + ->where('created_by', auth::id()) + ->firstOrFail(); + $template->update(['content' => $request->content]); + } else { + // Create or update by language + $template = ExperienceCertificateTemplate::updateOrCreate( + [ + 'language' => $request->language, + 'created_by' => auth::id() + ], + [ + 'content' => $request->content + ] + ); + } + + return redirect()->back()->with('success', __('Experience Certificate template updated successfully.')); + + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/FedaPayPaymentController.php b/app/Http/Controllers/FedaPayPaymentController.php new file mode 100644 index 000000000..2d35944e3 --- /dev/null +++ b/app/Http/Controllers/FedaPayPaymentController.php @@ -0,0 +1,134 @@ + 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['fedapay_secret_key'])) { + return back()->withErrors(['error' => 'FedaPay not configured']); + } + + $this->configureFedaPay($settings['payment_settings']); + + $transaction = Transaction::retrieve($validated['transaction_id']); + + if ($transaction->status === 'approved') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'fedapay', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['transaction_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return back()->withErrors(['error' => __('Payment processing failed')]); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['fedapay_secret_key'])) { + return response()->json(['error' => __('FedaPay not configured')], 400); + } + + $this->configureFedaPay($settings['payment_settings']); + + $user = auth()->user(); + + $transaction = Transaction::create([ + 'description' => 'Plan: ' . $plan->name, + 'amount' => $pricing['final_price'] * 100, // Amount in cents + 'currency' => ['iso' => 'XOF'], + 'callback_url' => route('fedapay.callback'), + 'customer' => [ + 'firstname' => $user->name ?? 'Customer', + 'email' => $user->email, + ], + 'custom_metadata' => [ + 'plan_id' => $plan->id, + 'user_id' => $user->id, + 'billing_cycle' => $validated['billing_cycle'], + 'coupon_code' => $validated['coupon_code'] ?? null, + ] + ]); + + $token = $transaction->generateToken(); + + return response()->json([ + 'success' => true, + 'payment_url' => $token->url, + 'transaction_id' => $transaction->id, + 'token' => $token->token + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function callback(Request $request) + { + try { + $settings = getPaymentGatewaySettings(); + $this->configureFedaPay($settings['payment_settings']); + + $transactionId = $request->input('id'); + $transaction = Transaction::retrieve($transactionId); + + if ($transaction->status === 'approved') { + $metadata = $transaction->custom_metadata; + + processPaymentSuccess([ + 'user_id' => $metadata['user_id'], + 'plan_id' => $metadata['plan_id'], + 'billing_cycle' => $metadata['billing_cycle'], + 'payment_method' => 'fedapay', + 'coupon_code' => $metadata['coupon_code'] ?? null, + 'payment_id' => $transactionId, + ]); + + return redirect()->route('plans.index')->with('success', __('Payment successful and plan activated')); + } + + return redirect()->route('plans.index')->with('error', __('Payment was not completed')); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } + + private function configureFedaPay($settings) + { + FedaPay::setApiKey($settings['fedapay_secret_key']); + FedaPay::setEnvironment($settings['fedapay_mode'] === 'live' ? 'live' : 'sandbox'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/FlutterwavePaymentController.php b/app/Http/Controllers/FlutterwavePaymentController.php new file mode 100644 index 000000000..03482beb0 --- /dev/null +++ b/app/Http/Controllers/FlutterwavePaymentController.php @@ -0,0 +1,77 @@ + 'required|string', + 'tx_ref' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['flutterwave_secret_key'])) { + return back()->withErrors(['error' => __('Flutterwave not configured')]); + } + + // Verify payment with Flutterwave API + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.flutterwave.com/v3/transactions/" . $validated['payment_id'] . "/verify", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + "Authorization: Bearer " . $settings['payment_settings']['flutterwave_secret_key'], + "Content-Type: application/json", + ], + )); + + $response = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + + if ($httpCode !== 200) { + return back()->withErrors(['error' => __('Payment verification failed - API error')]); + } + + $result = json_decode($response, true); + + if (!$result) { + return back()->withErrors(['error' => __('Payment verification failed - Invalid response')]); + } + + if ($result['status'] === 'success' && $result['data']['status'] === 'successful') { + // Check if payment amount matches plan price + $expectedAmount = $plan->price; + $paidAmount = $result['data']['amount']; + + if (abs($paidAmount - $expectedAmount) > 0.01) { + return back()->withErrors(['error' => __('Payment amount verification failed')]); + } + + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'flutterwave', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['payment_id'], + ]); + + return redirect()->route('plans.index')->with('success', __('Payment successful! Your plan has been activated.')); + } + + return back()->withErrors(['error' => __('Payment verification failed')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'flutterwave'); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/GoalTypeController.php b/app/Http/Controllers/GoalTypeController.php new file mode 100644 index 000000000..d02bd4142 --- /dev/null +++ b/app/Http/Controllers/GoalTypeController.php @@ -0,0 +1,171 @@ +can('manage-goal-types')) { + $query = GoalType::where(function ($q) { + if (Auth::user()->can('manage-any-goal-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-goal-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'id'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'id'; + } + + $query->orderBy($sortField, $sortDirection); + + $goalTypes = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/performance/goal-types/index', [ + 'goalTypes' => $goalTypes, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-goal-types')) { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + GoalType::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', 'Goal type created successfully'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, GoalType $goalType) + { + if (Auth::user()->can('edit-goal-types')) { + // Check if goal type belongs to current company + if (!in_array($goalType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this goal type'); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $goalType->update([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', 'Goal type updated successfully'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(GoalType $goalType) + { + if (Auth::user()->can('delete-goal-types')) { + // Check if goal type belongs to current company + if (!in_array($goalType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this goal type'); + } + + // Check if goal type is being used in goals + if ($goalType->goals()->count() > 0) { + return redirect()->back()->with('error', 'Cannot delete goal type as it is being used in employee goals'); + } + + $goalType->delete(); + + return redirect()->back()->with('success', 'Goal type deleted successfully'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Toggle the status of the specified resource. + */ + public function toggleStatus(GoalType $goalType) + { + if (Auth::user()->can('edit-goal-types')) { + // Check if goal type belongs to current company + if (!in_array($goalType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this goal type'); + } + + $goalType->update([ + 'status' => $goalType->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', 'Goal type status updated successfully'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/HolidayController.php b/app/Http/Controllers/HolidayController.php new file mode 100644 index 000000000..d9bbbb543 --- /dev/null +++ b/app/Http/Controllers/HolidayController.php @@ -0,0 +1,434 @@ +can('manage-holidays')) { + $query = Holiday::with(['branches'])->where(function ($q) { + if (Auth::user()->can('manage-any-holidays')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-holidays')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%'.$request->search.'%') + ->orWhere('description', 'like', '%'.$request->search.'%'); + }); + } + + // Handle category filter + if ($request->has('category') && ! empty($request->category)) { + $query->where('category', $request->category); + } + + // Handle branch filter + if ($request->has('branch_id') && ! empty($request->branch_id)) { + $query->whereHas('branches', function ($q) use ($request) { + $q->where('branches.id', $request->branch_id); + }); + } + + // Handle date range filter + if ($request->has('date_from') && ! empty($request->date_from)) { + $query->where(function ($q) use ($request) { + $q->where('start_date', '>=', $request->date_from) + ->orWhere('end_date', '>=', $request->date_from); + }); + } + if ($request->has('date_to') && ! empty($request->date_to)) { + $query->where(function ($q) use ($request) { + $q->where('start_date', '<=', $request->date_to) + ->orWhere('end_date', '<=', $request->date_to); + }); + } + + if (! isDemo()) { + // Handle year filter + if ($request->has('year') && ! empty($request->year)) { + $year = $request->year; + $query->where(function ($q) use ($year) { + $q->whereYear('start_date', $year) + ->orWhereYear('end_date', $year); + }); + } else { + // Default to current year if no year specified and not in demo mode + $currentYear = date('Y'); + $query->where(function ($q) use ($currentYear) { + $q->whereYear('start_date', $currentYear) + ->orWhereYear('end_date', $currentYear); + }); + } + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'start_date', 'end_date', 'category', 'is_paid', 'is_recurring', 'is_half_day']; + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field === 'date' ? 'start_date' : $request->sort_field; + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $holidays = $query->paginate($request->per_page ?? 10); + + // Get branches for filter dropdown + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get categories for filter dropdown + $categories = Holiday::whereIn('created_by', getCompanyAndUsersId()) + ->select('category') + ->distinct() + ->pluck('category') + ->toArray(); + + // Get available years for filter dropdown + $years = Holiday::whereIn('created_by', getCompanyAndUsersId()) + ->selectRaw('YEAR(start_date) as year') + ->distinct() + ->pluck('year') + ->toArray(); + + // Add current year if not in the list + $currentYear = (int) date('Y'); + if (! in_array($currentYear, $years)) { + $years[] = $currentYear; + } + sort($years); + + return Inertia::render('hr/holidays/index', [ + 'holidays' => $holidays, + 'branches' => $branches, + 'categories' => $categories, + 'years' => $years, + 'filters' => $request->all(['search', 'category', 'branch_id', 'date_from', 'date_to', 'year', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Display the calendar view. + */ + public function calendar(Request $request) + { + $year = $request->year ?? date('Y'); + + $holidays = Holiday::with(['branches']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where(function ($q) use ($year) { + $q->whereYear('start_date', $year) + ->orWhereYear('end_date', $year); + }) + ->get(); + + // Get branches for filter dropdown + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get categories for filter dropdown + $categories = Holiday::whereIn('created_by', getCompanyAndUsersId()) + ->select('category') + ->distinct() + ->pluck('category') + ->toArray(); + + // Get available years for filter dropdown + $years = Holiday::whereIn('created_by', getCompanyAndUsersId()) + ->selectRaw('YEAR(start_date) as year') + ->distinct() + ->pluck('year') + ->toArray(); + + // Add current year if not in the list + $currentYear = (int) date('Y'); + if (! in_array($currentYear, $years)) { + $years[] = $currentYear; + } + sort($years); + + // Format holidays for FullCalendar + $calendarEvents = $holidays->map(function ($holiday) { + return [ + 'id' => $holiday->id, + 'title' => $holiday->name, + 'start' => $holiday->start_date, + 'end' => $holiday->end_date ? \Carbon\Carbon::parse($holiday->end_date)->addDay()->format('Y-m-d') : null, + 'allDay' => true, + 'backgroundColor' => $this->getCategoryColor($holiday->category), + 'borderColor' => $this->getCategoryColor($holiday->category), + 'extendedProps' => [ + 'category' => $holiday->category, + 'description' => $holiday->description, + 'is_paid' => $holiday->is_paid, + 'is_half_day' => $holiday->is_half_day, + 'is_recurring' => $holiday->is_recurring, + 'branches' => $holiday->branches->pluck('name')->toArray(), + ], + ]; + }); + + return Inertia::render('hr/holidays/calendar', [ + 'holidays' => $holidays, + 'calendarEvents' => $calendarEvents, + 'branches' => $branches, + 'categories' => $categories, + 'years' => $years, + 'currentYear' => (int) $year, + 'filters' => $request->all(['category', 'branch_id']), + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'category' => 'required|string|max:255', + 'description' => 'nullable|string', + 'is_recurring' => 'nullable|boolean', + 'is_paid' => 'nullable|boolean', + 'is_half_day' => 'nullable|boolean', + 'branch_ids' => 'required|array', + 'branch_ids.*' => 'exists:branches,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if branches belong to current company + $branchIds = $request->branch_ids; + $validBranches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->whereIn('id', $branchIds) + ->pluck('id') + ->toArray(); + + if (count($validBranches) !== count($branchIds)) { + return redirect()->back()->with('error', __('Invalid branch selection')); + } + + $holiday = Holiday::create([ + 'name' => $request->name, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'category' => $request->category, + 'description' => $request->description, + 'is_recurring' => $request->is_recurring ?? false, + 'is_paid' => $request->is_paid ?? true, + 'is_half_day' => $request->is_half_day ?? false, + 'created_by' => creatorId(), + ]); + + // Attach branches + $holiday->branches()->attach($validBranches); + + return redirect()->back()->with('success', __('Holiday created successfully')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Holiday $holiday) + { + // Check if holiday belongs to current company + if (! in_array($holiday->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this holiday')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'start_date' => 'required|date', + 'end_date' => 'nullable|date|after_or_equal:start_date', + 'category' => 'required|string|max:255', + 'description' => 'nullable|string', + 'is_recurring' => 'nullable|boolean', + 'is_paid' => 'nullable|boolean', + 'is_half_day' => 'nullable|boolean', + 'branch_ids' => 'required|array', + 'branch_ids.*' => 'exists:branches,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if branches belong to current company + $branchIds = $request->branch_ids; + $validBranches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->whereIn('id', $branchIds) + ->pluck('id') + ->toArray(); + + if (count($validBranches) !== count($branchIds)) { + return redirect()->back()->with('error', __('Invalid branch selection')); + } + + $holiday->update([ + 'name' => $request->name, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'category' => $request->category, + 'description' => $request->description, + 'is_recurring' => $request->is_recurring ?? false, + 'is_paid' => $request->is_paid ?? true, + 'is_half_day' => $request->is_half_day ?? false, + ]); + + // Sync branches + $holiday->branches()->sync($validBranches); + + return redirect()->back()->with('success', __('Holiday updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Holiday $holiday) + { + // Check if holiday belongs to current company + if (! in_array($holiday->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this holiday')); + } + + // Detach all branches + $holiday->branches()->detach(); + + // Delete the holiday + $holiday->delete(); + + return redirect()->back()->with('success', __('Holiday deleted successfully')); + } + + /** + * Export holidays to PDF. + */ + public function exportPdf(Request $request) + { + $year = $request->year ?? date('Y'); + + $query = Holiday::with(['branches']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where(function ($q) use ($year) { + $q->whereYear('start_date', $year) + ->orWhereYear('end_date', $year); + }); + + if ($request->category) { + $query->where('category', $request->category); + } + + if ($request->branch_id) { + $query->whereHas('branches', function ($q) use ($request) { + $q->where('branches.id', $request->branch_id); + }); + } + + $holidays = $query->orderBy('start_date', 'asc')->get(); + + $html = view('exports.holidays-pdf', compact('holidays', 'year'))->render(); + + return response($html) + ->header('Content-Type', 'text/html') + ->header('Content-Disposition', "attachment; filename=holidays-{$year}.html"); + } + + /** + * Export holidays to iCal format. + */ + public function exportIcal(Request $request) + { + $year = $request->year ?? date('Y'); + + $query = Holiday::with(['branches']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where(function ($q) use ($year) { + $q->whereYear('start_date', $year) + ->orWhereYear('end_date', $year); + }); + + if ($request->category) { + $query->where('category', $request->category); + } + + if ($request->branch_id) { + $query->whereHas('branches', function ($q) use ($request) { + $q->where('branches.id', $request->branch_id); + }); + } + + $holidays = $query->orderBy('start_date', 'asc')->get(); + + $icalContent = "BEGIN:VCALENDAR\r\n"; + $icalContent .= "VERSION:2.0\r\n"; + $icalContent .= "PRODID:-//Company//Holidays//EN\r\n"; + $icalContent .= "CALSCALE:GREGORIAN\r\n"; + + foreach ($holidays as $holiday) { + $startDate = \Carbon\Carbon::parse($holiday->start_date)->format('Ymd'); + $endDate = $holiday->end_date ? \Carbon\Carbon::parse($holiday->end_date)->addDay()->format('Ymd') : \Carbon\Carbon::parse($holiday->start_date)->addDay()->format('Ymd'); + + $icalContent .= "BEGIN:VEVENT\r\n"; + $icalContent .= 'UID:'.md5($holiday->id.$holiday->name)."@company.com\r\n"; + $icalContent .= "DTSTART;VALUE=DATE:{$startDate}\r\n"; + $icalContent .= "DTEND;VALUE=DATE:{$endDate}\r\n"; + $icalContent .= 'SUMMARY:'.str_replace(',', '\,', $holiday->name)."\r\n"; + if ($holiday->description) { + $icalContent .= 'DESCRIPTION:'.str_replace(',', '\,', $holiday->description)."\r\n"; + } + $icalContent .= "END:VEVENT\r\n"; + } + + $icalContent .= "END:VCALENDAR\r\n"; + + return response($icalContent) + ->header('Content-Type', 'text/calendar') + ->header('Content-Disposition', "attachment; filename=holidays-{$year}.ics"); + } + + /** + * Get color for holiday category + */ + private function getCategoryColor($category) + { + $colors = [ + 'national' => '#3b82f6', + 'religious' => '#8b5cf6', + 'company-specific' => '#10b77f', + 'regional' => '#f59e0b', + ]; + + return $colors[$category] ?? '#6b7280'; + } +} diff --git a/app/Http/Controllers/HrDocumentController.php b/app/Http/Controllers/HrDocumentController.php new file mode 100644 index 000000000..fc7d1909d --- /dev/null +++ b/app/Http/Controllers/HrDocumentController.php @@ -0,0 +1,236 @@ +can('manage-hr-documents')) { + $query = HrDocument::with(['category', 'uploader', 'approver'])->where(function ($q) { + if (Auth::user()->can('manage-any-hr-documents')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-hr-documents')) { + $q->where('created_by', Auth::id())->orWhere('uploaded_by', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhere('file_name', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('category_id') && !empty($request->category_id) && $request->category_id !== 'all') { + $query->where('category_id', $request->category_id); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + + + // Auto-update expired documents + HrDocument::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', '!=', 'Expired') + ->where('expiry_date', '<', Carbon::today()) + ->update(['status' => 'Expired']); + + // Handle sorting + $allowedSortFields = ['id', 'title', 'status', 'effective_date', 'expiry_date', 'download_count', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + if ($sortField === 'document') { + $sortField = 'title'; + } elseif ($sortField === 'expires') { + $sortField = 'expiry_date'; + } + + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $hrDocuments = $query->paginate($request->per_page ?? 10); + + $categories = DocumentCategory::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/documents/hr-documents/index', [ + 'hrDocuments' => $hrDocuments, + 'categories' => $categories, + 'filters' => $request->all(['search', 'category_id', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'category_id' => 'required|exists:document_categories,id', + 'file' => 'required|string', + 'effective_date' => 'nullable|date', + 'expiry_date' => 'nullable|date|after:effective_date', + 'requires_acknowledgment' => 'boolean', + ]); + if ($validator->fails()) { + return redirect()->back()->withErrors($validator->errors())->withInput(); + } + + // Extract filename from URL or use default + $fileUrl = $request->file; + $fileName = basename(parse_url($fileUrl, PHP_URL_PATH)) ?: 'document_' . time(); + + HrDocument::create([ + 'title' => $request->title, + 'description' => $request->description, + 'category_id' => $request->category_id, + 'file_name' => $fileName, + 'file_path' => $fileUrl, + 'file_type' => 'application/octet-stream', + 'file_size' => 0, + 'effective_date' => $request->effective_date, + 'expiry_date' => $request->expiry_date, + 'requires_acknowledgment' => $request->boolean('requires_acknowledgment'), + 'uploaded_by' => creatorId(), + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Document uploaded successfully')); + } + + public function update(Request $request, HrDocument $hrDocument) + { + if (!in_array($hrDocument->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this document')); + } + + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'category_id' => 'required|exists:document_categories,id', + 'file' => 'nullable|string', + 'effective_date' => 'nullable|date', + 'expiry_date' => 'nullable|date|after:effective_date', + 'requires_acknowledgment' => 'boolean', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator->errors())->withInput(); + } + + $updateData = [ + 'title' => $request->title, + 'description' => $request->description, + 'category_id' => $request->category_id, + 'effective_date' => $request->effective_date, + 'expiry_date' => $request->expiry_date, + 'requires_acknowledgment' => $request->boolean('requires_acknowledgment'), + ]; + + // Handle file replacement from media library + if ($request->has('file') && !empty($request->file)) { + $fileUrl = $request->file; + $fileName = basename(parse_url($fileUrl, PHP_URL_PATH)) ?: 'document_' . time(); + + $updateData = array_merge($updateData, [ + 'file_name' => $fileName, + 'file_path' => $fileUrl, + 'file_type' => 'application/octet-stream', + 'file_size' => 0, + 'version' => $this->incrementVersion($hrDocument->version), + ]); + } + + $hrDocument->update($updateData); + + return redirect()->back()->with('success', __('Document updated successfully')); + } + + public function destroy(HrDocument $hrDocument) + { + if (!in_array($hrDocument->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this document')); + } + + $hrDocument->delete(); + return redirect()->back()->with('success', __('Document deleted successfully')); + } + + public function download(HrDocument $hrDocument) + { + if (!in_array($hrDocument->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to download this document')); + } + + // Increment download count + $hrDocument->increment('download_count'); + + + $filePath = getStorageFilePath($hrDocument->file_path); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Certificate file not found')); + } + + return response()->download($filePath); + } + + public function updateStatus(Request $request, HrDocument $hrDocument) + { + if (!in_array($hrDocument->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this document')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:Draft,Under Review,Approved,Published,Archived,Expired', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator->errors()); + } + + $updateData = ['status' => $request->status]; + + if ($request->status === 'Approved' && !$hrDocument->approved_at) { + $updateData['approved_by'] = creatorId(); + $updateData['approved_at'] = now(); + } + + $hrDocument->update($updateData); + return redirect()->back()->with('success', __('Document status updated successfully')); + } + + private function incrementVersion($currentVersion) + { + $parts = explode('.', $currentVersion); + $parts[1] = isset($parts[1]) ? (int)$parts[1] + 1 : 1; + return implode('.', $parts); + } +} diff --git a/app/Http/Controllers/ImpersonateController.php b/app/Http/Controllers/ImpersonateController.php new file mode 100644 index 000000000..b71a5679d --- /dev/null +++ b/app/Http/Controllers/ImpersonateController.php @@ -0,0 +1,52 @@ + auth()->id(), + 'impersonated_user_id' => $userId, + 'ip_address' => $request->ip(), + 'timestamp' => now() + ]); + + $originalUserId = auth()->id(); + + // Login as the target user first + auth()->loginUsingId($userId); + // Then store original user ID in session + session()->put('impersonated_user_id', $userId); + session()->put('impersonated_by', $originalUserId); + session()->save(); + + return redirect('/dashboard')->with('success', __('Now impersonating :name', ['name' => $user->name])); + } + + public function leave(Request $request) + { + Log::info('Impersonation ended', [ + 'timestamp' => now() + ]); + + $originalUserId = session('impersonated_by'); + if ($originalUserId) { + auth()->loginUsingId($originalUserId); + session()->forget('impersonated_by'); + session()->forget('impersonated_user_id'); + session()->save(); + } + + return redirect('/companies')->with('success', __('Returned to admin panel')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/InterviewController.php b/app/Http/Controllers/InterviewController.php new file mode 100644 index 000000000..8674ef96a --- /dev/null +++ b/app/Http/Controllers/InterviewController.php @@ -0,0 +1,228 @@ +can('manage-interviews')) { + $query = Interview::with(['candidate', 'job', 'round', 'interviewType'])->where(function ($q) { + if (Auth::user()->can('manage-any-interviews')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-interviews')) { + $q->where('created_by', Auth::id())->orwhereJsonContains('interviewers', (string) Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('candidate', function ($q) use ($request) { + $q->where('first_name', 'like', '%' . $request->search . '%') + ->orWhere('last_name', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('candidate_id') && !empty($request->candidate_id) && $request->candidate_id !== 'all') { + $query->where('candidate_id', $request->candidate_id); + } + + $query->orderBy('id', 'desc'); + $interviews = $query->paginate($request->per_page ?? 10); + + $candidates = Candidate::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'first_name', 'last_name') + ->where('status', 'Interview') + ->get(); + + $interviewTypes = InterviewType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $employees = UserModel::with('employee') + ->whereIn('type', ['manager', 'hr', 'employee']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name', 'type') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'type' => $user->type, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + + return Inertia::render('hr/recruitment/interviews/index', [ + 'interviews' => $interviews, + 'candidates' => $candidates, + 'interviewTypes' => $interviewTypes, + 'employees' => $employees, + 'filters' => $request->all(['search', 'status', 'candidate_id', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'candidate_id' => 'required|exists:candidates,id', + 'round_id' => 'required|exists:interview_rounds,id', + 'interview_type_id' => 'required|exists:interview_types,id', + 'scheduled_date' => 'required|date|after_or_equal:today', + 'scheduled_time' => 'required|date_format:H:i', + 'duration' => 'required|integer|min:15|max:480', + 'location' => 'nullable|string|max:255', + 'meeting_link' => 'nullable|url', + 'interviewers' => 'required|array|min:1', + 'interviewers.*' => 'exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if interview already exists for this candidate and round + $existingInterview = Interview::where('candidate_id', $request->candidate_id) + ->where('round_id', $request->round_id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($existingInterview) { + return redirect()->back()->with('error', __('Interview already exists for this interview round')); + } + + $candidate = Candidate::find($request->candidate_id); + + Interview::create([ + 'candidate_id' => $request->candidate_id, + 'job_id' => $candidate->job_id, + 'round_id' => $request->round_id, + 'interview_type_id' => $request->interview_type_id, + 'scheduled_date' => $request->scheduled_date, + 'scheduled_time' => $request->scheduled_time, + 'duration' => $request->duration, + 'location' => $request->location, + 'meeting_link' => $request->meeting_link, + 'interviewers' => $request->interviewers, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Interview scheduled successfully')); + } + + public function update(Request $request, Interview $interview) + { + if (!in_array($interview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this interview')); + } + + $validator = Validator::make($request->all(), [ + 'candidate_id' => 'required|exists:candidates,id', + 'round_id' => 'required|exists:interview_rounds,id', + 'interview_type_id' => 'required|exists:interview_types,id', + 'scheduled_date' => 'required|date', + 'scheduled_time' => 'required', + 'duration' => 'required|integer|min:15|max:480', + 'location' => 'nullable|string|max:255', + 'meeting_link' => 'nullable|url', + 'interviewers' => 'required|array|min:1', + 'interviewers.*' => 'exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if interview already exists for this candidate and round (excluding current record) + $existingInterview = Interview::where('candidate_id', $request->candidate_id) + ->where('round_id', $request->round_id) + ->where('id', '!=', $interview->id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($existingInterview) { + return redirect()->back()->with('error', __('Interview already exists for this interview round')); + } + + $candidate = Candidate::find($request->candidate_id); + + $interview->update([ + 'candidate_id' => $request->candidate_id, + 'job_id' => $candidate->job_id, + 'round_id' => $request->round_id, + 'interview_type_id' => $request->interview_type_id, + 'scheduled_date' => $request->scheduled_date, + 'scheduled_time' => $request->scheduled_time, + 'duration' => $request->duration, + 'location' => $request->location, + 'meeting_link' => $request->meeting_link, + 'interviewers' => $request->interviewers, + ]); + + return redirect()->back()->with('success', __('Interview updated successfully')); + } + + public function destroy(Interview $interview) + { + if (!in_array($interview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this interview')); + } + + $interview->delete(); + return redirect()->back()->with('success', __('Interview deleted successfully')); + } + + public function updateStatus(Request $request, Interview $interview) + { + if (!in_array($interview->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this interview')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:Scheduled,Completed,Cancelled,No-show', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $interview->update(['status' => $request->status]); + return redirect()->back()->with('success', __('Interview status updated successfully')); + } + + public function getRoundsByCandidate(Candidate $candidate) + { + if (!in_array($candidate->created_by, getCompanyAndUsersId())) { + return response()->json([]); + } + + $rounds = InterviewRound::where('job_id', $candidate->job_id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return response()->json($rounds); + } +} diff --git a/app/Http/Controllers/InterviewFeedbackController.php b/app/Http/Controllers/InterviewFeedbackController.php new file mode 100644 index 000000000..1d050ff77 --- /dev/null +++ b/app/Http/Controllers/InterviewFeedbackController.php @@ -0,0 +1,220 @@ +can('manage-interview-feedback')) { + $query = InterviewFeedback::with(['interview.candidate', 'interview.job', 'interview.round'])->where(function ($q) { + if (Auth::user()->can('manage-any-interview-feedback')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-interview-feedback')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('interview.candidate', function ($q) use ($request) { + $q->where('first_name', 'like', '%' . $request->search . '%') + ->orWhere('last_name', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('recommendation') && !empty($request->recommendation) && $request->recommendation !== 'all') { + $query->where('recommendation', $request->recommendation); + } + + if ($request->has('interviewer_id') && !empty($request->interviewer_id) && $request->interviewer_id !== 'all') { + $query->where('interviewer_id', $request->interviewer_id); + } + + $sortField = $request->get('sort_field'); + $sortDirection = $request->get('sort_direction', 'desc'); + + $allowedSortFields = ['created_at']; + if ($sortField && in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + + $interviewFeedback = $query->paginate($request->per_page ?? 10); + + $interviewFeedback->getCollection()->transform(function ($feedback) { + $feedback->interviewer_names = $feedback->interviewer_names; + return $feedback; + }); + + $interviews = Interview::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'Completed') + ->with(['candidate', 'job', 'round']) + ->when( + Auth::user()->can('manage-own-interview-feedback') && !Auth::user()->can('manage-any-interview-feedback'), + function ($q) { + $q->whereJsonContains('interviewers', (string) Auth::id()); + } + ) + ->when( + !Auth::user()->can('manage-any-interview-feedback') && !Auth::user()->can('manage-own-interview-feedback'), + function ($q) { + $q->whereRaw('1 = 0'); + } + ) + ->get(); + + $interviewers = User::whereIn('created_by', getCompanyAndUsersId()) + ->whereIn('type', ['manager', 'hr', 'employee']) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/recruitment/interview-feedback/index', [ + 'interviewFeedback' => $interviewFeedback, + 'interviews' => $interviews, + 'interviewers' => $interviewers, + 'filters' => $request->all(['search', 'recommendation', 'interviewer_id', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'interview_id' => 'required|exists:interviews,id', + 'interviewer_id' => 'required', + 'technical_rating' => 'nullable|numeric|min:0.5|max:5', + 'communication_rating' => 'nullable|numeric|min:0.5|max:5', + 'cultural_fit_rating' => 'nullable|numeric|min:0.5|max:5', + 'overall_rating' => 'nullable|numeric|min:0.5|max:5', + 'strengths' => 'nullable|string', + 'weaknesses' => 'nullable|string', + 'comments' => 'nullable|string', + 'recommendation' => 'nullable', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $existingFeedback = InterviewFeedback::where('interview_id', $request->interview_id) + ->where('interviewer_id', $request->interviewer_id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($existingFeedback) { + return redirect()->back()->with('error', __('Feedback already exists for this interview and interviewer')); + } + + InterviewFeedback::create([ + 'interview_id' => $request->interview_id, + 'interviewer_id' => $request->interviewer_id, + 'technical_rating' => $request->technical_rating, + 'communication_rating' => $request->communication_rating, + 'cultural_fit_rating' => $request->cultural_fit_rating, + 'overall_rating' => $request->overall_rating, + 'strengths' => $request->strengths, + 'weaknesses' => $request->weaknesses, + 'comments' => $request->comments, + 'recommendation' => $request->recommendation, + 'created_by' => creatorId(), + ]); + + Interview::where('id', $request->interview_id)->update(['feedback_submitted' => true]); + + return redirect()->back()->with('success', __('Interview feedback submitted successfully')); + } + + public function update(Request $request, InterviewFeedback $interviewFeedback) + { + if (!in_array($interviewFeedback->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this feedback')); + } + + $validator = Validator::make($request->all(), [ + 'interview_id' => 'required|exists:interviews,id', + 'interviewer_id' => 'required', + 'technical_rating' => 'nullable|numeric|min:0.5|max:5', + 'communication_rating' => 'nullable|numeric|min:0.5|max:5', + 'cultural_fit_rating' => 'nullable|numeric|min:0.5|max:5', + 'overall_rating' => 'nullable|numeric|min:0.5|max:5', + 'strengths' => 'nullable|string', + 'weaknesses' => 'nullable|string', + 'comments' => 'nullable|string', + 'recommendation' => 'nullable', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $existingFeedback = InterviewFeedback::where('interview_id', $request->interview_id) + ->where('interviewer_id', $request->interviewer_id) + ->where('id', '!=', $interviewFeedback->id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($existingFeedback) { + return redirect()->back()->with('error', __('Feedback already exists for this interview and interviewer')); + } + + $interviewFeedback->update([ + 'interview_id' => $request->interview_id, + 'interviewer_id' => $request->interviewer_id, + 'technical_rating' => $request->technical_rating, + 'communication_rating' => $request->communication_rating, + 'cultural_fit_rating' => $request->cultural_fit_rating, + 'overall_rating' => $request->overall_rating, + 'strengths' => $request->strengths, + 'weaknesses' => $request->weaknesses, + 'comments' => $request->comments, + 'recommendation' => $request->recommendation, + ]); + + Interview::where('id', $request->interview_id)->update(['feedback_submitted' => true]); + + return redirect()->back()->with('success', __('Interview feedback updated successfully')); + } + + public function destroy(InterviewFeedback $interviewFeedback) + { + if (!in_array($interviewFeedback->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this feedback')); + } + + $interviewId = $interviewFeedback->interview_id; + $interviewFeedback->delete(); + + $remainingFeedback = InterviewFeedback::where('interview_id', $interviewId)->exists(); + Interview::where('id', $interviewId)->update(['feedback_submitted' => $remainingFeedback]); + + return redirect()->back()->with('success', __('Interview feedback deleted successfully')); + } + + public function getInterviewers(Interview $interview) + { + if (!in_array($interview->created_by, getCompanyAndUsersId())) { + return response()->json([]); + } + + $interviewers = User::whereIn('id', $interview->interviewers) + ->whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + return response()->json($interviewers); + } +} diff --git a/app/Http/Controllers/InterviewRoundController.php b/app/Http/Controllers/InterviewRoundController.php new file mode 100644 index 000000000..9da9b4e15 --- /dev/null +++ b/app/Http/Controllers/InterviewRoundController.php @@ -0,0 +1,171 @@ +can('manage-interview-rounds')) { + $query = InterviewRound::with(['job'])->where(function ($q) { + if (Auth::user()->can('manage-any-interview-rounds')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-interview-rounds')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('job_id') && !empty($request->job_id) && $request->job_id !== 'all') { + $query->where('job_id', $request->job_id); + } + + // Handle sorting + $sortField = $request->get('sort_field'); + $sortDirection = $request->get('sort_direction', 'asc'); + + // Validate sort field + $allowedSortFields = ['created_at']; + if ($sortField && in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + $interviewRounds = $query->paginate($request->per_page ?? 10); + + $jobPostings = JobPosting::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'title', 'job_code') + ->get(); + + return Inertia::render('hr/recruitment/interview-rounds/index', [ + 'interviewRounds' => $interviewRounds, + 'jobPostings' => $jobPostings, + 'filters' => $request->all(['search', 'status', 'job_id', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'job_id' => 'required|exists:job_postings,id', + 'name' => 'required|string|max:255', + 'sequence_number' => 'required|integer|min:1', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if interview round with same job_id and sequence_number already exists + $existingRound = InterviewRound::where('job_id', $request->job_id) + ->where('sequence_number', $request->sequence_number) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($existingRound) { + return redirect()->back()->with('error', __('Interview round with sequence number :sequence already exists for this job posting', ['sequence' => $request->sequence_number])); + } + + InterviewRound::create([ + 'job_id' => $request->job_id, + 'name' => $request->name, + 'sequence_number' => $request->sequence_number, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Interview round created successfully')); + } + + public function update(Request $request, InterviewRound $interviewRound) + { + if (!in_array($interviewRound->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this interview round')); + } + + $validator = Validator::make($request->all(), [ + 'job_id' => 'required|exists:job_postings,id', + 'name' => 'required|string|max:255', + 'sequence_number' => 'required|integer|min:1', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if interview round with same job_id and sequence_number already exists (excluding current record) + $existingRound = InterviewRound::where('job_id', $request->job_id) + ->where('sequence_number', $request->sequence_number) + ->where('id', '!=', $interviewRound->id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($existingRound) { + return redirect()->back()->with('error', __('Interview round with sequence number :sequence already exists for this job posting', ['sequence' => $request->sequence_number])); + } + + $interviewRound->update([ + 'job_id' => $request->job_id, + 'name' => $request->name, + 'sequence_number' => $request->sequence_number, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Interview round updated successfully')); + } + + public function destroy(InterviewRound $interviewRound) + { + if (!in_array($interviewRound->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this interview round')); + } + + if ($interviewRound->interviews()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete interview round as it has associated interviews')); + } + + $interviewRound->delete(); + return redirect()->back()->with('success', __('Interview round deleted successfully')); + } + + public function toggleStatus(InterviewRound $interviewRound) + { + if (!in_array($interviewRound->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this interview round')); + } + + $interviewRound->update([ + 'status' => $interviewRound->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Interview round status updated successfully')); + } +} diff --git a/app/Http/Controllers/InterviewTypeController.php b/app/Http/Controllers/InterviewTypeController.php new file mode 100644 index 000000000..d59ee1370 --- /dev/null +++ b/app/Http/Controllers/InterviewTypeController.php @@ -0,0 +1,132 @@ +can('manage-interview-types')) { + $query = InterviewType::where(function ($q) { + if (Auth::user()->can('manage-any-interview-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-interview-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + $interviewTypes = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/interview-types/index', [ + 'interviewTypes' => $interviewTypes, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + InterviewType::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Interview type created successfully')); + } + + public function update(Request $request, InterviewType $interviewType) + { + if (!in_array($interviewType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this interview type')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $interviewType->update([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Interview type updated successfully')); + } + + public function destroy(InterviewType $interviewType) + { + if (!in_array($interviewType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this interview type')); + } + + if ($interviewType->interviews()->count() > 0) { + return redirect()->back()->with('error', _('Cannot delete interview type as it is being used in interviews')); + } + + $interviewType->delete(); + return redirect()->back()->with('success', __('Interview type deleted successfully')); + } + + public function toggleStatus(InterviewType $interviewType) + { + if (!in_array($interviewType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this interview type')); + } + + $interviewType->update([ + 'status' => $interviewType->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Interview type status updated successfully')); + } +} diff --git a/app/Http/Controllers/IpRestrictionController.php b/app/Http/Controllers/IpRestrictionController.php new file mode 100644 index 000000000..74fdd4f60 --- /dev/null +++ b/app/Http/Controllers/IpRestrictionController.php @@ -0,0 +1,57 @@ +can('create-ip-restriction')) { + + $request->validate([ + 'ip_address' => 'required|unique:ip_restrictions,ip_address', + ]); + + IpRestriction::create([ + 'ip_address' => $request->ip_address, + 'created_by' => Auth::id(), + ]); + + return redirect()->back()->with('success', __('IP Address Added Successfully.')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(Request $request, IpRestriction $ipRestriction) + { + if (Auth::user()->can('edit-ip-restriction')) { + $request->validate([ + 'ip_address' => 'required|unique:ip_restrictions,ip_address,'.$ipRestriction->id, + ]); + + $ipRestriction->update([ + 'ip_address' => $request->ip_address, + ]); + + return redirect()->back()->with('success', __('IP Address Update Successfully.')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy(IpRestriction $ipRestriction) + { + if (Auth::user()->can('delete-ip-restriction')) { + $ipRestriction->delete(); + + return redirect()->back()->with('success', __('IP Address Delete Successfully.')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/IyzipayPaymentController.php b/app/Http/Controllers/IyzipayPaymentController.php new file mode 100644 index 000000000..df6adac27 --- /dev/null +++ b/app/Http/Controllers/IyzipayPaymentController.php @@ -0,0 +1,230 @@ +setApiKey($settings['iyzipay_public_key']); + $options->setSecretKey($settings['iyzipay_secret_key']); + $options->setBaseUrl($settings['iyzipay_mode'] === 'live' + ? 'https://api.iyzipay.com' + : 'https://sandbox-api.iyzipay.com'); + + return $options; + } + + public function processPayment(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'token' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['iyzipay_secret_key']) || !isset($settings['payment_settings']['iyzipay_public_key'])) { + return back()->withErrors(['error' => __('Iyzipay not configured')]); + } + + // Retrieve payment result from Iyzipay + $paymentResult = $this->retrieveIyzipayPayment($validated['token'], $settings['payment_settings']); + + if ($paymentResult && $paymentResult->getPaymentStatus() === 'SUCCESS') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'iyzipay', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $paymentResult->getPaymentId(), + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'iyzipay'); + } + } + + public function createPaymentForm(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['iyzipay_secret_key']) || !isset($settings['payment_settings']['iyzipay_public_key'])) { + return response()->json(['error' => __('Iyzipay not configured')], 400); + } + + $user = auth()->user(); + $conversationId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + $options = $this->getIyzipayOptions($settings['payment_settings']); + + // Create checkout form initialize request + $checkoutRequest = new CreateCheckoutFormInitializeRequest(); + $checkoutRequest->setLocale(Locale::EN); + $checkoutRequest->setConversationId($conversationId); + $checkoutRequest->setPrice(number_format($pricing['final_price'], 2, '.', '')); + $checkoutRequest->setPaidPrice(number_format($pricing['final_price'], 2, '.', '')); + $checkoutRequest->setCurrency(Currency::USD); + $checkoutRequest->setBasketId('plan_' . $plan->id); + $checkoutRequest->setPaymentGroup(PaymentGroup::SUBSCRIPTION); + $checkoutRequest->setCallbackUrl(route('iyzipay.callback')); + $checkoutRequest->setEnabledInstallments([1]); + + // Set buyer information + $buyer = new Buyer(); + $buyer->setId($user->id); + $buyer->setName($user->name ?? 'Customer'); + $buyer->setSurname('User'); + $buyer->setGsmNumber('+1234567890'); + $buyer->setEmail($user->email); + $buyer->setIdentityNumber('11111111111'); + $buyer->setLastLoginDate(now()->format('Y-m-d H:i:s')); + $buyer->setRegistrationDate($user->created_at->format('Y-m-d H:i:s')); + $buyer->setRegistrationAddress('123 Main Street'); + $buyer->setIp($request->ip()); + $buyer->setCity('New York'); + $buyer->setCountry('United States'); + $buyer->setZipCode('10001'); + $checkoutRequest->setBuyer($buyer); + + // Set shipping address + $shippingAddress = new Address(); + $shippingAddress->setContactName($user->name ?? 'Customer User'); + $shippingAddress->setCity('New York'); + $shippingAddress->setCountry('United States'); + $shippingAddress->setAddress('123 Main Street'); + $shippingAddress->setZipCode('10001'); + $checkoutRequest->setShippingAddress($shippingAddress); + + // Set billing address + $billingAddress = new Address(); + $billingAddress->setContactName($user->name ?? 'Customer User'); + $billingAddress->setCity('New York'); + $billingAddress->setCountry('United States'); + $billingAddress->setAddress('123 Main Street'); + $billingAddress->setZipCode('10001'); + $checkoutRequest->setBillingAddress($billingAddress); + + // Set basket items + $basketItem = new BasketItem(); + $basketItem->setId($plan->id); + $basketItem->setName($plan->name); + $basketItem->setCategory1('Subscription'); + $basketItem->setItemType(BasketItemType::VIRTUAL); + $basketItem->setPrice(number_format($pricing['final_price'], 2, '.', '')); + $basketItems = [$basketItem]; + $checkoutRequest->setBasketItems($basketItems); + + // Initialize checkout form + $checkoutFormInitialize = CheckoutFormInitialize::create($checkoutRequest, $options); + + if ($checkoutFormInitialize->getStatus() === 'success') { + return response()->json([ + 'success' => true, + 'redirect_url' => $checkoutFormInitialize->getPaymentPageUrl(), + 'token' => $checkoutFormInitialize->getToken() + ]); + } else { + return response()->json(['error' => $checkoutFormInitialize->getErrorMessage()], 400); + } + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment form creation failed')], 500); + } + } + + public function callback(Request $request) + { + try { + $token = $request->input('token'); + $settings = getPaymentGatewaySettings(); + + if (!$token) { + return redirect()->route('plans.index')->withErrors(['error' => __('Invalid payment response')]); + } + + // Retrieve payment result from Iyzipay + $paymentResult = $this->retrieveIyzipayPayment($token, $settings['payment_settings']); + + if ($paymentResult && $paymentResult->getPaymentStatus() === 'SUCCESS') { + // Extract conversation ID to find the plan and user + $conversationId = $paymentResult->getConversationId(); + $parts = explode('_', $conversationId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', // Default, should be stored in session or passed + 'payment_method' => 'iyzipay', + 'payment_id' => $paymentResult->getPaymentId(), + ]); + + return redirect()->route('plans.index')->with('success', __('Payment successful! Your plan has been activated.')); + } + } + } + + return redirect()->route('plans.index')->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return redirect()->route('plans.index')->withErrors(['error' => __('Payment processing failed')]); + } + } + + private function retrieveIyzipayPayment($token, $settings) + { + try { + $options = $this->getIyzipayOptions($settings); + + $request = new RetrieveCheckoutFormRequest(); + $request->setToken($token); + + $checkoutForm = CheckoutForm::retrieve($request, $options); + + return $checkoutForm; + } catch (\Exception $e) { + return null; + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/JobCategoryController.php b/app/Http/Controllers/JobCategoryController.php new file mode 100644 index 000000000..1a5331074 --- /dev/null +++ b/app/Http/Controllers/JobCategoryController.php @@ -0,0 +1,155 @@ +can('manage-job-categories')) { + $query = JobCategory::where(function ($q) { + if (Auth::user()->can('manage-any-job-categories')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-job-categories')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'id'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'id'; + } + + $query->orderBy($sortField, $sortDirection); + + $jobCategories = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/job-categories/index', [ + 'jobCategories' => $jobCategories, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + JobCategory::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Job category created successfully')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, JobCategory $jobCategory) + { + // Check if job category belongs to current company + if (!in_array($jobCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this job category')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $jobCategory->update([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Job category updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(JobCategory $jobCategory) + { + // Check if job category belongs to current company + if (!in_array($jobCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this job category')); + } + + // Check if job category is being used in job requisitions + if ($jobCategory->jobRequisitions()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete job category as it is being used in job requisitions')); + } + + $jobCategory->delete(); + + return redirect()->back()->with('success', __('Job category deleted successfully')); + } + + /** + * Toggle the status of the specified resource. + */ + public function toggleStatus(JobCategory $jobCategory) + { + // Check if job category belongs to current company + if (!in_array($jobCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this job category')); + } + + $jobCategory->update([ + 'status' => $jobCategory->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Job category status updated successfully')); + } +} diff --git a/app/Http/Controllers/JobLocationController.php b/app/Http/Controllers/JobLocationController.php new file mode 100644 index 000000000..564cbc3bc --- /dev/null +++ b/app/Http/Controllers/JobLocationController.php @@ -0,0 +1,148 @@ +can('manage-job-locations')) { + $query = JobLocation::where(function ($q) { + if (Auth::user()->can('manage-any-job-locations')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-job-locations')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('city', 'like', '%' . $request->search . '%') + ->orWhere('address', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('is_remote') && $request->is_remote !== 'all') { + $query->where('is_remote', $request->is_remote === 'true'); + } + + $query->orderBy('id', 'desc'); + $jobLocations = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/job-locations/index', [ + 'jobLocations' => $jobLocations, + 'filters' => $request->all(['search', 'status', 'is_remote', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'address' => 'nullable|string', + 'city' => 'nullable|string|max:255', + 'state' => 'nullable|string|max:255', + 'country' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'is_remote' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + JobLocation::create([ + 'name' => $request->name, + 'address' => $request->address, + 'city' => $request->city, + 'state' => $request->state, + 'country' => $request->country, + 'postal_code' => $request->postal_code, + 'is_remote' => $request->boolean('is_remote'), + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Job location created successfully')); + } + + public function update(Request $request, JobLocation $jobLocation) + { + if (!in_array($jobLocation->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this job location'); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'address' => 'nullable|string', + 'city' => 'nullable|string|max:255', + 'state' => 'nullable|string|max:255', + 'country' => 'nullable|string|max:255', + 'postal_code' => 'nullable|string|max:20', + 'is_remote' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $jobLocation->update([ + 'name' => $request->name, + 'address' => $request->address, + 'city' => $request->city, + 'state' => $request->state, + 'country' => $request->country, + 'postal_code' => $request->postal_code, + 'is_remote' => $request->boolean('is_remote'), + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Job location updated successfully')); + } + + public function destroy(JobLocation $jobLocation) + { + if (!in_array($jobLocation->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this job location'); + } + + if ($jobLocation->jobPostings()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete job location as it is being used in job postings')); + } + + $jobLocation->delete(); + return redirect()->back()->with('success', __('Job location deleted successfully')); + } + + public function toggleStatus(JobLocation $jobLocation) + { + if (!in_array($jobLocation->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this job location'); + } + + $jobLocation->update([ + 'status' => $jobLocation->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Job location status updated successfully')); + } +} diff --git a/app/Http/Controllers/JobPostingController.php b/app/Http/Controllers/JobPostingController.php new file mode 100644 index 000000000..a7107453a --- /dev/null +++ b/app/Http/Controllers/JobPostingController.php @@ -0,0 +1,341 @@ +can('manage-job-postings')) { + $query = JobPosting::with(['requisition', 'jobType', 'location', 'department'])->withCount('candidates')->where(function ($q) { + if (Auth::user()->can('manage-any-job-postings')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-job-postings')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%'.$request->search.'%') + ->orWhere('job_code', 'like', '%'.$request->search.'%'); + }); + } + + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('is_published') && $request->is_published !== 'all') { + $query->where('is_published', $request->is_published === 'true'); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'id'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field to prevent 500 errors + $allowedSortFields = ['job_code', 'title', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'id'; + } + + $query->orderBy($sortField, $sortDirection); + $jobPostings = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/job-postings/index', [ + 'jobPostings' => $jobPostings, + 'filters' => $request->all(['search', 'status', 'is_published', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function create() + { + if (! Auth::user()->can('create-job-postings')) { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + $jobTypes = JobType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $locations = JobLocation::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $departments = Department::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name', 'branch_id') + ->get(); + + return Inertia::render('hr/recruitment/job-postings/create', [ + 'jobTypes' => $jobTypes, + 'locations' => $locations, + 'branches' => $branches, + 'departments' => $departments, + 'customQuestions' => CustomQuestion::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'question', 'required') + ->get(), + ]); + } + + public function show(JobPosting $jobPosting) + { + if (! Auth::user()->can('view-job-postings')) { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + if (! in_array($jobPosting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view this job posting')); + } + + $jobPosting->load(['requisition', 'jobType', 'location', 'department.branch', 'branch']); + + return Inertia::render('hr/recruitment/job-postings/show', [ + 'jobPosting' => $jobPosting, + 'customQuestions' => CustomQuestion::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'question', 'required') + ->get(), + ]); + } + + public function edit(JobPosting $jobPosting) + { + if (! Auth::user()->can('edit-job-postings')) { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + if (! in_array($jobPosting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to edit this job posting')); + } + + $jobTypes = JobType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $locations = JobLocation::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $departments = Department::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name', 'branch_id') + ->get(); + + return Inertia::render('hr/recruitment/job-postings/edit', [ + 'jobPosting' => $jobPosting, + 'jobTypes' => $jobTypes, + 'locations' => $locations, + 'branches' => $branches, + 'departments' => $departments, + 'customQuestions' => CustomQuestion::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'question', 'required') + ->get(), + ]); + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'job_type_id' => 'required|exists:job_types,id', + 'location_id' => 'required|exists:job_locations,id', + 'branch_id' => 'nullable|exists:branches,id', + 'department_id' => 'nullable|exists:departments,id', + 'priority' => 'nullable|in:Low,Medium,High', + 'skills' => 'required|array', + 'positions' => 'required|integer|min:1', + 'min_experience' => 'required|numeric|min:0', + 'max_experience' => 'required|numeric|min:0', + 'min_salary' => 'required|numeric|min:0', + 'max_salary' => 'required|numeric|min:0', + 'description' => 'required|string', + 'requirements' => 'required|string', + 'benefits' => 'nullable|string', + 'start_date' => 'required|date', + 'application_deadline' => 'required|date|after:today', + 'application_type' => 'required|in:existing,custom', + 'application_url' => 'required|string', + 'applicant' => 'nullable|array', + 'visibility' => 'nullable|array', + 'custom_question' => 'nullable|array', + 'is_featured' => 'boolean', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $jobPosting = new JobPosting; + $jobPosting->job_code = JobPosting::generateJobCode(null); + $jobPosting->title = $request->title; + $jobPosting->job_type_id = $request->job_type_id; + $jobPosting->location_id = $request->location_id; + $jobPosting->branch_id = $request->branch_id; + $jobPosting->department_id = $request->department_id; + $jobPosting->priority = $request->priority ?: 'Medium'; + $jobPosting->skills = $request->skills; + $jobPosting->positions = $request->positions; + $jobPosting->min_experience = $request->min_experience; + $jobPosting->max_experience = $request->max_experience; + $jobPosting->min_salary = $request->min_salary; + $jobPosting->max_salary = $request->max_salary; + $jobPosting->description = $request->description; + $jobPosting->requirements = $request->requirements; + $jobPosting->benefits = $request->benefits; + $jobPosting->start_date = $request->start_date; + $jobPosting->application_deadline = $request->application_deadline; + $jobPosting->visibility = $request->has('visibility') ? $request->visibility : null; + $jobPosting->custom_question = $request->has('custom_question') ? $request->custom_question : null; + $jobPosting->applicant = $request->has('applicant') ? $request->applicant : null; + $jobPosting->code = uniqid(); + $jobPosting->application_type = $request->application_type; + $jobPosting->application_url = $request->application_url; + $jobPosting->is_featured = $request->boolean('is_featured'); + $jobPosting->created_by = creatorId(); + $jobPosting->save(); + + return redirect()->route('hr.recruitment.job-postings.index')->with('success', __('Job posting created successfully')); + } + + public function update(Request $request, JobPosting $jobPosting) + { + if (! in_array($jobPosting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this job posting')); + } + + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'job_type_id' => 'required|exists:job_types,id', + 'location_id' => 'required|exists:job_locations,id', + 'branch_id' => 'required|exists:branches,id', + 'department_id' => 'nullable|exists:departments,id', + 'priority' => 'required|in:Low,Medium,High', + 'status' => 'required|in:Draft,Published,Closed', + 'positions' => 'required|integer|min:1', + 'min_experience' => 'required|numeric|min:0', + 'max_experience' => 'nullable|numeric|min:0', + 'min_salary' => 'nullable|numeric|min:0', + 'max_salary' => 'nullable|numeric|min:0', + 'description' => 'nullable|string', + 'requirements' => 'nullable|string', + 'education' => 'nullable|string', + 'benefits' => 'nullable|string', + 'start_date' => 'nullable|date', + 'application_deadline' => 'nullable|date', + 'application_type' => 'required|in:existing,custom', + 'application_url' => 'required|string', + 'skills' => 'required|array', + 'applicant' => 'nullable|array', + 'visibility' => 'nullable|array', + 'custom_question' => 'nullable|array', + 'is_featured' => 'boolean', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $jobPosting->title = $request->title; + $jobPosting->job_type_id = $request->job_type_id; + $jobPosting->location_id = $request->location_id; + $jobPosting->branch_id = $request->branch_id; + $jobPosting->department_id = $request->department_id; + $jobPosting->priority = $request->priority; + $jobPosting->status = $request->status; + $jobPosting->skills = $request->skills; + $jobPosting->positions = $request->positions; + $jobPosting->min_experience = $request->min_experience; + $jobPosting->max_experience = $request->max_experience; + $jobPosting->min_salary = $request->min_salary; + $jobPosting->max_salary = $request->max_salary; + $jobPosting->description = $request->description; + $jobPosting->requirements = $request->requirements; + $jobPosting->benefits = $request->benefits; + $jobPosting->start_date = $request->start_date; + $jobPosting->application_deadline = $request->application_deadline; + $jobPosting->visibility = $request->has('visibility') ? $request->visibility : null; + $jobPosting->applicant = $request->has('applicant') ? $request->applicant : null; + $jobPosting->custom_question = $request->has('custom_question') ? $request->custom_question : null; + $jobPosting->application_type = $request->application_type; + $jobPosting->application_url = $request->application_url; + $jobPosting->is_featured = $request->boolean('is_featured'); + $jobPosting->save(); + + return redirect()->route('hr.recruitment.job-postings.index')->with('success', __('Job posting updated successfully')); + } + + public function destroy(JobPosting $jobPosting) + { + if (! in_array($jobPosting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this job posting')); + } + + if ($jobPosting->candidates()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete job posting as it has associated candidates')); + } + + $jobPosting->delete(); + + return redirect()->back()->with('success', __('Job posting deleted successfully')); + } + + public function publish(JobPosting $jobPosting) + { + if (! in_array($jobPosting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to publish this job posting')); + } + + $jobPosting->update([ + 'is_published' => true, + 'publish_date' => now(), + 'status' => 'Published', + ]); + + return redirect()->back()->with('success', __('Job posting published successfully')); + } + + public function unpublish(JobPosting $jobPosting) + { + if (! in_array($jobPosting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to unpublish this job posting')); + } + + $jobPosting->update([ + 'is_published' => false, + 'status' => 'Draft', + ]); + + return redirect()->back()->with('success', __('Job posting unpublished successfully')); + } +} diff --git a/app/Http/Controllers/JobRequisitionController.php b/app/Http/Controllers/JobRequisitionController.php new file mode 100644 index 000000000..6607cb674 --- /dev/null +++ b/app/Http/Controllers/JobRequisitionController.php @@ -0,0 +1,197 @@ +can('manage-job-requisitions')) { + $query = JobRequisition::with(['jobCategory', 'department.branch', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-job-requisitions')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-job-requisitions')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('requisition_code', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('priority') && !empty($request->priority) && $request->priority !== 'all') { + $query->where('priority', $request->priority); + } + + $query->orderBy('id', 'desc'); + $jobRequisitions = $query->paginate($request->per_page ?? 10); + + $jobCategories = JobCategory::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $departments = Department::with('branch') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name', 'branch_id') + ->get(); + + return Inertia::render('hr/recruitment/job-requisitions/index', [ + 'jobRequisitions' => $jobRequisitions, + 'jobCategories' => $jobCategories, + 'departments' => $departments, + 'filters' => $request->all(['search', 'status', 'priority', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'job_category_id' => 'required|exists:job_categories,id', + 'department_id' => 'nullable|exists:departments,id', + 'positions_count' => 'required|integer|min:1', + 'budget_min' => 'nullable|numeric|min:0', + 'budget_max' => 'nullable|numeric|min:0', + 'skills_required' => 'nullable|string', + 'education_required' => 'nullable|string', + 'experience_required' => 'nullable|string', + 'description' => 'nullable|string', + 'responsibilities' => 'nullable|string', + 'priority' => 'required|in:Low,Medium,High', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $requisitionCode = 'REQ-' . creatorId() . '-' . str_pad( + JobRequisition::whereIn('created_by', getCompanyAndUsersId())->count() + 1, + 4, + '0', + STR_PAD_LEFT + ); + + JobRequisition::create([ + 'requisition_code' => $requisitionCode, + 'title' => $request->title, + 'job_category_id' => $request->job_category_id, + 'department_id' => $request->department_id, + 'positions_count' => $request->positions_count, + 'budget_min' => $request->budget_min, + 'budget_max' => $request->budget_max, + 'skills_required' => $request->skills_required, + 'education_required' => $request->education_required, + 'experience_required' => $request->experience_required, + 'description' => $request->description, + 'responsibilities' => $request->responsibilities, + 'priority' => $request->priority, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Job requisition created successfully')); + } + + public function update(Request $request, JobRequisition $jobRequisition) + { + if (!in_array($jobRequisition->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this job requisition'); + } + + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'job_category_id' => 'required|exists:job_categories,id', + 'department_id' => 'nullable|exists:departments,id', + 'positions_count' => 'required|integer|min:1', + 'budget_min' => 'nullable|numeric|min:0', + 'budget_max' => 'nullable|numeric|min:0', + 'skills_required' => 'nullable|string', + 'education_required' => 'nullable|string', + 'experience_required' => 'nullable|string', + 'description' => 'nullable|string', + 'responsibilities' => 'nullable|string', + 'priority' => 'required|in:Low,Medium,High', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $jobRequisition->update($request->only([ + 'title', + 'job_category_id', + 'department_id', + 'positions_count', + 'budget_min', + 'budget_max', + 'skills_required', + 'education_required', + 'experience_required', + 'description', + 'responsibilities', + 'priority' + ])); + + return redirect()->back()->with('success', __('Job requisition updated successfully')); + } + + public function destroy(JobRequisition $jobRequisition) + { + if (!in_array($jobRequisition->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this job requisition'); + } + + if ($jobRequisition->jobPostings()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete job requisition as it has associated job postings')); + } + + $jobRequisition->delete(); + return redirect()->back()->with('success', __('Job requisition deleted successfully')); + } + + public function updateStatus(Request $request, JobRequisition $jobRequisition) + { + if (!in_array($jobRequisition->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this job requisition'); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:Draft,Pending Approval,Approved,On Hold,Closed', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $updateData = ['status' => $request->status]; + + if ($request->status === 'Approved') { + $updateData['approved_by'] = creatorId(); + $updateData['approval_date'] = now(); + } + + $jobRequisition->update($updateData); + return redirect()->back()->with('success', __('Job requisition status updated successfully')); + } +} diff --git a/app/Http/Controllers/JobTypeController.php b/app/Http/Controllers/JobTypeController.php new file mode 100644 index 000000000..dfd728409 --- /dev/null +++ b/app/Http/Controllers/JobTypeController.php @@ -0,0 +1,133 @@ +can('manage-job-types')) { + $query = JobType::where(function ($q) { + if (Auth::user()->can('manage-any-job-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-job-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'id'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'id'; + } + + $query->orderBy($sortField, $sortDirection); + + $jobTypes = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/job-types/index', [ + 'jobTypes' => $jobTypes, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + JobType::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Job type created successfully')); + } + + public function update(Request $request, JobType $jobType) + { + if (!in_array($jobType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this job type')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $jobType->update([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Job type updated successfully')); + } + + public function destroy(JobType $jobType) + { + if (!in_array($jobType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this job type')); + } + + if ($jobType->jobPostings()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete job type as it is being used in job postings')); + } + + $jobType->delete(); + return redirect()->back()->with('success', __('Job type deleted successfully')); + } + + public function toggleStatus(JobType $jobType) + { + if (!in_array($jobType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this job type')); + } + + $jobType->update([ + 'status' => $jobType->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Job type status updated successfully')); + } +} diff --git a/app/Http/Controllers/JoiningLetterTemplateController.php b/app/Http/Controllers/JoiningLetterTemplateController.php new file mode 100644 index 000000000..2ece1aab5 --- /dev/null +++ b/app/Http/Controllers/JoiningLetterTemplateController.php @@ -0,0 +1,43 @@ +can('update-joining-letter')) { + $request->validate([ + 'content' => 'required|string' + ]); + + if ($request->templateId) { + // Update existing template + $template = JoiningLetterTemplate::where('id', $request->templateId) + ->where('created_by', auth::id()) + ->firstOrFail(); + $template->update(['content' => $request->content]); + } else { + // Create or update by language + $template = JoiningLetterTemplate::updateOrCreate( + [ + 'language' => $request->language, + 'created_by' => auth::id() + ], + [ + 'content' => $request->content + ] + ); + } + + return redirect()->back()->with('success', __('Joining Letter template updated successfully.')); + + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/KhaltiPaymentController.php b/app/Http/Controllers/KhaltiPaymentController.php new file mode 100644 index 000000000..942ad9392 --- /dev/null +++ b/app/Http/Controllers/KhaltiPaymentController.php @@ -0,0 +1,108 @@ + 'required|string', + 'amount' => 'required|numeric', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['khalti_secret_key'])) { + return back()->withErrors(['error' => __('Khalti not configured')]); + } + + // Verify payment with Khalti API + $isValid = $this->verifyKhaltiPayment($validated['token'], $validated['amount'], $settings['payment_settings']); + + if ($isValid) { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'khalti', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['token'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment verification failed')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'khalti'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['khalti_public_key'])) { + return response()->json(['error' => __('Khalti not configured')], 400); + } + + return response()->json([ + 'success' => true, + 'public_key' => $settings['payment_settings']['khalti_public_key'], + 'amount' => $pricing['final_price'] * 100, // Khalti uses paisa + 'product_identity' => 'plan_' . $plan->id, + 'product_name' => $plan->name, + 'product_url' => route('plans.index'), + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + private function verifyKhaltiPayment($token, $amount, $settings) + { + try { + $url = 'https://khalti.com/api/v2/payment/verify/'; + + $data = [ + 'token' => $token, + 'amount' => $amount * 100, // Convert to paisa + ]; + + $headers = [ + 'Authorization: Key ' . $settings['khalti_secret_key'], + 'Content-Type: application/json', + ]; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + curl_close($ch); + + $result = json_decode($response, true); + + return isset($result['state']['name']) && $result['state']['name'] === 'Completed'; + + } catch (\Exception $e) { + return false; + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/LandingPage/CustomPageController.php b/app/Http/Controllers/LandingPage/CustomPageController.php new file mode 100644 index 000000000..54204c912 --- /dev/null +++ b/app/Http/Controllers/LandingPage/CustomPageController.php @@ -0,0 +1,114 @@ +filled('search')) { + $search = $request->get('search'); + $query->where(function ($q) use ($search) { + $q->where('title', 'like', "%{$search}%") + ->orWhere('content', 'like', "%{$search}%") + ->orWhere('slug', 'like', "%{$search}%"); + }); + } + + // Sorting + $sortField = $request->get('sort_field', 'sort_order'); + $sortDirection = $request->get('sort_direction', 'asc'); + + if (in_array($sortField, ['title', 'created_at', 'sort_order'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->ordered(); + } + + $pages = $query->paginate($request->get('per_page', 10)) + ->withQueryString(); + + return Inertia::render('landing-page/custom-pages/index', [ + 'pages' => $pages, + 'filters' => $request->only(['search', 'sort_field', 'sort_direction', 'per_page']) + ]); + } + + public function create() + { + return Inertia::render('landing-page/custom-pages/create'); + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255|unique:landing_page_custom_pages,title,' . $customPage->id, + 'content' => 'required|string', + 'meta_title' => 'nullable|string|max:255', + 'meta_description' => 'nullable|string', + 'is_active' => 'boolean', + 'sort_order' => 'nullable|integer' + ]); + + LandingPageCustomPage::create($validated); + + return redirect()->route('landing-page.custom-pages.index')->with('success', __('Custom page created successfully!')); + } + + public function edit(LandingPageCustomPage $customPage) + { + return Inertia::render('landing-page/custom-pages/edit', [ + 'page' => $customPage + ]); + } + + public function update(Request $request, LandingPageCustomPage $customPage) + { + $validated = $request->validate([ + 'title' => 'required|string|max:255|unique:landing_page_custom_pages,title,' . $customPage->id, + 'content' => 'required|string', + 'meta_title' => 'nullable|string|max:255', + 'meta_description' => 'nullable|string', + 'is_active' => 'sometimes|boolean', + 'sort_order' => 'nullable|integer' + ]); + + // Ensure is_active is properly handled + if (!isset($validated['is_active'])) { + $validated['is_active'] = $request->has('is_active') ? (bool)$request->input('is_active') : false; + } + + $customPage->update($validated); + + return redirect()->route('landing-page.custom-pages.index')->with('success', __('Custom page updated successfully!')); + } + + public function destroy(LandingPageCustomPage $customPage) + { + $customPage->delete(); + return back()->with('success', __('Custom page deleted successfully!')); + } + + public function show($slug) + { + $page = LandingPageCustomPage::where('slug', $slug)->where('is_active', true)->firstOrFail(); + $landingSettings = \App\Models\LandingPageSetting::getSettings(); + + // Track page visit for super admin analytics + // \Shetabit\Visitor\Facade\Visitor::visit(); + + return Inertia::render('landing-page/custom-page', [ + 'page' => $page, + 'customPages' => LandingPageCustomPage::active()->ordered()->get(), + 'settings' => $landingSettings + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/LandingPageController.php b/app/Http/Controllers/LandingPageController.php new file mode 100644 index 000000000..a384fbfd4 --- /dev/null +++ b/app/Http/Controllers/LandingPageController.php @@ -0,0 +1,164 @@ +getHost(); + $hostParts = explode('.', $host); + + // Track general landing page visit + // \Shetabit\Visitor\Facade\Visitor::visit(); + + // Check if landing page is enabled in settings + if (!isLandingPageEnabled()) { + return redirect()->route('login'); + } + + $landingSettings = LandingPageSetting::getSettings(); + + $plans = collect(); + + if (isSaas()) { + $plans = Plan::where('is_plan_enable', 'on')->get()->map(function ($plan) { + $features = []; + if ($plan->enable_custdomain === 'on') + $features[] = 'Custom Domain'; + if ($plan->enable_custsubdomain === 'on') + $features[] = 'Subdomain'; + if ($plan->pwa_business === 'on') + $features[] = 'PWA'; + if ($plan->enable_chatgpt === 'on') + $features[] = 'AI Integration'; + + return [ + 'id' => $plan->id, + 'name' => $plan->name, + 'price' => $plan->price, + 'yearly_price' => $plan->yearly_price, + 'duration' => $plan->duration, + 'description' => $plan->description, + 'features' => $features, + 'stats' => [ + 'employees' => $plan->max_employees, + 'users' => $plan->max_users, + 'storage' => $plan->storage_limit . ' GB', + ], + 'is_plan_enable' => $plan->is_plan_enable, + 'is_popular' => false // Will be set based on subscriber count + ]; + }); + + // Mark most subscribed plan as popular + $planSubscriberCounts = Plan::withCount('users')->get()->pluck('users_count', 'id'); + if ($planSubscriberCounts->isNotEmpty()) { + $mostSubscribedPlanId = $planSubscriberCounts->keys()->sortByDesc(function ($planId) use ($planSubscriberCounts) { + return $planSubscriberCounts[$planId]; + })->first(); + + $plans = $plans->map(function ($plan) use ($mostSubscribedPlanId) { + if ($plan['id'] == $mostSubscribedPlanId && $plan['price'] != '0') { + $plan['is_popular'] = true; + } + return $plan; + }); + } + } + + return Inertia::render('landing-page/index', [ + 'plans' => $plans, + 'testimonials' => [], + 'faqs' => [], + 'customPages' => LandingPageCustomPage::active()->ordered()->get() ?? [], + 'settings' => $landingSettings + ]); + } + + public function submitContact(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255', + 'email' => 'required|email|max:255', + 'subject' => 'required|string|max:255', + 'message' => 'required|string' + ]); + + if (isSaaS()) { + $user = User::where('type', 'superadmin')->orWhere('type', 'super admin')->first(); + } else { + $user = User::where('type', 'company')->first(); + } + + $contact = new Contact(); + $contact->name = $request->name; + $contact->email = $request->email; + $contact->subject = $request->subject; + $contact->message = $request->message; + $contact->created_by = $user->id; + $contact->save(); + + return back()->with('success', __('Thank you for your message. We will get back to you soon!')); + } + + public function subscribe(Request $request) + { + $request->validate([ + 'email' => 'required|email|max:255' + ]); + + try { + // Check if email already exists + $existingSubscriber = NewsLetter::where('email', $request->email)->first(); + + if ($existingSubscriber) { + return back()->with('error', __('This email is already subscribed to our newsletter.')); + } + + // Create new newsletter subscription + NewsLetter::create([ + 'email' => $request->email + ]); + + return back()->with('success', __('Thank you for subscribing to our newsletter!')); + } catch (\Exception $e) { + \Log::error('Newsletter subscription failed: ' . $e->getMessage()); + return back()->with('error', __('Something went wrong. Please try again later.')); + } + } + + public function settings() + { + $landingSettings = LandingPageSetting::getSettings(); + + return Inertia::render('landing-page/settings', [ + 'settings' => $landingSettings + ]); + } + + public function updateSettings(Request $request) + { + $request->validate([ + 'company_name' => 'required|string|max:255', + 'contact_email' => 'required|email|max:255', + 'contact_phone' => 'required|string|max:255', + 'contact_address' => 'required|string|max:255', + 'config_sections' => 'required|array' + ]); + $landingSettings = LandingPageSetting::getSettings(); + $landingSettings->update($request->all()); + + return back()->with('success', __('Landing page settings updated successfully!')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/LanguageController.php b/app/Http/Controllers/LanguageController.php new file mode 100644 index 000000000..b77358a62 --- /dev/null +++ b/app/Http/Controllers/LanguageController.php @@ -0,0 +1,275 @@ +pluck('code')->contains($lang)) { + $selectedLang = $lang; + } + $defaultData = []; + if (File::exists(resource_path("lang/{$selectedLang}.json"))) { + $defaultData = json_decode(File::get(resource_path("lang/{$selectedLang}.json")), true); + } + return Inertia::render('manage-language', [ + 'languages' => $languages, + 'defaultLang' => $selectedLang, + 'defaultData' => $defaultData, + ]); + } + + // Load a language file + public function load(Request $request) + { + $langListPath = resource_path('lang/language.json'); + $languages = collect(); + if (File::exists($langListPath)) { + $languages = collect(json_decode(File::get($langListPath), true)); + } + $lang = $request->get('lang', 'en'); + if (!$languages->pluck('code')->contains($lang)) { + return response()->json(['error' => __('Language not found')], 404); + } + $langPath = resource_path("lang/{$lang}.json"); + if (!File::exists($langPath)) { + return response()->json(['error' => __('Language file not found')], 404); + } + $data = json_decode(File::get($langPath), true); + return response()->json(['data' => $data]); + } + + // Save a language file + public function save(Request $request) + { + try { + $langListPath = resource_path('lang/language.json'); + $languages = collect(); + if (File::exists($langListPath)) { + $languages = collect(json_decode(File::get($langListPath), true)); + } + $lang = $request->get('lang'); + $data = $request->get('data'); + if (!$lang || !is_array($data) || !$languages->pluck('code')->contains($lang)) { + if ($request->expectsJson()) { + return response()->json(['error' => __('Invalid request')], 400); + } + return redirect()->back()->with('error', __('Invalid request')); + } + $langPath = resource_path("lang/{$lang}.json"); + if (!File::exists($langPath)) { + if ($request->expectsJson()) { + return response()->json(['error' => __('Language file not found')], 404); + } + return redirect()->back()->with('error', __('Language file not found')); + } + File::put($langPath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + if ($request->expectsJson()) { + return response()->json(['success' => __('Language updated successfully')]); + } + return redirect()->back()->with('success', __('Language updated successfully')); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json(['error' => __('Failed to update language file: ') . $e->getMessage()], 500); + } + return redirect()->back()->with('error', __('Failed to update language file: ') . $e->getMessage()); + } + } + + public function createLanguage(Request $request) + { + $request->validate([ + 'code' => 'required|string|max:10', + 'name' => 'required|string|max:255', + 'countryCode' => 'required|string|size:2' + ], [ + 'code.required' => __('Language code is required.'), + 'code.string' => __('Language code must be a valid string.'), + 'code.max' => __('Language code must not exceed 10 characters.'), + 'name.required' => __('Language name is required.'), + 'name.string' => __('Language name must be a valid string.'), + 'name.max' => __('Language name must not exceed 255 characters.'), + 'countryCode.required' => __('Country code is required.'), + 'countryCode.string' => __('Country code must be a valid string.'), + 'countryCode.size' => __('Country code must be exactly 2 characters.'), + ]); + + try { + // Check if language already exists in language.json + $languagesFile = resource_path('lang/language.json'); + + if (!is_writable($languagesFile)) { + return response()->json(['error' => __('Language file is not writable. Please check file permissions.')], 500); + } + + $languages = json_decode(File::get($languagesFile), true); + + $existingLanguage = collect($languages)->firstWhere('code', $request->code); + if ($existingLanguage) { + return response()->json(['error' => __('The language code already exists')], 422); + } + + $languages[] = [ + 'code' => $request->code, + 'name' => $request->name, + 'countryCode' => strtoupper($request->countryCode) + ]; + + $result = File::put($languagesFile, json_encode($languages, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + if ($result === false) { + return response()->json(['error' => __('Failed to write to language file. Please check file permissions.')], 500); + } + + // Copy en.json to new language + $enFile = resource_path('lang/en.json'); + $newLangFile = resource_path("lang/{$request->code}.json"); + if (File::exists($enFile)) { + $enContent = File::get($enFile); + File::put($newLangFile, $enContent); + } else { + // Create empty translation file if en.json doesn't exist + File::put($newLangFile, json_encode([], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + } + + return response()->json(['success' => true, 'message' => __('The language has been created successfully.')]); + } catch (\Exception $e) { + return response()->json(['error' => 'Failed to create language: ' . $e->getMessage()], 500); + } + } + + public function deleteLanguage($languageCode) + { + if ($languageCode === 'en') { + return response()->json(['error' => __('Cannot delete English language')], 422); + } + + try { + // Remove from language.json + $languagesFile = resource_path('lang/language.json'); + $languages = json_decode(File::get($languagesFile), true); + $languages = array_filter($languages, fn($lang) => $lang['code'] !== $languageCode); + File::put($languagesFile, json_encode(array_values($languages), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + // Delete main language file + $mainLangFile = resource_path("lang/{$languageCode}.json"); + if (File::exists($mainLangFile)) { + File::delete($mainLangFile); + } + + return response()->json(['success' => true, 'message' => __('The language has been deleted.')]); + } catch (\Exception $e) { + return response()->json(['error' => __('Failed to delete language: :error', ['error' => $e->getMessage()])], 500); + } + } + + public function toggleLanguageStatus($languageCode) + { + if ($languageCode === 'en') { + return response()->json(['error' => __('Cannot disable English language')], 422); + } + + try { + $languagesFile = resource_path('lang/language.json'); + $languages = json_decode(File::get($languagesFile), true); + + foreach ($languages as &$language) { + if ($language['code'] === $languageCode) { + $language['enabled'] = !($language['enabled'] ?? true); + break; + } + } + + File::put($languagesFile, json_encode($languages, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + return response()->json(['success' => true, 'message' => __('The language status updated successfully.')]); + } catch (\Exception $e) { + return response()->json(['error' => __('Failed to update language status: :error', ['error' => $e->getMessage()])], 500); + } + } + + public function updateTranslations(Request $request, $locale) + { + $newTranslations = $request->input('translations'); + $path = resource_path("lang/{$locale}.json"); + + try { + // Ensure directory exists + $dir = dirname($path); + if (!is_dir($dir)) { + mkdir($dir, 0755, true); + } + + // Try to make file writable if it exists + if (file_exists($path)) { + @chmod($path, 0666); + } + + // Load existing translations + $existingTranslations = []; + if (file_exists($path)) { + $existingContent = File::get($path); + $existingTranslations = json_decode($existingContent, true) ?? []; + } + + // Merge new translations with existing ones + $mergedTranslations = array_merge($existingTranslations, $newTranslations); + + $result = File::put($path, json_encode($mergedTranslations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + if ($result === false) { + // If File::put fails, try alternative method + $handle = @fopen($path, 'w'); + if ($handle) { + fwrite($handle, json_encode($mergedTranslations, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + fclose($handle); + @chmod($path, 0666); + } else { + return response()->json(['error' => __('Cannot write to translation file. Please check permissions.')], 500); + } + } + + return response()->json(['success' => true, 'message' => __('Translations updated successfully')]); + } catch (\Exception $e) { + return response()->json(['error' => __('Failed to save translations: ') . $e->getMessage()], 500); + } + } + + public function changeLanguage(Request $request) + { + $languageCode = $request->input('language'); + + // RTL languages that should automatically set layoutDirection to 'right' + $rtlLanguages = ['ar', 'he']; + $isRtl = in_array($languageCode, $rtlLanguages); + + if (config('app.is_demo')) { + return redirect()->back()->cookie('app_language', $languageCode, 60 * 24 * 365); + } + + if ($request->user()) { + $request->user()->update(['lang' => $languageCode]); + + // Auto-update layoutDirection for RTL languages + if ($isRtl) { + updateSetting('layoutDirection', 'right', $request->user()->id); + } + } + + return redirect()->back(); + } + +} diff --git a/app/Http/Controllers/LeaveApplicationController.php b/app/Http/Controllers/LeaveApplicationController.php new file mode 100644 index 000000000..281905095 --- /dev/null +++ b/app/Http/Controllers/LeaveApplicationController.php @@ -0,0 +1,412 @@ +can('manage-leave-applications')) { + $query = LeaveApplication::with(['employee', 'leaveType', 'leavePolicy', 'approver', 'creator']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-leave-applications')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-leave-applications')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('reason', 'like', '%'.$request->search.'%') + ->orWhereHas('employee', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%'.$request->search.'%'); + }) + ->orWhereHas('leaveType', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%'.$request->search.'%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && ! empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + // Handle leave type filter + if ($request->has('leave_type_id') && ! empty($request->leave_type_id) && $request->leave_type_id !== 'all') { + $query->where('leave_type_id', $request->leave_type_id); + } + + // Handle status filter + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['start_date', 'end_date', 'created_at'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $leaveApplications = $query->paginate($request->per_page ?? 10); + + $leaveApplications->getCollection()->transform(function ($application) { + if ($application->employee) { + $rawAvatar = $application->employee->getRawOriginal('avatar'); + $application->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $application; + }); + + // Get employees for filter dropdown + $employees = User::where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name']); + + // Get leave types for filter dropdown + $leaveTypes = LeaveType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'color']); + + return Inertia::render('hr/leave-applications/index', [ + 'leaveApplications' => $leaveApplications, + 'employees' => $this->getFilteredEmployees(), + 'leaveTypes' => $leaveTypes, + 'filters' => $request->all(['search', 'employee_id', 'leave_type_id', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-leave-applications') && ! Auth::user()->can('manage-any-leave-applications')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + + return $employees; + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'leave_type_id' => 'required|exists:leave_types,id', + 'start_date' => 'required|date|after_or_equal:today', + 'end_date' => 'required|date|after_or_equal:start_date', + 'reason' => 'required|string', + 'attachment' => 'nullable|string', + ]); + + $validated['created_by'] = creatorId(); + + // Calculate total days + $startDate = Carbon::parse($validated['start_date']); + $endDate = Carbon::parse($validated['end_date']); + $validated['total_days'] = $startDate->diffInDays($endDate) + 1; + + // Get leave policy for this leave type + $leavePolicy = LeavePolicy::where('leave_type_id', $validated['leave_type_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->first(); + + if (! $leavePolicy) { + return redirect()->back()->with('error', __('No active policy found for selected leave type.')); + } + + $validated['leave_policy_id'] = $leavePolicy->id; + + // Validate days per application + if ( + $validated['total_days'] < $leavePolicy->min_days_per_application || + $validated['total_days'] > $leavePolicy->max_days_per_application + ) { + return redirect()->back()->with( + 'error', + __('Leave days must be between :min and :max days as per the leave policy.', [ + 'min' => $leavePolicy->min_days_per_application, + 'max' => $leavePolicy->max_days_per_application, + ]) + ); + } + + // Check if employee has enough leave balance + $currentYear = now()->year; + $leaveBalance = \App\Models\LeaveBalance::where('employee_id', $validated['employee_id']) + ->where('leave_type_id', $validated['leave_type_id']) + ->where('year', $currentYear) + ->first(); + + if (! $leaveBalance) { + // Create initial balance if doesn't exist + $leaveBalance = \App\Models\LeaveBalance::create([ + 'employee_id' => $validated['employee_id'], + 'leave_type_id' => $validated['leave_type_id'], + 'leave_policy_id' => $leavePolicy->id, + 'year' => $currentYear, + 'allocated_days' => $leavePolicy->max_days_per_year ?? 10, + 'used_days' => 0, + 'remaining_days' => $leavePolicy->max_days_per_year ?? 10, + 'created_by' => creatorId(), + ]); + } + + // Check if enough balance available + if ($leaveBalance->remaining_days < $validated['total_days']) { + return redirect()->back()->with( + 'error', + __('Insufficient leave balance. Available: :available days, Requested: :requested days', [ + 'available' => $leaveBalance->remaining_days, + 'requested' => $validated['total_days'], + ]) + ); + } + + // Handle attachment from media library + if ($request->has('attachment')) { + $validated['attachment'] = $request->attachment; + } + + // Set status based on policy + $validated['status'] = $leavePolicy->requires_approval ? 'pending' : 'approved'; + + $leaveApplication = LeaveApplication::create($validated); + + // Create attendance records if auto-approved + if ($validated['status'] === 'approved') { + $leaveApplication->createAttendanceRecords(); + } + + return redirect()->back()->with('success', __('Leave application created successfully.')); + } + + public function update(Request $request, $leaveApplicationId) + { + $leaveApplication = LeaveApplication::where('id', $leaveApplicationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveApplication) { + try { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'leave_type_id' => 'required|exists:leave_types,id', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'reason' => 'required|string', + 'attachment' => 'nullable|string', + ]); + + // Calculate total days + $startDate = Carbon::parse($validated['start_date']); + $endDate = Carbon::parse($validated['end_date']); + $validated['total_days'] = $startDate->diffInDays($endDate) + 1; + + // Get leave policy + $leavePolicy = LeavePolicy::where('leave_type_id', $validated['leave_type_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->first(); + + if (! $leavePolicy) { + return redirect()->back()->with('error', __('No active policy found for selected leave type.')); + } + + $validated['leave_policy_id'] = $leavePolicy->id; + + // Handle attachment from media library + if ($request->has('attachment')) { + $validated['attachment'] = $request->attachment; + } + + $leaveApplication->update($validated); + + return redirect()->back()->with('success', __('Leave application updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update leave application')); + } + } else { + return redirect()->back()->with('error', __('Leave application Not Found.')); + } + } + + public function destroy($leaveApplicationId) + { + $leaveApplication = LeaveApplication::where('id', $leaveApplicationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveApplication) { + try { + $leaveApplication->delete(); + + return redirect()->back()->with('success', __('Leave application deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete leave application')); + } + } else { + return redirect()->back()->with('error', __('Leave application Not Found.')); + } + } + + public function updateStatus(Request $request, $leaveApplicationId) + { + $validated = $request->validate([ + 'status' => 'required|in:approved,rejected', + 'manager_comments' => 'nullable|string', + ]); + + $leaveApplication = LeaveApplication::where('id', $leaveApplicationId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveApplication) { + try { + $leaveApplication->update([ + 'status' => $validated['status'], + 'manager_comments' => $validated['manager_comments'], + 'approved_by' => Auth::id(), + 'approved_at' => now(), + ]); + + // Create attendance records if approved + if ($validated['status'] === 'approved') { + // Double-check balance before final approval + $currentYear = now()->year; + $leaveBalance = \App\Models\LeaveBalance::where('employee_id', $leaveApplication->employee_id) + ->where('leave_type_id', $leaveApplication->leave_type_id) + ->where('year', $currentYear) + ->first(); + + if ($leaveBalance && $leaveBalance->remaining_days < $leaveApplication->total_days) { + return redirect()->back()->with( + 'error', + __('Cannot approve: Insufficient leave balance. Available: :available days, Required: :required days', [ + 'available' => $leaveBalance->remaining_days, + 'required' => $leaveApplication->total_days, + ]) + ); + } + + $leaveApplication->createAttendanceRecords(); + } + + return redirect()->back()->with('success', __('Leave application status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update leave application status')); + } + } else { + return redirect()->back()->with('error', __('Leave application Not Found.')); + } + } + + public function export() + { + if (Auth::user()->can('export-leave-applications')) { + try { + $leaveApplications = LeaveApplication::with(['employee', 'leaveType', 'approver']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-leave-applications')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-leave-applications')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + })->get(); + + $fileName = 'leave_applications_'.date('Y-m-d_His').'.csv'; + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="'.$fileName.'"', + ]; + + $callback = function () use ($leaveApplications) { + $file = fopen('php://output', 'w'); + fputcsv($file, [ + 'Employee', + 'Leave Type', + 'Start Date', + 'End Date', + 'Total Days', + 'Reason', + 'Status', + 'Approved By', + 'Approved At', + 'Manager Comments', + 'Applied On', + ]); + + foreach ($leaveApplications as $application) { + fputcsv($file, [ + $application->employee->name ?? '', + $application->leaveType->name ?? '', + $application->start_date ? date('Y-m-d', strtotime($application->start_date)) : '', + $application->end_date ? date('Y-m-d', strtotime($application->end_date)) : '', + $application->total_days ?? '', + $application->reason ?? '', + $application->status ?? '', + $application->approver->name ?? '', + $application->approved_at ?? '', + $application->manager_comments ?? '', + $application->created_at ?? '', + ]); + } + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to export leave applications: :message', ['message' => $e->getMessage()])], 500); + } + } else { + return response()->json(['message' => __('Permission Denied.')], 403); + } + } +} diff --git a/app/Http/Controllers/LeaveBalanceController.php b/app/Http/Controllers/LeaveBalanceController.php new file mode 100644 index 000000000..2c9d58841 --- /dev/null +++ b/app/Http/Controllers/LeaveBalanceController.php @@ -0,0 +1,284 @@ +can('manage-leave-balances')) { + $query = LeaveBalance::with(['employee', 'leaveType', 'leavePolicy', 'creator']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-leave-balances')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-leave-balances')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->whereHas('employee', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%' . $request->search . '%'); + }) + ->orWhereHas('leaveType', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + // Handle leave type filter + if ($request->has('leave_type_id') && !empty($request->leave_type_id) && $request->leave_type_id !== 'all') { + $query->where('leave_type_id', $request->leave_type_id); + } + + // Handle year filter + if ($request->has('year') && !empty($request->year) && $request->year !== 'all') { + $query->where('year', $request->year); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'year') { + $query->orderBy('year', $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + } else { + $query->orderBy('created_at', 'desc'); + } + + $leaveBalances = $query->paginate($request->per_page ?? 10); + + $leaveBalances->getCollection()->transform(function ($balance) { + if ($balance->employee) { + $rawAvatar = $balance->employee->getRawOriginal('avatar'); + $balance->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $balance; + }); + + // Get employees for filter dropdown + $employees = User::where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name']); + + // Get leave types for filter dropdown + $leaveTypes = LeaveType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'color']); + + // Get years for filter + $years = LeaveBalance::whereIn('created_by', getCompanyAndUsersId()) + ->distinct() + ->pluck('year') + ->sort() + ->values(); + + return Inertia::render('hr/leave-balances/index', [ + 'leaveBalances' => $leaveBalances, + 'employees' => $this->getFilteredEmployees(), + 'leaveTypes' => $leaveTypes, + 'years' => $years, + 'filters' => $request->all(['search', 'employee_id', 'leave_type_id', 'year', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-leave-balances') && !Auth::user()->can('manage-any-leave-balances')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + return $employees; + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'leave_type_id' => 'required|exists:leave_types,id', + 'year' => 'required|integer|min:2020|max:2030', + 'allocated_days' => 'required|numeric|min:0', + 'carried_forward' => 'nullable|numeric|min:0', + 'manual_adjustment' => 'nullable|numeric', + 'adjustment_reason' => 'nullable|string', + ]); + + $validated['created_by'] = creatorId(); + $validated['carried_forward'] = $validated['carried_forward'] ?? 0; + $validated['manual_adjustment'] = $validated['manual_adjustment'] ?? 0; + + // Get leave policy for this leave type + $leavePolicy = LeavePolicy::where('leave_type_id', $validated['leave_type_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->first(); + + if (!$leavePolicy) { + return redirect()->back()->with('error', __('No active policy found for selected leave type.')); + } + + $validated['leave_policy_id'] = $leavePolicy->id; + + // Check if balance already exists + $exists = LeaveBalance::where('employee_id', $validated['employee_id']) + ->where('leave_type_id', $validated['leave_type_id']) + ->where('year', $validated['year']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Leave balance already exists for this employee, leave type, and year.')); + } + + // Calculate remaining days + $validated['used_days'] = 0; + $validated['remaining_days'] = ($validated['allocated_days'] + $validated['carried_forward'] + $validated['manual_adjustment']) - $validated['used_days']; + + LeaveBalance::create($validated); + + return redirect()->back()->with('success', __('Leave balance created successfully.')); + } + + public function update(Request $request, $leaveBalanceId) + { + $leaveBalance = LeaveBalance::where('id', $leaveBalanceId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveBalance) { + try { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'leave_type_id' => 'required|exists:leave_types,id', + 'year' => 'required|integer|min:2020|max:2030', + 'allocated_days' => 'required|numeric|min:0', + 'carried_forward' => 'nullable|numeric|min:0', + 'manual_adjustment' => 'nullable|numeric', + 'adjustment_reason' => 'nullable|string', + ]); + + $validated['carried_forward'] = $validated['carried_forward'] ?? 0; + $validated['manual_adjustment'] = $validated['manual_adjustment'] ?? 0; + + // Get leave policy + $leavePolicy = LeavePolicy::where('leave_type_id', $validated['leave_type_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->first(); + + if (!$leavePolicy) { + return redirect()->back()->with('error', __('No active policy found for selected leave type.')); + } + + $validated['leave_policy_id'] = $leavePolicy->id; + + // Recalculate remaining days + $validated['remaining_days'] = ($validated['allocated_days'] + $validated['carried_forward'] + $validated['manual_adjustment']) - $leaveBalance->used_days; + + $leaveBalance->update($validated); + + return redirect()->back()->with('success', __('Leave balance updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update leave balance')); + } + } else { + return redirect()->back()->with('error', __('Leave balance Not Found.')); + } + } + + public function destroy($leaveBalanceId) + { + $leaveBalance = LeaveBalance::where('id', $leaveBalanceId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveBalance) { + try { + $leaveBalance->delete(); + return redirect()->back()->with('success', __('Leave balance deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete leave balance')); + } + } else { + return redirect()->back()->with('error', __('Leave balance Not Found.')); + } + } + + public function adjust(Request $request, $leaveBalanceId) + { + $validated = $request->validate([ + 'manual_adjustment' => 'required|numeric', + 'adjustment_reason' => 'required|string', + ]); + + $leaveBalance = LeaveBalance::where('id', $leaveBalanceId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveBalance) { + try { + $leaveBalance->update([ + 'manual_adjustment' => $validated['manual_adjustment'], + 'adjustment_reason' => $validated['adjustment_reason'], + ]); + + // Recalculate remaining days + $leaveBalance->calculateRemainingDays(); + $leaveBalance->save(); + + return redirect()->back()->with('success', __('Leave balance adjusted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to adjust leave balance')); + } + } else { + return redirect()->back()->with('error', __('Leave balance Not Found.')); + } + } +} diff --git a/app/Http/Controllers/LeavePolicyController.php b/app/Http/Controllers/LeavePolicyController.php new file mode 100644 index 000000000..1dcc31f6c --- /dev/null +++ b/app/Http/Controllers/LeavePolicyController.php @@ -0,0 +1,189 @@ +can('manage-leave-policies')) { + $query = LeavePolicy::with(['leaveType', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-leave-policies')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-leave-policies')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhereHas('leaveType', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle leave type filter + if ($request->has('leave_type_id') && !empty($request->leave_type_id) && $request->leave_type_id !== 'all') { + $query->where('leave_type_id', $request->leave_type_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['name', 'created_at'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + } else { + $query->orderBy('created_at', 'desc'); + } + + $leavePolicies = $query->paginate($request->per_page ?? 10); + + // Get leave types for filter dropdown + $leaveTypes = LeaveType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get(['id', 'name', 'color']); + + return Inertia::render('hr/leave-policies/index', [ + 'leavePolicies' => $leavePolicies, + 'leaveTypes' => $leaveTypes, + 'filters' => $request->all(['search', 'leave_type_id', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'leave_type_id' => 'required|exists:leave_types,id', + 'accrual_type' => 'required|in:monthly,yearly', + 'accrual_rate' => 'required|numeric|min:0', + 'carry_forward_limit' => 'required|integer|min:0', + 'min_days_per_application' => 'required|integer|min:1', + 'max_days_per_application' => 'required|integer|min:1', + 'requires_approval' => 'boolean', + 'status' => 'nullable|in:active,inactive', + ]); + + $validated['created_by'] = creatorId(); + $validated['status'] = $validated['status'] ?? 'active'; + $validated['requires_approval'] = $validated['requires_approval'] ?? true; + + // Check if leave type belongs to the current user's company + $leaveType = LeaveType::where('id', $validated['leave_type_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$leaveType) { + return redirect()->back()->with('error', __('Invalid leave type selected.')); + } + + LeavePolicy::create($validated); + + return redirect()->back()->with('success', __('Leave policy created successfully.')); + } + + public function update(Request $request, $leavePolicyId) + { + $leavePolicy = LeavePolicy::where('id', $leavePolicyId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leavePolicy) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'leave_type_id' => 'required|exists:leave_types,id', + 'accrual_type' => 'required|in:monthly,yearly', + 'accrual_rate' => 'required|numeric|min:0', + 'carry_forward_limit' => 'required|integer|min:0', + 'min_days_per_application' => 'required|integer|min:1', + 'max_days_per_application' => 'required|integer|min:1', + 'requires_approval' => 'boolean', + 'status' => 'nullable|in:active,inactive', + ]); + + // Check if leave type belongs to the current user's company + $leaveType = LeaveType::where('id', $validated['leave_type_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$leaveType) { + return redirect()->back()->with('error', __('Invalid leave type selected.')); + } + + $leavePolicy->update($validated); + + return redirect()->back()->with('success', __('Leave policy updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update leave policy')); + } + } else { + return redirect()->back()->with('error', __('Leave policy Not Found.')); + } + } + + public function destroy($leavePolicyId) + { + $leavePolicy = LeavePolicy::where('id', $leavePolicyId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leavePolicy) { + try { + $leavePolicy->delete(); + return redirect()->back()->with('success', __('Leave policy deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete leave policy')); + } + } else { + return redirect()->back()->with('error', __('Leave policy Not Found.')); + } + } + + public function toggleStatus($leavePolicyId) + { + $leavePolicy = LeavePolicy::where('id', $leavePolicyId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leavePolicy) { + try { + $leavePolicy->status = $leavePolicy->status === 'active' ? 'inactive' : 'active'; + $leavePolicy->save(); + + return redirect()->back()->with('success', __('Leave policy status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update leave policy status')); + } + } else { + return redirect()->back()->with('error', __('Leave policy Not Found.')); + } + } +} diff --git a/app/Http/Controllers/LeaveTypeController.php b/app/Http/Controllers/LeaveTypeController.php new file mode 100644 index 000000000..ad219a06b --- /dev/null +++ b/app/Http/Controllers/LeaveTypeController.php @@ -0,0 +1,166 @@ +can('manage-leave-types')) { + $query = LeaveType::with(['creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-leave-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-leave-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['name', 'created_at'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + } else { + $query->orderBy('created_at', 'desc'); + } + + $leaveTypes = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/leave-types/index', [ + 'leaveTypes' => $leaveTypes, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'max_days_per_year' => 'required|integer|min:0', + 'is_paid' => 'boolean', + 'color' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/', + 'status' => 'nullable|in:active,inactive', + ]); + + $validated['created_by'] = creatorId(); + $validated['status'] = $validated['status'] ?? 'active'; + + // Check if leave type with same name already exists + $exists = LeaveType::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Leave type with this name already exists.')); + } + + LeaveType::create($validated); + + return redirect()->back()->with('success', __('Leave type created successfully.')); + } + + public function update(Request $request, $leaveTypeId) + { + $leaveType = LeaveType::where('id', $leaveTypeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveType) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'max_days_per_year' => 'required|integer|min:0', + 'is_paid' => 'boolean', + 'color' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/', + 'status' => 'nullable|in:active,inactive', + ]); + + // Check if leave type with same name already exists (excluding current) + $exists = LeaveType::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('id', '!=', $leaveTypeId) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Leave type with this name already exists.')); + } + + $leaveType->update($validated); + + return redirect()->back()->with('success', __('Leave type updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update leave type')); + } + } else { + return redirect()->back()->with('error', __('Leave type Not Found.')); + } + } + + public function destroy($leaveTypeId) + { + $leaveType = LeaveType::where('id', $leaveTypeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveType) { + try { + $leaveType->delete(); + return redirect()->back()->with('success', __('Leave type deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete leave type')); + } + } else { + return redirect()->back()->with('error', __('Leave type Not Found.')); + } + } + + public function toggleStatus($leaveTypeId) + { + $leaveType = LeaveType::where('id', $leaveTypeId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($leaveType) { + try { + $leaveType->status = $leaveType->status === 'active' ? 'inactive' : 'active'; + $leaveType->save(); + + return redirect()->back()->with('success', __('Leave type status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update leave type status')); + } + } else { + return redirect()->back()->with('error', __('Leave type Not Found.')); + } + } +} diff --git a/app/Http/Controllers/LoginHistoryController.php b/app/Http/Controllers/LoginHistoryController.php new file mode 100644 index 000000000..240768afe --- /dev/null +++ b/app/Http/Controllers/LoginHistoryController.php @@ -0,0 +1,78 @@ +can('manage-login-history')) { + $query = LoginHistory::with('user:id,name,email,type')->where(function ($q) { + if (isSaaS()) { + if (Auth::user()->hasRole('superadmin')) { + $q->where('created_by', Auth::id()); + } else if (Auth::user()->hasRole('company')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + } else { + if (Auth::user()->hasRole('company')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + } + }); + + // Search functionality + if ($request->filled('search')) { + $search = $request->search; + $query->whereHas('user', function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + })->orWhere('ip', 'like', "%{$search}%"); + } + + // Sorting + $sortField = $request->get('sort_field', 'date'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['date', 'ip']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'date'; + } + + $query->orderBy($sortField, $sortDirection); + + // Pagination + $perPage = $request->get('per_page', 10); + $loginHistory = $query->paginate($perPage)->withQueryString(); + + return Inertia::render('login-history/index', [ + 'loginHistory' => $loginHistory, + 'filters' => $request->only(['search', 'sort_field', 'sort_direction', 'per_page']) + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + + } + + public function destroy(LoginHistory $loginDetail) + { + if (Auth::user()->can('delete-login-history')) { + $loginDetail->delete(); + return redirect()->back()->with('success', 'Login history deleted successfully.'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/MediaController.php b/app/Http/Controllers/MediaController.php new file mode 100644 index 000000000..f4a92cce2 --- /dev/null +++ b/app/Http/Controllers/MediaController.php @@ -0,0 +1,407 @@ +user(); + $directoryId = request('directory_id'); + + $mediaQuery = Media::WithPermissionCheck(); + + // Filter by directory + if ($directoryId) { + $mediaQuery->where('directory_id', $directoryId); + } + // When no directory is selected, show all files (don't filter by directory_id) + + $media = $mediaQuery->latest()->get()->map(function ($media) { + try { + $url = getImageUrlPrefix() . '/storage/media/' . $media->file_name; + return [ + 'id' => $media->id, + 'name' => $media->name, + 'file_name' => $media->file_name, + 'url' => $url, + 'thumb_url' => $url, + 'size' => $media->size, + 'mime_type' => $media->mime_type, + 'creator_id' => $media->creator_id, + 'directory_id' => $media->directory_id, // Add this field + 'created_at' => $media->created_at, + ]; + } catch (\Exception $e) { + return null; + } + })->filter(); + + // Get directories + $directories = MediaDirectory::withPermissionCheck() + ->whereNull('parent_id') + ->get(['id', 'name', 'slug']); + + return response()->json([ + 'media' => $media, + 'directories' => $directories + ]); + } + + private function getFullUrl($url) + { + if (str_starts_with($url, 'http')) { + return $url; + } + + $baseUrl = request()->getSchemeAndHttpHost(); + return $baseUrl . $url; + } + + private function getUserFriendlyError(\Exception $e, $fileName): string + { + $message = $e->getMessage(); + $extension = strtoupper(pathinfo($fileName, PATHINFO_EXTENSION)); + + // Handle media library collection errors + if (str_contains($message, 'was not accepted into the collection')) { + if (str_contains($message, 'mime:')) { + return __("File type not allowed : :extension", ['extension' => $extension]); + } + return __("File format not supported : :extension", ['extension' => $extension]); + } + + // Handle storage errors + if (str_contains($message, 'storage') || str_contains($message, 'disk')) { + return __("Storage error : :extension", ['extension' => $extension]); + } + + // Handle file size errors + if (str_contains($message, 'size') || str_contains($message, 'large')) { + return __("File too large : :extension", ['extension' => $extension]); + } + + // Handle permission errors + if (str_contains($message, 'permission') || str_contains($message, 'denied')) { + return __("Permission denied : :extension", ['extension' => $extension]); + } + + // Generic fallback + return __("Upload failed : :extension", ['extension' => $extension]); + } + + public function batchStore(Request $request) + { + // Check storage limits + if (isSaaS()) { + $storageCheck = $this->checkStorageLimit($request->file('files')); + if ($storageCheck) { + return $storageCheck; + } + } + + + $config = StorageConfigService::getStorageConfig(); + $validationRules = StorageConfigService::getFileValidationRules(); + + // Custom validation with user-friendly messages + $allowedTypes = isset($config['allowed_file_types']) && $config['allowed_file_types'] + ? strtoupper(str_replace(',', ', ', $config['allowed_file_types'])) + : __('Please check storage settings'); + + $validator = Validator::make($request->all(), [ + 'files' => 'required|array', + 'files.*' => array_merge(['file'], $validationRules), + ], [ + 'files.*.mimes' => __('Only specified file types are allowed: :types', [ + 'types' => $allowedTypes + ]), + 'files.*.max' => __('File size cannot exceed :max MB.', ['max' => $config['max_file_size_mb']]), + ]); + + + // Additional file validation + $allowedExtensions = array_map('trim', explode(',', strtolower($config['allowed_file_types']))); + $allowedTypesStr = strtoupper(implode(', ', $allowedExtensions)); + + foreach ($request->file('files') as $file) { + $extension = strtolower($file->getClientOriginalExtension()); + + if (!in_array($extension, $allowedExtensions)) { + return response()->json([ + 'message' => __('File type not allowed: :type', ['type' => strtoupper($extension)]), + 'errors' => [__('Only specified file types are allowed: :types', ['types' => $allowedTypesStr])] + ], 422); + } + } + + + if ($validator->fails()) { + return response()->json([ + 'message' => __('File validation failed'), + 'errors' => $validator->errors()->all(), + 'allowed_types' => $config['allowed_file_types'], + 'max_size_mb' => $config['max_file_size_mb'] + ], 422); + } + + $uploadedMedia = []; + $errors = []; + + foreach ($request->file('files') as $file) { + try { + // Configure dynamic storage before upload + DynamicStorageService::configureDynamicDisks(); + + $activeDisk = StorageConfigService::getActiveDisk(); + + // Store file directly to storage + $fileName = $file->getClientOriginalName(); + $hashedName = $file->hashName(); + $storedPath = $file->storeAs('media', $hashedName, $activeDisk); + + // Create media record directly + $media = new Media(); + $media->model_type = 'App\Models\User'; + $media->model_id = creatorId(); + $media->collection_name = 'files'; + $media->name = pathinfo($fileName, PATHINFO_FILENAME); + $media->file_name = $hashedName; + $media->mime_type = $file->getMimeType(); + $media->disk = $activeDisk; + $media->size = $file->getSize(); + $media->manipulations = []; + $media->custom_properties = []; + $media->generated_conversions = []; + $media->responsive_images = []; + $media->uuid = Str::uuid(); + + $media->created_by = creatorId(); + if ($request->has('directory_id') && $request->directory_id) { + $media->directory_id = $request->directory_id; + } + $media->save(); + + // Update user storage usage + if (isSaaS()) { + $this->updateStorageUsage(getUser(), $media->size); + } + + // Force thumbnail generationAdd commentMore actions + try { + $media->getUrl('thumb'); + } catch (\Exception $e) { + // Thumbnail generation failed, but continue + } + + $originalUrl = Storage::disk($activeDisk)->url('media/' . $hashedName); + $thumbUrl = $originalUrl; // Default to original + + $uploadedMedia[] = [ + 'id' => $media->id, + 'name' => $media->name, + 'file_name' => $media->file_name, + 'url' => $originalUrl, + 'thumb_url' => $thumbUrl, + 'size' => $media->size, + 'mime_type' => $media->mime_type, + 'creator_id' => $media->creator_id, + 'directory_id' => $media->directory_id, // Add this field + 'created_at' => $media->created_at, + ]; + } catch (\Exception $e) { + if (isset($storedPath) && Storage::disk($activeDisk)->exists($storedPath)) { + Storage::disk($activeDisk)->delete($storedPath); + } + + // Log the actual error for debugging + Log::error('Media upload failed', [ + 'file' => $file->getClientOriginalName(), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + + $errors[] = [ + 'file' => $file->getClientOriginalName(), + 'error' => $this->getUserFriendlyError($e, $file->getClientOriginalName()) + ]; + } + } + + if (count($uploadedMedia) > 0 && empty($errors)) { + return response()->json([ + 'message' => count($uploadedMedia) . __(' file(s) uploaded successfully'), + 'data' => $uploadedMedia + ]); + } elseif (count($uploadedMedia) > 0 && !empty($errors)) { + return response()->json([ + 'message' => count($uploadedMedia) . ' uploaded, ' . count($errors) . ' failed', + 'data' => $uploadedMedia, + 'errors' => array_column($errors, 'error') + ]); + } else { + return response()->json([ + 'message' => 'Upload failed', + 'errors' => array_column($errors, 'error') + ], 422); + } + } + + public function download($id) + { + $user = auth()->user(); + $query = Media::WithPermissionCheck()->where('id', $id); + + $media = $query->first(); + + if (!$media) { + return response()->json(['error' => __('Media file not found')], 404); + } + + try { + $disk = Storage::disk($media->disk); + $filePath = 'media/' . $media->file_name; + + if (!$disk->exists($filePath)) { + return response()->json(['error' => __('File not found on storage')], 404); + } + + // For all storage types, use download method + return $disk->download($filePath, $media->file_name); + } catch (\Exception $e) { + return response()->json(['error' => __('File storage unavailable: ') . $e->getMessage()], 500); + } + } + public function destroy($id) + { + $user = auth()->user(); + + // Check delete-media permission + if (!$user->hasPermissionTo('delete-media')) { + return response()->json(['error' => __('Permission denied')], 403); + } + + $query = Media::where('id', $id); + + // SuperAdmin and users with manage-any-media can delete any media + if ($user->type !== 'superadmin' && !$user->hasPermissionTo('manage-any-media')) { + $query->where('created_by', $user->id); + } + + $media = $query->firstOrFail(); + $mediaItem = $media->model; + + $fileSize = $media->size; + + try { + // Delete file from storage + Storage::disk($media->disk)->delete('media/' . $media->file_name); + $media->delete(); + } catch (\Exception $e) { + // If storage disk is unavailable, force delete from database + $media->forceDelete(); + } + + // Update user storage usage + if (isSaaS()) { + $this->updateStorageUsage(getUser(), -$fileSize); + } + + return response()->json(['message' => __('Media deleted successfully')]); + } + + private function checkStorageLimit($files) + { + $user = auth()->user(); + if ($user->type === 'superadmin') return null; + + $limit = $this->getUserStorageLimit($user); + if (!$limit) return null; + + $uploadSize = collect($files)->sum('size'); + $currentUsage = $this->getUserStorageUsage($user); + + if (($currentUsage + $uploadSize) > $limit) { + return response()->json([ + 'message' => __(key: 'Storage limit exceeded'), + 'errors' => [__('Please delete files or upgrade plan')] + ], 422); + } + + return null; + } + + private function getUserStorageLimit($user) + { + if ($user->type === 'company' && $user->plan) { + return $user->plan->storage_limit * 1024 * 1024; + } + + if ($user->created_by) { + $company = User::find($user->created_by); + if ($company && $company->plan) { + return $company->plan->storage_limit * 1024 * 1024; + } + } + + return null; + } + + private function getUserStorageUsage($user) + { + if ($user->type === 'company') { + return User::where('created_by', $user->id) + ->orWhere('id', $user->id) + ->sum('storage_limit'); + } + + if ($user->created_by) { + $company = User::find($user->created_by); + if ($company) { + return User::where('created_by', $company->id) + ->orWhere('id', $company->id) + ->sum('storage_limit'); + } + } + + return $user->storage_limit; + } + + private function updateStorageUsage($user, $size) + { + $user->increment('storage_limit', $size); + } + + public function createDirectory(Request $request) + { + $request->validate([ + 'name' => 'required|string|max:255', + ]); + + $slug = Str::slug($request->name . '-' . time()); + + $directory = MediaDirectory::create([ + 'name' => $request->name, + 'slug' => $slug, + 'created_by' => creatorId(), + ]); + + return response()->json([ + 'message' => __('Directory created successfully'), + 'directory' => $directory + ]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/MeetingAttendeeController.php b/app/Http/Controllers/MeetingAttendeeController.php new file mode 100644 index 000000000..1f7928ff0 --- /dev/null +++ b/app/Http/Controllers/MeetingAttendeeController.php @@ -0,0 +1,283 @@ +can('manage-meeting-attendees')) { + $query = MeetingAttendee::with(['meeting.type', 'user'])->where(function ($q) { + if (Auth::user()->can('manage-any-meeting-attendees')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-meeting-attendees')) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('user', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%'); + })->orWhereHas('meeting', function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('rsvp_status') && !empty($request->rsvp_status) && $request->rsvp_status !== 'all') { + $query->where('rsvp_status', $request->rsvp_status); + } + + if ($request->has('attendance_status') && !empty($request->attendance_status) && $request->attendance_status !== 'all') { + $query->where('attendance_status', $request->attendance_status); + } + + if ($request->has('meeting_id') && !empty($request->meeting_id) && $request->meeting_id !== 'all') { + $query->where('meeting_id', $request->meeting_id); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'rsvp_date') { + $query->orderBy('rsvp_date', $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $meetingAttendees = $query->paginate($request->per_page ?? 10); + + $meetings = Meeting::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'title', 'meeting_date') + ->orderBy('meeting_date', 'desc') + ->get(); + + $employees = User::whereIn('created_by', getCompanyAndUsersId()) + ->where('type', 'employee') + ->select('id', 'name') + ->get(); + + return Inertia::render('meetings/meeting-attendees/index', [ + 'meetingAttendees' => $meetingAttendees, + 'meetings' => $meetings, + 'employees' => $this->getFilteredEmployees(), + 'filters' => $request->all(['search', 'rsvp_status', 'attendance_status', 'meeting_id', 'per_page', 'sort_field', 'sort_direction']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-meeting-attendees') && !Auth::user()->can('manage-any-meeting-attendees')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + return $employees; + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'meeting_id' => 'required|exists:meetings,id', + 'user_id' => 'required|exists:users,id', + 'type' => 'required|in:Required,Optional', + 'rsvp_status' => 'nullable|in:Pending,Accepted,Declined,Tentative', + 'attendance_status' => 'nullable|in:Not Attended,Present,Late,Left Early', + 'decline_reason' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if attendee already exists + $exists = MeetingAttendee::where('meeting_id', $request->meeting_id) + ->where('user_id', $request->user_id) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('User is already added to this meeting')); + } + + MeetingAttendee::create([ + 'meeting_id' => $request->meeting_id, + 'user_id' => $request->user_id, + 'type' => $request->type, + 'rsvp_status' => $request->rsvp_status ?? 'Pending', + 'attendance_status' => $request->attendance_status ?? 'Not Attended', + 'rsvp_date' => $request->rsvp_status && $request->rsvp_status !== 'Pending' ? now() : null, + 'decline_reason' => $request->decline_reason, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Meeting attendee added successfully')); + } + + public function update(Request $request, MeetingAttendee $meetingAttendee) + { + if (!in_array($meetingAttendee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this attendee')); + } + + $validator = Validator::make($request->all(), [ + 'meeting_id' => 'required|exists:meetings,id', + 'user_id' => 'required|exists:users,id', + 'type' => 'required|in:Required,Optional', + 'rsvp_status' => 'nullable|in:Pending,Accepted,Declined,Tentative', + 'attendance_status' => 'nullable|in:Not Attended,Present,Late,Left Early', + 'decline_reason' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $meetingAttendee->update([ + 'meeting_id' => $request->meeting_id, + 'user_id' => $request->user_id, + 'type' => $request->type, + 'rsvp_status' => $request->rsvp_status ?? 'Pending', + 'attendance_status' => $request->attendance_status ?? 'Not Attended', + 'rsvp_date' => $request->rsvp_status && $request->rsvp_status !== 'Pending' ? now() : null, + 'decline_reason' => $request->decline_reason, + ]); + + return redirect()->back()->with('success', __('Meeting attendee updated successfully')); + } + + public function destroy(MeetingAttendee $meetingAttendee) + { + if (!in_array($meetingAttendee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this attendee')); + } + + $meetingAttendee->delete(); + return redirect()->back()->with('success', __('Meeting attendee removed successfully')); + } + + public function updateRsvp(Request $request, MeetingAttendee $meetingAttendee) + { + if (!in_array($meetingAttendee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this RSVP')); + } + + $validator = Validator::make($request->all(), [ + 'rsvp_status' => 'required|in:Pending,Accepted,Declined,Tentative', + 'decline_reason' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $meetingAttendee->update([ + 'rsvp_status' => $request->rsvp_status, + 'rsvp_date' => now(), + 'decline_reason' => $request->decline_reason, + ]); + + return redirect()->back()->with('success', __('RSVP updated successfully')); + } + + public function updateAttendance(Request $request, MeetingAttendee $meetingAttendee) + { + if (!in_array($meetingAttendee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update attendance')); + } + + $validator = Validator::make($request->all(), [ + 'attendance_status' => 'required|in:Not Attended,Present,Late,Left Early', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $meetingAttendee->update([ + 'attendance_status' => $request->attendance_status, + ]); + + return redirect()->back()->with('success', __('Attendance updated successfully')); + } + + public function updateMeetingRsvp(Request $request, MeetingAttendee $meetingAttendee) + { + if (!in_array($meetingAttendee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this RSVP')); + } + + $validator = Validator::make($request->all(), [ + 'rsvp_status' => 'required|in:Pending,Accepted,Declined,Tentative', + 'decline_reason' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $meetingAttendee->update([ + 'rsvp_status' => $request->rsvp_status, + 'rsvp_date' => now(), + 'decline_reason' => $request->decline_reason, + ]); + + return redirect()->back()->with('success', __('RSVP updated successfully')); + } + + public function updateMeetingAttendance(Request $request, MeetingAttendee $meetingAttendee) + { + if (!in_array($meetingAttendee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update attendance')); + } + + $validator = Validator::make($request->all(), [ + 'attendance_status' => 'required|in:Not Attended,Present,Late,Left Early', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $meetingAttendee->update([ + 'attendance_status' => $request->attendance_status, + ]); + + return redirect()->back()->with('success', __('Attendance updated successfully')); + } +} diff --git a/app/Http/Controllers/MeetingController.php b/app/Http/Controllers/MeetingController.php new file mode 100644 index 000000000..6aff3e2ea --- /dev/null +++ b/app/Http/Controllers/MeetingController.php @@ -0,0 +1,308 @@ +can('manage-meetings')) { + $query = Meeting::with(['type', 'room', 'organizer'])->where(function ($q) { + if (Auth::user()->can('manage-any-meetings')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-meetings')) { + $q->where('created_by', Auth::id())->orWhere('organizer_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('type_id') && !empty($request->type_id) && $request->type_id !== 'all') { + $query->where('type_id', $request->type_id); + } + + if ($request->has('organizer_id') && !empty($request->organizer_id) && $request->organizer_id !== 'all') { + $query->where('organizer_id', $request->organizer_id); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['title', 'meeting_date'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $meetings = $query->paginate($request->per_page ?? 10); + + $meetingTypes = MeetingType::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + $meetingRooms = MeetingRoom::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name', 'type') + ->get(); + + $employees = User::whereIn('created_by', getCompanyAndUsersId()) + ->where('type', 'employee') + ->select('id', 'name') + ->get(); + + return Inertia::render('meetings/meetings/index', [ + 'meetings' => $meetings, + 'meetingTypes' => $meetingTypes, + 'meetingRooms' => $meetingRooms, + 'employees' => $this->getFilteredEmployees(), + 'filters' => $request->all(['search', 'status', 'type_id', 'organizer_id', 'per_page', 'sort_field', 'sort_direction']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-meetings') && !Auth::user()->can('manage-any-meetings')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + return $employees; + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'type_id' => 'required|exists:meeting_types,id', + 'room_id' => 'nullable|exists:meeting_rooms,id', + 'meeting_date' => 'required|date|after_or_equal:today', + 'start_time' => 'required|date_format:H:i', + 'end_time' => 'required|date_format:H:i|after:start_time', + 'agenda' => 'nullable|string', + 'recurrence' => 'required|in:None,Daily,Weekly,Monthly', + 'recurrence_end_date' => 'nullable|date|after:meeting_date', + 'organizer_id' => 'required|exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $startTime = Carbon::createFromFormat('H:i', $request->start_time); + $endTime = Carbon::createFromFormat('H:i', $request->end_time); + $duration = $endTime->diffInMinutes($startTime); + + $meetingData = [ + 'title' => $request->title, + 'description' => $request->description, + 'type_id' => $request->type_id, + 'room_id' => $request->room_id, + 'start_time' => $request->start_time, + 'end_time' => $request->end_time, + 'duration' => $duration, + 'agenda' => $request->agenda, + 'recurrence' => $request->recurrence, + 'recurrence_end_date' => $request->recurrence_end_date, + 'organizer_id' => $request->organizer_id, + 'created_by' => creatorId(), + ]; + + // Create meetings based on recurrence + $this->createRecurringMeetings($meetingData, $request->meeting_date, $request->recurrence, $request->recurrence_end_date); + + return redirect()->back()->with('success', __('Meeting created successfully')); + } + + public function update(Request $request, Meeting $meeting) + { + if (!in_array($meeting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this meeting')); + } + + // Convert time format if needed + if ($request->start_time) { + // Handle different time formats (HH:MM:SS to HH:MM) + if (strlen($request->start_time) === 8) { + $request->merge(['start_time' => substr($request->start_time, 0, 5)]); + } + } + if ($request->end_time) { + // Handle different time formats (HH:MM:SS to HH:MM) + if (strlen($request->end_time) === 8) { + $request->merge(['end_time' => substr($request->end_time, 0, 5)]); + } + } + + $validator = Validator::make($request->all(), [ + 'title' => 'required|string|max:255', + 'description' => 'nullable|string', + 'type_id' => 'required|exists:meeting_types,id', + 'room_id' => 'nullable|exists:meeting_rooms,id', + 'meeting_date' => 'required|date', + 'start_time' => 'required|date_format:H:i', + 'end_time' => 'required|date_format:H:i', + 'agenda' => 'nullable|string', + 'recurrence' => 'required|in:None,Daily,Weekly,Monthly', + 'recurrence_end_date' => 'nullable|date|after_or_equal:meeting_date', + 'organizer_id' => 'required|exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $startTime = Carbon::createFromFormat('H:i', $request->start_time); + $endTime = Carbon::createFromFormat('H:i', $request->end_time); + $duration = $endTime->diffInMinutes($startTime); + + $meeting->update([ + 'title' => $request->title, + 'description' => $request->description, + 'type_id' => $request->type_id, + 'room_id' => $request->room_id, + 'meeting_date' => $request->meeting_date, + 'start_time' => $request->start_time, + 'end_time' => $request->end_time, + 'duration' => $duration, + 'agenda' => $request->agenda, + 'recurrence' => $request->recurrence, + 'recurrence_end_date' => $request->recurrence_end_date, + 'organizer_id' => $request->organizer_id, + ]); + + return redirect()->back()->with('success', __('Meeting updated successfully')); + } + + public function destroy(Meeting $meeting) + { + if (!in_array($meeting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this meeting')); + } + + $meeting->delete(); + return redirect()->back()->with('success', __('Meeting deleted successfully')); + } + + public function updateStatus(Request $request, Meeting $meeting) + { + if (!in_array($meeting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this meeting')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:Scheduled,In Progress,Completed,Cancelled', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $meeting->update(['status' => $request->status]); + return redirect()->back()->with('success', __('Meeting status updated successfully')); + } + + public function updateMeetingStatus(Request $request, Meeting $meeting) + { + if (!in_array($meeting->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this meeting status')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:Scheduled,In Progress,Completed,Cancelled', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $meeting->update(['status' => $request->status]); + return redirect()->back()->with('success', __('Meeting status updated successfully')); + } + + private function createRecurringMeetings($meetingData, $startDate, $recurrence, $endDate) + { + $currentDate = Carbon::parse($startDate); + $endDate = $endDate ? Carbon::parse($endDate) : null; + $meetings = []; + + // Create first meeting + $meetingData['meeting_date'] = $currentDate->format('Y-m-d'); + $meetings[] = Meeting::create($meetingData); + + // Create recurring meetings if not 'None' + if ($recurrence !== 'None' && $endDate) { + while ($currentDate->lt($endDate)) { + switch ($recurrence) { + case 'Daily': + $currentDate->addDay(); + break; + case 'Weekly': + $currentDate->addWeek(); + break; + case 'Monthly': + $currentDate->addMonth(); + break; + } + + if ($currentDate->lte($endDate)) { + $meetingData['meeting_date'] = $currentDate->format('Y-m-d'); + $meetings[] = Meeting::create($meetingData); + } + } + } + + return $meetings; + } +} diff --git a/app/Http/Controllers/MeetingMinuteController.php b/app/Http/Controllers/MeetingMinuteController.php new file mode 100644 index 000000000..e2db36d73 --- /dev/null +++ b/app/Http/Controllers/MeetingMinuteController.php @@ -0,0 +1,185 @@ +can('manage-meeting-minutes')) { + $query = MeetingMinute::with(['meeting.type', 'recorder'])->where(function ($q) { + if (Auth::user()->can('manage-any-meeting-minutes')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-meeting-minutes')) { + $q->where('created_by', Auth::id())->orWhere('recorded_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('topic', 'like', '%' . $request->search . '%') + ->orWhere('content', 'like', '%' . $request->search . '%') + ->orWhereHas('meeting', function ($mq) use ($request) { + $mq->where('title', 'like', '%' . $request->search . '%'); + }); + }); + } + + if ($request->has('type') && !empty($request->type) && $request->type !== 'all') { + $query->where('type', $request->type); + } + + if ($request->has('meeting_id') && !empty($request->meeting_id) && $request->meeting_id !== 'all') { + $query->where('meeting_id', $request->meeting_id); + } + + if ($request->has('recorded_by') && !empty($request->recorded_by) && $request->recorded_by !== 'all') { + $query->where('recorded_by', $request->recorded_by); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['topic', 'recorded_at'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $meetingMinutes = $query->paginate($request->per_page ?? 10); + + $meetings = Meeting::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'title', 'meeting_date') + ->orderBy('meeting_date', 'desc') + ->get(); + + $employees = User::whereIn('created_by', getCompanyAndUsersId()) + ->where('type', 'employee') + ->select('id', 'name') + ->get(); + + return Inertia::render('meetings/meeting-minutes/index', [ + 'meetingMinutes' => $meetingMinutes, + 'meetings' => $meetings, + 'employees' => $this->getFilteredEmployees(), + 'filters' => $request->all(['search', 'type', 'meeting_id', 'recorded_by', 'per_page', 'sort_field', 'sort_direction']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-meeting-minutes') && !Auth::user()->can('manage-any-meeting-minutes')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + return $employees; + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'meeting_id' => 'required|exists:meetings,id', + 'topic' => 'required|string|max:255', + 'content' => 'required|string', + 'type' => 'required|in:Discussion,Decision,Action Item,Note', + 'recorded_by' => 'required|exists:users,id', + 'recorded_at' => 'nullable|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + MeetingMinute::create([ + 'meeting_id' => $request->meeting_id, + 'topic' => $request->topic, + 'content' => $request->content, + 'type' => $request->type, + 'recorded_by' => $request->recorded_by, + 'recorded_at' => $request->recorded_at ?? now(), + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Meeting minute created successfully')); + } + + public function update(Request $request, MeetingMinute $meetingMinute) + { + if (!in_array($meetingMinute->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this minute')); + } + + $validator = Validator::make($request->all(), [ + 'meeting_id' => 'required|exists:meetings,id', + 'topic' => 'required|string|max:255', + 'content' => 'required|string', + 'type' => 'required|in:Discussion,Decision,Action Item,Note', + 'recorded_by' => 'required|exists:users,id', + 'recorded_at' => 'nullable|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $meetingMinute->update([ + 'meeting_id' => $request->meeting_id, + 'topic' => $request->topic, + 'content' => $request->content, + 'type' => $request->type, + 'recorded_by' => $request->recorded_by, + 'recorded_at' => $request->recorded_at ?? $meetingMinute->recorded_at, + ]); + + return redirect()->back()->with('success', __('Meeting minute updated successfully')); + } + + public function destroy(MeetingMinute $meetingMinute) + { + if (!in_array($meetingMinute->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this minute')); + } + + $meetingMinute->delete(); + return redirect()->back()->with('success', __('Meeting minute deleted successfully')); + } +} diff --git a/app/Http/Controllers/MeetingRoomController.php b/app/Http/Controllers/MeetingRoomController.php new file mode 100644 index 000000000..21c00d87f --- /dev/null +++ b/app/Http/Controllers/MeetingRoomController.php @@ -0,0 +1,160 @@ +can('manage-meeting-rooms')) { + $query = MeetingRoom::withCount('meetings')->where(function ($q) { + if (Auth::user()->can('manage-any-meeting-rooms')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-meeting-rooms')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhere('location', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('type') && !empty($request->type) && $request->type !== 'all') { + $query->where('type', $request->type); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'name') { + $query->orderBy('name', $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $meetingRooms = $query->paginate($request->per_page ?? 10); + + return Inertia::render('meetings/meeting-rooms/index', [ + 'meetingRooms' => $meetingRooms, + 'filters' => $request->all(['search', 'type', 'status', 'per_page', 'sort_field', 'sort_direction']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'type' => 'required|in:Physical,Virtual', + 'location' => 'nullable|string|max:255', + 'capacity' => 'required|integer|min:1', + 'equipment' => 'nullable|array', + 'booking_url' => 'nullable', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + MeetingRoom::create([ + 'name' => $request->name, + 'description' => $request->description, + 'type' => $request->type, + 'location' => $request->location, + 'capacity' => $request->capacity, + 'equipment' => $request->equipment, + 'booking_url' => $request->booking_url, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Meeting room created successfully')); + } + + public function update(Request $request, MeetingRoom $meetingRoom) + { + if (!in_array($meetingRoom->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this meeting room')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'type' => 'required|in:Physical,Virtual', + 'location' => 'nullable|string|max:255', + 'capacity' => 'required|integer|min:1', + 'equipment' => 'nullable|array', + 'booking_url' => 'nullable|url', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $meetingRoom->update([ + 'name' => $request->name, + 'description' => $request->description, + 'type' => $request->type, + 'location' => $request->location, + 'capacity' => $request->capacity, + 'equipment' => $request->equipment, + 'booking_url' => $request->booking_url, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Meeting room updated successfully')); + } + + public function destroy(MeetingRoom $meetingRoom) + { + if (!in_array($meetingRoom->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this meeting room')); + } + + if ($meetingRoom->meetings()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete meeting room as it is being used in meetings')); + } + + $meetingRoom->delete(); + return redirect()->back()->with('success', __('Meeting room deleted successfully')); + } + + public function toggleStatus(MeetingRoom $meetingRoom) + { + if (!in_array($meetingRoom->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this meeting room')); + } + + $meetingRoom->update([ + 'status' => $meetingRoom->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Meeting room status updated successfully')); + } +} diff --git a/app/Http/Controllers/MeetingTypeController.php b/app/Http/Controllers/MeetingTypeController.php new file mode 100644 index 000000000..4c0a7f12b --- /dev/null +++ b/app/Http/Controllers/MeetingTypeController.php @@ -0,0 +1,143 @@ +can('manage-meeting-types')) { + $query = MeetingType::withCount('meetings')->where(function ($q) { + if (Auth::user()->can('manage-any-meeting-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-meeting-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['name', 'created_at'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $meetingTypes = $query->paginate($request->per_page ?? 10); + + return Inertia::render('meetings/meeting-types/index', [ + 'meetingTypes' => $meetingTypes, + 'filters' => $request->all(['search', 'status', 'per_page', 'sort_field', 'sort_direction']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'color' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/', + 'default_duration' => 'required|integer|min:15|max:480', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + MeetingType::create([ + 'name' => $request->name, + 'description' => $request->description, + 'color' => $request->color, + 'default_duration' => $request->default_duration, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Meeting type created successfully')); + } + + public function update(Request $request, MeetingType $meetingType) + { + if (!in_array($meetingType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this meeting type')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'color' => 'required|string|regex:/^#[0-9A-Fa-f]{6}$/', + 'default_duration' => 'required|integer|min:15|max:480', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $meetingType->update([ + 'name' => $request->name, + 'description' => $request->description, + 'color' => $request->color, + 'default_duration' => $request->default_duration, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Meeting type updated successfully')); + } + + public function destroy(MeetingType $meetingType) + { + if (!in_array($meetingType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this meeting type')); + } + + if ($meetingType->meetings()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete meeting type as it is being used in meetings')); + } + + $meetingType->delete(); + return redirect()->back()->with('success', __('Meeting type deleted successfully')); + } + + public function toggleStatus(MeetingType $meetingType) + { + if (!in_array($meetingType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this meeting type')); + } + + $meetingType->update([ + 'status' => $meetingType->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Meeting type status updated successfully')); + } +} diff --git a/app/Http/Controllers/MercadoPagoController.php b/app/Http/Controllers/MercadoPagoController.php new file mode 100644 index 000000000..0ea0b3209 --- /dev/null +++ b/app/Http/Controllers/MercadoPagoController.php @@ -0,0 +1,405 @@ + $accessToken, + 'mode' => $settings['payment_settings']['mercadopago_mode'] ?? 'sandbox', + 'currency' => $settings['general_settings']['defaultCurrency'] ?? 'BRL' + ]; + } + + /** + * Create a MercadoPago checkout preference + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse + */ + public function createPreference(Request $request) + { + try { + $request->validate([ + 'plan_id' => 'required|exists:plans,id', + 'billing_cycle' => 'nullable|in:monthly,yearly', + 'coupon_code' => 'nullable|string', + ]); + + // Support for both billing_cycle and coupon from form + $billingCycle = $request->billing_cycle ?? 'monthly'; + $couponCode = $request->coupon_code ?? $request->coupon ?? null; + + $plan = Plan::findOrFail($request->plan_id); + $amount = $plan->getPriceForCycle($billingCycle); + + // Apply coupon if provided + if ($couponCode) { + // Get coupon and apply discount + $coupon = Coupon::where('code', strtoupper($couponCode)) + ->where('is_active', '1') + ->first(); + + if ($coupon) { + $usedCoupon = $coupon->used_coupon(); + if ($usedCoupon < $coupon->limit) { + if ($coupon->type == 'percentage') { + $discount = ($amount / 100) * $coupon->discount; + } else { + $discount = $coupon->discount; + } + + // Check min/max spend + if ($amount >= $coupon->minimum_spend && ($coupon->maximum_spend == 0 || $amount <= $coupon->maximum_spend)) { + $amount = $amount - $discount; + } + } + } + } + + // Get MercadoPago credentials + $credentials = $this->getMercadoPagoCredentials(); + if (!$credentials['access_token']) { + throw new \Exception(__('MercadoPago API credentials not found')); + } + + // Initialize MercadoPago SDK + try { + $accessToken = $credentials['access_token']; + + // For MercadoPago, access tokens for API v1 should start with APP_USR- or TEST- + if (empty($accessToken)) { + throw new \Exception(__('MercadoPago access token is empty')); + } + + // Set the access token + SDK::setAccessToken($accessToken); + + // Set SDK configurations + SDK::setIntegratorId("dev_vcardgo"); + } catch (\Exception $e) { + throw new \Exception(__('Failed to initialize MercadoPago SDK: :message', ['message' => $e->getMessage()])); + } + + // Create preference + $preference = new Preference(); + + // Create item with required fields + $item = new Item(); + $item->title = "Plan: " . $plan->name . " (" . $request->billing_cycle . ")"; + $item->quantity = 1; + $item->unit_price = (float)$amount; + $item->currency_id = $credentials['currency']; + $item->id = "plan_" . $plan->id; + + $preference->items = [$item]; + + // Set back URLs - use absolute URLs with proper route generation + $preference->back_urls = [ + "success" => route('mercadopago.success'), + "failure" => route('mercadopago.failure'), + "pending" => route('mercadopago.pending') + ]; + + // Don't set auto_return as it's causing issues + // $preference->auto_return = "approved"; + + // Set external reference + $externalReference = 'plan_' . $plan->id . '_' . auth()->id() . '_' . $billingCycle; + if ($couponCode) { + $externalReference .= '_coupon_' . $couponCode; + } + $preference->external_reference = $externalReference; + + // Set notification URL + $preference->notification_url = route('mercadopago.webhook'); + + // Set additional required fields + $preference->binary_mode = true; // No pending status, only success or failure + + // Set payer information if available + if (auth()->check()) { + $payer = new \MercadoPago\Payer(); + $payer->name = auth()->user()->name; + $payer->email = auth()->user()->email; + $preference->payer = $payer; + } + + // Save preference with better error handling + try { + $result = $preference->save(); + + if (!$result) { + throw new \Exception(__('Failed to save MercadoPago preference')); + } + } catch (\Exception $e) { + throw new \Exception(message: __('Failed to save MercadoPago preference: :message', ['message' => $e->getMessage()])); + } + + // Check if preference was created successfully + if (!$preference->id) { + throw new \Exception(__('MercadoPago preference was not created properly')); + } + + // Determine redirect URL based on mode + $redirectUrl = $credentials['mode'] === 'sandbox' ? $preference->sandbox_init_point : $preference->init_point; + + if (!$redirectUrl) { + throw new \Exception(__('MercadoPago redirect URL is not available')); + } + + // Return response based on request type + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'checkout_url' => $preference->init_point, + 'sandbox_url' => $preference->sandbox_init_point, + 'redirect_url' => $redirectUrl, + 'preference_id' => $preference->id, + 'mode' => $credentials['mode'] + ]); + } + + // For form submissions, redirect directly + return redirect($redirectUrl); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json(['error' => __('Failed to create payment preference: :message', ['message' => $e->getMessage()])], 500); + } + return redirect()->back()->with('error', __('Failed to create payment preference: :message', ['message' => $e->getMessage()])); + } + } + + /** + * Handle successful payment for plans + */ + public function success(Request $request, $plan_id = null, $coupon_id = null, $flag = null) + { + try { + $paymentId = $request->payment_id; + $status = $request->status ?? $flag; + $externalReference = $request->external_reference; + $preferenceId = $request->preference_id; + + // Handle plan.mercado.callback route + if ($plan_id && $request->routeIs('plan.mercado.callback')) { + $planId = $plan_id; + $couponCode = $request->query('coupon_id'); + $status = $request->query('flag') ?? $flag; + } + + // If we don't have plan_id from the route, try to get it from external reference + if (!isset($planId) && $externalReference) { + // Parse external reference + $parts = explode('_', $externalReference); + if (count($parts) < 4) { + return redirect()->route('plans.index')->with('error', __('Invalid payment reference format')); + } + + $planId = (int)$parts[1]; + $userId = (int)$parts[2]; + $billingCycle = $parts[3]; + + // Check if coupon was used + if (count($parts) > 5 && $parts[4] === 'coupon') { + $couponCode = $parts[5]; + } + } else if (!isset($planId)) { + return redirect()->route('plans.index')->with('error', __('Invalid payment reference')); + } + + // Set default values if not set + $userId = $userId ?? auth()->id(); + $billingCycle = $billingCycle ?? 'monthly'; + + // Verify user - skip for plan.mercado.callback route which might have a different user ID + if ($userId !== auth()->id() && !request()->routeIs('plan.mercado.callback')) { + return redirect()->route('plans.index')->with('error', __('Unauthorized payment reference')); + } + + // Get plan + $plan = Plan::find($planId); + if (!$plan) { + return redirect()->route('plans.index')->with('error', __('Plan not found')); + } + + // Create plan order + $planOrder = new PlanOrder(); + $planOrder->plan_id = $planId; + $planOrder->user_id = $userId; + $planOrder->payment_method = 'mercadopago'; + $planOrder->payment_id = $paymentId; + $planOrder->amount = $plan->getPriceForCycle($billingCycle); + $planOrder->billing_cycle = $billingCycle; + $planOrder->status = 'completed'; + $planOrder->coupon_code = $couponCode ?? null; + $planOrder->save(); + + // Activate subscription + $planOrder->activateSubscription(); + + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'message' => __('Payment successful! Your subscription has been activated.') + ]); + } + + return redirect()->route('plans.index')->with('success', __('Payment successful! Your subscription has been activated.')); + } catch (\Exception $e) { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'error' => __('Failed to process payment: :message',['message' => $e->getMessage()]) + ], 500); + } + + return redirect()->route('plans.index')->with('error', __('Failed to process payment: :message',['message' => $e->getMessage()])); + } + } + + /** + * Handle failed payment + */ + public function failure(Request $request) + { + if ($request->expectsJson()) { + return response()->json([ + 'success' => false, + 'error' => __('Payment failed. Please try again.') + ], 400); + } + + return redirect()->route('plans.index')->with('error', __('Payment failed. Please try again.')); + } + + /** + * Handle pending payment + */ + public function pending(Request $request) + { + if ($request->expectsJson()) { + return response()->json([ + 'success' => true, + 'status' => 'pending', + 'message' => __('Your payment is pending. We will notify you once it is confirmed.') + ]); + } + + return redirect()->route('plans.index')->with('info', __('Your payment is pending. We will notify you once it is confirmed.')); + } + + /** + * Handle MercadoPago webhook notifications + */ + public function webhook(Request $request) + { + try { + $data = $request->all(); + + // Acknowledge receipt of the webhook + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['status' => 'error', 'message' => $e->getMessage()], 500); + } + } + + /** + * Process direct card payment + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function processPayment(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'token' => 'required|string', + 'payment_method_id' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + + // Get MercadoPago credentials + $credentials = $this->getMercadoPagoCredentials(); + + if (!$credentials['access_token']) { + throw new \Exception(__('MercadoPago API credentials not found')); + } + + // Initialize MercadoPago SDK + try { $accessToken = $credentials['access_token']; + + SDK::setAccessToken($accessToken); + } catch (\Exception $e) { + throw new \Exception(__('Failed to initialize MercadoPago SDK: :message', ['message' => $e->getMessage()])); + } + + $payment = new Payment(); + $payment->transaction_amount = (float)$pricing['final_price']; + $payment->token = $validated['token']; + $payment->description = "Plan: " . $plan->name; + $payment->installments = 1; + $payment->payment_method_id = $validated['payment_method_id']; + $payment->payer = array("email" => auth()->user()->email); + + $payment->save(); + + if ($payment->status == 'approved') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'] ?? 'monthly', + 'payment_method' => 'mercadopago', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $payment->id, + ]); + + return response()->json([ + 'success' => true, + 'message' => __('Payment successful! Your subscription has been activated.') + ]); + } else if ($payment->status == 'in_process' || $payment->status == 'pending') { + return response()->json([ + 'success' => true, + 'status' => 'pending', + 'message' => __('Your payment is being processed. We will notify you once it is confirmed.') + ]); + } else { + return response()->json([ + 'success' => false, + 'error' => __('Payment failed: :status', ['status' => $payment->status_detail]) + ], 400); + } + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'error' => __('Failed to process payment: :message', ['message' => $e->getMessage()]) + ], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/MidtransPaymentController.php b/app/Http/Controllers/MidtransPaymentController.php new file mode 100644 index 000000000..2f3801996 --- /dev/null +++ b/app/Http/Controllers/MidtransPaymentController.php @@ -0,0 +1,188 @@ + 'required|string', + 'order_id' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['midtrans_secret_key'])) { + return back()->withErrors(['error' => __('Midtrans not configured')]); + } + + if (in_array($validated['transaction_status'], ['capture', 'settlement'])) { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'midtrans', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['order_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'midtrans'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['midtrans_secret_key'])) { + return response()->json(['error' => __('Midtrans not configured')], 400); + } + + $user = auth()->user(); + $orderId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + + // Convert to IDR (whole numbers only, no cents) + $amount = intval($pricing['final_price']); + + $paymentData = [ + 'transaction_details' => [ + 'order_id' => $orderId, + 'gross_amount' => $amount + ], + 'credit_card' => [ + 'secure' => true + ], + 'customer_details' => [ + 'first_name' => $user->name ?? 'Customer', + 'email' => $user->email, + ], + 'item_details' => [ + [ + 'id' => $plan->id, + 'price' => $amount, + 'quantity' => 1, + 'name' => $plan->name + ] + ] + ]; + + $snapToken = $this->createSnapToken($paymentData, $settings['payment_settings']); + + if ($snapToken) { + $baseUrl = $settings['payment_settings']['midtrans_mode'] === 'live' + ? 'https://app.midtrans.com' + : 'https://app.sandbox.midtrans.com'; + + return response()->json([ + 'success' => true, + 'snap_token' => $snapToken, + 'payment_url' => $baseUrl . '/snap/v1/transactions/' . $snapToken, + 'order_id' => $orderId + ]); + } + + throw new \Exception(__('Failed to create Midtrans snap token')); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function callback(Request $request) + { + try { + $orderId = $request->input('order_id'); + $transactionStatus = $request->input('transaction_status'); + + if ($orderId && in_array($transactionStatus, ['capture', 'settlement'])) { + $parts = explode('_', $orderId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = \App\Models\User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', + 'payment_method' => 'midtrans', + 'payment_id' => $request->input('transaction_id'), + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } + + private function createSnapToken($paymentData, $settings) + { + try { + $baseUrl = $settings['midtrans_mode'] === 'live' + ? 'https://app.midtrans.com' + : 'https://app.sandbox.midtrans.com'; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $baseUrl . '/snap/v1/transactions'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($paymentData)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Basic ' . base64_encode($settings['midtrans_secret_key'] . ':'), + 'Content-Type: application/json', + 'Accept: application/json' + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($curlError) { + throw new \Exception(__('cURL Error: ') . $curlError); + } + + if ($httpCode !== 201) { + throw new \Exception(__('HTTP Error: ') . $httpCode . ' - ' . $response); + } + + $result = json_decode($response, true); + + if (!isset($result['token'])) { + throw new \Exception(__('No token in response: ') . $response); + } + + return $result['token']; + + } catch (\Exception $e) { + return false; + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/MolliePaymentController.php b/app/Http/Controllers/MolliePaymentController.php new file mode 100644 index 000000000..83dda8858 --- /dev/null +++ b/app/Http/Controllers/MolliePaymentController.php @@ -0,0 +1,257 @@ + $settings['payment_settings']['mollie_api_key'] ?? null, + 'currency' => $settings['general_settings']['defaultCurrency'] ?? 'EUR' + ]; + } + + public function processPayment(Request $request) + { + $validated = $request->validate([ + 'plan_id' => 'required|exists:plans,id', + 'billing_cycle' => 'required|in:monthly,yearly', + 'coupon_code' => 'nullable|string', + 'customer_details' => 'required|array', + 'customer_details.firstName' => 'required|string', + 'customer_details.lastName' => 'required|string', + 'customer_details.email' => 'required|email' + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $credentials = $this->getMollieCredentials(); + + if (!$credentials['api_key']) { + return back()->withErrors(['error' => __('Mollie not configured')]); + } + + $paymentId = 'mollie_' . $plan->id . '_' . time() . '_' . uniqid(); + + // Create pending order + createPlanOrder([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'mollie', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $paymentId, + 'status' => 'pending' + ]); + + // Initialize Mollie SDK + $mollie = new MollieApiClient(); + $mollie->setApiKey($credentials['api_key']); + + $paymentData = [ + 'amount' => [ + 'currency' => $credentials['currency'], + 'value' => number_format($pricing['final_price'], 2, '.', '') + ], + 'description' => 'Plan Subscription - ' . $plan->name, + 'redirectUrl' => route('mollie.success'), + 'metadata' => [ + 'payment_id' => $paymentId, + 'plan_id' => $plan->id, + 'user_id' => auth()->id(), + 'billing_cycle' => $validated['billing_cycle'] + ] + ]; + + // Only add webhook URL if not localhost + if (!str_contains(config('app.url'), 'localhost')) { + $paymentData['webhookUrl'] = route('mollie.callback'); + } + + $payment = $mollie->payments->create($paymentData); + + // Update the plan order with the actual Mollie payment ID + PlanOrder::where('payment_id', $paymentId) + ->update(['payment_id' => $payment->id, 'notes' => __('Mollie Payment ID: ') . $payment->id]); + + return redirect($payment->getCheckoutUrl()); + + } catch (\Exception $e) { + return back()->withErrors(['error' => __('Payment failed. Please try again.')]); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'customer_name' => 'required|string', + 'customer_email' => 'required|email', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $credentials = $this->getMollieCredentials(); + + if (!$credentials['api_key']) { + throw new \Exception(__('Mollie API key not configured')); + } + + $paymentId = 'mollie_' . $plan->id . '_' . time() . '_' . uniqid(); + + // Create pending order + createPlanOrder([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'mollie', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $paymentId, + 'status' => 'pending' + ]); + + // Initialize Mollie SDK + $mollie = new MollieApiClient(); + $mollie->setApiKey($credentials['api_key']); + + $payment = $mollie->payments->create([ + 'amount' => [ + 'currency' => $credentials['currency'], + 'value' => number_format($pricing['final_price'], 2, '.', '') + ], + 'description' => 'Plan Subscription - ' . $plan->name, + 'redirectUrl' => route('mollie.success'), + 'webhookUrl' => route('mollie.callback'), + 'metadata' => [ + 'payment_id' => $paymentId, + 'plan_id' => $plan->id, + 'user_id' => auth()->id(), + 'billing_cycle' => $validated['billing_cycle'] + ] + ]); + + // Update the plan order with the actual Mollie payment ID + PlanOrder::where('payment_id', $paymentId) + ->update(['payment_id' => $payment->id, 'notes' => 'Mollie Payment ID: ' . $payment->id]); + + return response()->json([ + 'success' => true, + 'payment_id' => $payment->id, + 'checkout_url' => $payment->getCheckoutUrl() + ]); + + } catch (\Exception $e) { + return response()->json(['error' => $e->getMessage()], 500); + } + } + + public function checkPaymentStatus(Request $request) + { + $validated = $request->validate([ + 'payment_id' => 'required|string' + ]); + + try { + $credentials = $this->getMollieCredentials(); + $mollie = new MollieApiClient(); + $mollie->setApiKey($credentials['api_key']); + + $payment = $mollie->payments->get($validated['payment_id']); + + return response()->json([ + 'status' => $payment->status, + 'is_paid' => $payment->isPaid(), + 'is_failed' => $payment->isFailed(), + 'is_canceled' => $payment->isCanceled() + ]); + + } catch (\Exception $e) { + return response()->json(['error' => $e->getMessage()], 500); + } + } + + public function success(Request $request) + { + try { + $credentials = $this->getMollieCredentials(); + + if (!$credentials['api_key']) { + return redirect()->route('plans.index')->with('error', __('Payment configuration error.')); + } + + // Find the most recent pending order for this user + $userId = auth()->id(); + if ($userId) { + $planOrder = PlanOrder::where('user_id', $userId) + ->where('status', 'pending') + ->where('payment_method', 'mollie') + ->orderBy('created_at', 'desc') + ->first(); + + if ($planOrder) { + $mollie = new MollieApiClient(); + $mollie->setApiKey($credentials['api_key']); + + try { + $payment = $mollie->payments->get($planOrder->payment_id); + + if ($payment->isPaid()) { + $planOrder->update(['status' => 'approved']); + $planOrder->activateSubscription(); + + return redirect()->route('plans.index')->with('success', __('Payment completed successfully! Your plan has been activated.')); + } elseif ($payment->status === 'pending') { + return redirect()->route('plans.index')->with('info', __('Payment is being processed. Your plan will be activated shortly.')); + } else { + return redirect()->route('plans.index')->with('error', __('Payment was not successful. Please try again.')); + } + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('info', __('Payment is being processed. Your plan will be activated shortly.')); + } + } + } + + return redirect()->route('plans.index')->with('info', __('Payment is being processed. Your plan will be activated shortly.')); + + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment verification failed. Please contact support.')); + } + } + + public function callback(Request $request) + { + try { + $paymentId = $request->input('id'); + $credentials = $this->getMollieCredentials(); + + $mollie = new MollieApiClient(); + $mollie->setApiKey($credentials['api_key']); + + $payment = $mollie->payments->get($paymentId); + + if ($payment->isPaid()) { + $planOrder = PlanOrder::where('payment_id', $paymentId)->first(); + + if ($planOrder && $planOrder->status === 'pending') { + $planOrder->update(['status' => 'approved']); + $planOrder->activateSubscription(); + } + } + + return response('OK', 200); + } catch (\Exception $e) { + return response('ERROR', 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/NepalstePaymentController.php b/app/Http/Controllers/NepalstePaymentController.php new file mode 100644 index 000000000..d10c71a0c --- /dev/null +++ b/app/Http/Controllers/NepalstePaymentController.php @@ -0,0 +1,255 @@ + 'required|string', + 'status' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['nepalste_public_key']) || !isset($settings['payment_settings']['nepalste_secret_key'])) { + return back()->withErrors(['error' => __('Nepalste not configured')]); + } + + if ($validated['status'] === 'completed') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'nepalste', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['payment_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return back()->withErrors(['error' => __('Payment processing failed')]); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['nepalste_public_key']) || !isset($settings['payment_settings']['nepalste_secret_key'])) { + return response()->json(['error' => __('Nepalste not configured')], 400); + } + + $user = auth()->user(); + $orderId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + + // First get access token + $accessToken = $this->getAccessToken($settings['payment_settings']); + if (!$accessToken) { + return response()->json(['error' => __('Failed to get access token')], 500); + } + + $paymentData = [ + 'amount' => $pricing['final_price'], + 'purchase_order_id' => $orderId, + 'purchase_order_name' => $plan->name, + 'return_url' => route('nepalste.success', ['order_id' => $orderId, 'plan_id' => $plan->id, 'billing_cycle' => $validated['billing_cycle']]), + 'website_url' => route('plans.index'), + ]; + + $baseUrl = $settings['payment_settings']['nepalste_mode'] === 'live' + ? 'https://nepalste.com.np/pay/api/v1' + : 'https://nepalste.com.np/pay/sandbox/api/v1'; + + $response = $this->initiateNepalstePayment($baseUrl . '/payment/initiate', $paymentData, $accessToken); + + if ($response && isset($response['payment_url'])) { + return response()->json([ + 'success' => true, + 'payment_url' => $response['payment_url'], + 'order_id' => $orderId + ]); + } + + return response()->json(['error' => __('Payment initiation failed')], 500); + + } catch (\Exception $e) { + \Log::error('Nepalste payment creation error: ' . $e->getMessage()); + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + try { + $orderId = $request->input('order_id'); + $planId = $request->input('plan_id'); + $billingCycle = $request->input('billing_cycle'); + + if ($orderId && $planId) { + $plan = Plan::find($planId); + $user = auth()->user(); + + if ($plan && $user) { + // Assign plan to user + $user->plan_id = $plan->id; + $user->plan_expire_date = $billingCycle === 'yearly' ? now()->addYear() : now()->addMonth(); + $user->save(); + + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $billingCycle, + 'payment_method' => 'nepalste', + 'payment_id' => $orderId, + ]); + + return redirect()->route('plans.index')->with('success', 'Payment successful and plan activated'); + } + } + + return redirect()->route('plans.index')->with('error', 'Payment verification failed'); + + } catch (\Exception $e) { + \Log::error('Nepalste success error: ' . $e->getMessage()); + return redirect()->route('plans.index')->with('error', 'Payment processing failed'); + } + } + + public function callback(Request $request) + { + try { + $orderId = $request->input('purchase_order_id'); + $status = $request->input('status'); + + if ($orderId && $status === 'completed') { + $parts = explode('_', $orderId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = \App\Models\User::find($userId); + + if ($plan && $user) { + $user->plan_id = $plan->id; + $user->plan_expire_date = now()->addMonth(); + $user->save(); + + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', + 'payment_method' => 'nepalste', + 'payment_id' => $request->input('payment_id'), + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + \Log::error('Nepalste callback error: ' . $e->getMessage()); + return response()->json(['error' => 'Callback processing failed'], 500); + } + } + + private function getAccessToken($settings) + { + try { + $baseUrl = $settings['nepalste_mode'] === 'live' + ? 'https://nepalste.com.np/pay/api/v1' + : 'https://nepalste.com.np/pay/sandbox/api/v1'; + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $baseUrl . '/access-token'); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'consumer_key' => $settings['nepalste_public_key'], + 'consumer_secret' => $settings['nepalste_secret_key'] + ])); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + \Log::info('Nepalste Access Token Response', [ + 'response' => $response, + 'http_code' => $httpCode + ]); + + if ($httpCode === 200) { + $decoded = json_decode($response, true); + return $decoded['token'] ?? null; + } + + return null; + + } catch (\Exception $e) { + \Log::error('Nepalste access token error: ' . $e->getMessage()); + return null; + } + } + + private function initiateNepalstePayment($url, $data, $token) + { + try { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'Authorization: Bearer ' . $token, + ]); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + \Log::info('Nepalste Payment Response', [ + 'response' => $response, + 'http_code' => $httpCode, + 'url' => $url, + 'data' => $data + ]); + + if ($httpCode === 200) { + $decoded = json_decode($response, true); + if ($decoded && isset($decoded['payment_url'])) { + return $decoded; + } + } + + return false; + + } catch (\Exception $e) { + \Log::error('Nepalste payment request error: ' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/NewsletterController.php b/app/Http/Controllers/NewsletterController.php new file mode 100644 index 000000000..8588a78b9 --- /dev/null +++ b/app/Http/Controllers/NewsletterController.php @@ -0,0 +1,62 @@ +can('manage-newsletters')) { + $query = NewsLetter::query(); + + // Search functionality + if ($request->filled('search')) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('email', 'like', "%{$search}%") + ->orWhere('status', 'like', "%{$search}%"); + }); + } + + // Sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['email', 'status', 'created_at']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + // Pagination + $perPage = $request->get('per_page', 10); + $newsletters = $query->paginate($perPage)->withQueryString(); + + return Inertia::render('newsletters/index', [ + 'newsletters' => $newsletters, + 'filters' => $request->only(['search', 'sort_field', 'sort_direction', 'per_page']) + ]); + + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy(NewsLetter $newsletter) + { + if (Auth::user()->can('delete-newsletters')) { + $newsletter->delete(); + + return redirect()->back()->with('success', 'Newsletter subscription deleted successfully.'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/NocTemplateController.php b/app/Http/Controllers/NocTemplateController.php new file mode 100644 index 000000000..045aae2f1 --- /dev/null +++ b/app/Http/Controllers/NocTemplateController.php @@ -0,0 +1,43 @@ +can('update-noc')) { + $request->validate([ + 'content' => 'required|string' + ]); + + if ($request->templateId) { + // Update existing template + $template = NocTemplate::where('id', $request->templateId) + ->where('created_by', auth::id()) + ->firstOrFail(); + $template->update(['content' => $request->content]); + } else { + // Create or update by language + $template = NocTemplate::updateOrCreate( + [ + 'language' => $request->language, + 'created_by' => auth::id() + ], + [ + 'content' => $request->content + ] + ); + } + + return redirect()->back()->with('success', __('NOC template updated successfully.')); + + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/OfferController.php b/app/Http/Controllers/OfferController.php new file mode 100644 index 000000000..cc5c1c2dc --- /dev/null +++ b/app/Http/Controllers/OfferController.php @@ -0,0 +1,299 @@ +can('manage-offers')) { + $query = Offer::with(['candidate', 'job', 'department', 'approver'])->where(function ($q) { + if (Auth::user()->can('manage-any-offers')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-offers')) { + $q->where('created_by', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && ! empty($request->search)) { + $query->whereHas('candidate', function ($q) use ($request) { + $q->where('first_name', 'like', '%'.$request->search.'%') + ->orWhere('last_name', 'like', '%'.$request->search.'%'); + }); + } + + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('candidate_id') && ! empty($request->candidate_id) && $request->candidate_id !== 'all') { + $query->where('candidate_id', $request->candidate_id); + } + + $query->orderBy('id', 'desc'); + $offers = $query->paginate($request->per_page ?? 10); + + $candidates = Candidate::with('job') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'Offer') + ->select('id', 'first_name', 'last_name', 'job_id') + ->get(); + + $departments = Department::with('branch') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name', 'branch_id') + ->get(); + + $employees = User::whereIn('created_by', getCompanyAndUsersId()) + ->whereIn('type', ['manager', 'hr']) + ->select('id', 'name') + ->get(); + + $jobPostings = JobPosting::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'Published') + ->select('id', 'title', 'job_code') + ->get(); + + // Add current user to employees list + $currentUser = auth()->user(); + if ($currentUser && ! $employees->contains('id', $currentUser->id)) { + $employees->push($currentUser); + } + + return Inertia::render('hr/recruitment/offers/index', [ + 'offers' => $offers, + 'candidates' => $candidates, + 'departments' => $departments, + 'employees' => $employees, + 'jobPostings' => $jobPostings, + 'currentUser' => auth()->user(), + 'filters' => $request->all(['search', 'status', 'candidate_id', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'candidate_id' => 'required|exists:candidates,id', + 'position' => 'required', + 'department_id' => 'nullable|exists:departments,id', + 'salary' => 'required|numeric|min:0', + 'benefits' => 'nullable|string', + 'start_date' => 'required|date|after_or_equal:today', + 'expiration_date' => 'required|date|after_or_equal:today', + 'approved_by' => 'nullable|exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $candidate = Candidate::find($request->candidate_id); + + // Check if offer already exists for this candidate and job + $existingOffer = Offer::where('candidate_id', $request->candidate_id) + ->where('job_id', $candidate->job_id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($existingOffer) { + return redirect()->back()->with('error', __('An offer already exists for this candidate and position.')); + } + + Offer::create([ + 'candidate_id' => $request->candidate_id, + 'job_id' => $candidate->job_id, + 'offer_date' => now(), + 'position' => $request->position, + 'department_id' => $request->department_id, + 'salary' => $request->salary, + 'benefits' => $request->benefits, + 'start_date' => $request->start_date, + 'expiration_date' => $request->expiration_date, + 'approved_by' => $request->approved_by, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Offer created successfully')); + } + + public function update(Request $request, Offer $offer) + { + if (! in_array($offer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this offer')); + } + + $validator = Validator::make($request->all(), [ + 'candidate_id' => 'required|exists:candidates,id', + 'position' => 'required|string|max:255', + 'department_id' => 'nullable|exists:departments,id', + 'salary' => 'required|numeric|min:0', + 'benefits' => 'nullable|string', + 'start_date' => 'required|date', + 'expiration_date' => 'required|date', + 'approved_by' => 'nullable|exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $candidate = Candidate::find($request->candidate_id); + + // Check if offer already exists for this candidate and job (excluding current offer) + $existingOffer = Offer::where('candidate_id', $request->candidate_id) + ->where('job_id', $candidate->job_id) + ->where('id', '!=', $offer->id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($existingOffer) { + return redirect()->back()->with('error', __('An offer already exists for this candidate and position.')); + } + + $offer->update([ + 'candidate_id' => $request->candidate_id, + 'job_id' => $candidate->job_id, + 'position' => $request->position, + 'department_id' => $request->department_id, + 'salary' => $request->salary, + 'benefits' => $request->benefits, + 'start_date' => $request->start_date, + 'expiration_date' => $request->expiration_date, + 'approved_by' => $request->approved_by, + ]); + + return redirect()->back()->with('success', __('Offer updated successfully')); + } + + public function show(Offer $offer) + { + if (! in_array($offer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view this offer')); + } + + $offer->load(['candidate', 'job', 'department', 'approver']); + + return Inertia::render('hr/recruitment/offers/show', [ + 'offer' => $offer, + ]); + } + + public function destroy(Offer $offer) + { + if (! in_array($offer->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this offer')); + } + + $offer->delete(); + + return redirect()->back()->with('success', __('Offer deleted successfully')); + } + + public function updateStatus(Request $request, Offer $offer) + { + if (Auth::user()->can('approve-offers')) { + + $validator = Validator::make($request->all(), [ + 'status' => 'required|in:Draft,Sent,Accepted,Negotiating,Declined,Expired', + 'decline_reason' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator); + } + + $updateData = ['status' => $request->status]; + + if ($request->status === 'Declined' && $request->decline_reason) { + $updateData['decline_reason'] = $request->decline_reason; + } + + if (in_array($request->status, ['Accepted', 'Declined'])) { + $updateData['response_date'] = now(); + } + + $offer->update($updateData); + + // Update candidate status based on offer status + if ($request->status === 'Accepted') { + $candidate = Candidate::find($offer->candidate_id); + if ($candidate) { + $candidate->update([ + 'status' => 'Hired', + 'final_salary' => $offer->salary, + ]); + } + } elseif ($request->status === 'Declined') { + $candidate = Candidate::find($offer->candidate_id); + if ($candidate) { + $candidate->update([ + 'status' => 'Rejected', + ]); + } + } + + return redirect()->back()->with('success', __('Offer status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function getCandidateJob($candidateId) + { + $candidate = Candidate::with('job')->find($candidateId); + + if (! $candidate || ! in_array($candidate->created_by, getCompanyAndUsersId())) { + return response()->json(['error' => 'Candidate not found'], 404); + } + + $response = []; + + // Job data + if ($candidate->job) { + return response()->json([ + [ + 'label' => $candidate->job->job_code.' - '.$candidate->job->title, + 'value' => $candidate->job->id, + ], + ]); + } + + return response()->json([]); + } + + public function getJobDepartments($jobId) + { + $job = JobPosting::with('department')->find($jobId); + if (! $job || ! in_array($job->created_by, getCompanyAndUsersId())) { + return response()->json(['error' => 'Job not found'], 404); + } + + if ($job->department_id && $job->department) { + return response()->json([ + [ + 'label' => $job->department->name, + 'value' => $job->department->id, + ], + ]); + } + + return response()->json([]); + } +} diff --git a/app/Http/Controllers/OfferTemplateController.php b/app/Http/Controllers/OfferTemplateController.php new file mode 100644 index 000000000..d9a247342 --- /dev/null +++ b/app/Http/Controllers/OfferTemplateController.php @@ -0,0 +1,246 @@ +can('manage-offer-templates')) { + $query = OfferTemplate::where(function ($q) { + if (Auth::user()->can('manage-any-offer-templates')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-offer-templates')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('template_content', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + $allowedSortFields = ['created_at']; + $sortField = $request->get('sort_field'); + $sortDirection = $request->get('sort_direction', 'desc'); + + if ($sortField && in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + + $offerTemplates = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/offer-templates/index', [ + 'offerTemplates' => $offerTemplates, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function create() + { + if (Auth::user()->can('create-offer-templates')) { + return Inertia::render('hr/recruitment/offer-templates/create'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function show(OfferTemplate $offerTemplate) + { + if (Auth::user()->can('view-offer-templates')) { + return Inertia::render('hr/recruitment/offer-templates/show', [ + 'offerTemplate' => $offerTemplate, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function edit(OfferTemplate $offerTemplate) + { + if (Auth::user()->can('edit-offer-templates')) { + return Inertia::render('hr/recruitment/offer-templates/edit', [ + 'offerTemplate' => $offerTemplate, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + if (Auth::user()->can('create-offer-templates')) { + $variables = null; + if ($request->filled('variables') && is_string($request->variables)) { + $variables = array_values(array_filter(array_map('trim', explode(',', $request->variables)))); + } elseif (is_array($request->variables)) { + $variables = $request->variables; + } + + $validator = Validator::make(array_merge($request->all(), ['variables' => $variables]), [ + 'name' => 'required|string|max:255', + 'template_content' => 'required|string', + 'variables' => 'required|array', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + OfferTemplate::create([ + 'name' => $request->name, + 'template_content' => $request->template_content, + 'variables' => $variables, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->route('hr.recruitment.offer-templates.index') + ->with('success', __('Offer template created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(Request $request, OfferTemplate $offerTemplate) + { + if (Auth::user()->can('edit-offer-templates')) { + if (!in_array($offerTemplate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this template')); + } + + $variables = null; + if ($request->filled('variables') && is_string($request->variables)) { + $variables = array_values(array_filter(array_map('trim', explode(',', $request->variables)))); + } elseif (is_array($request->variables)) { + $variables = $request->variables; + } + + $validator = Validator::make(array_merge($request->all(), ['variables' => $variables]), [ + 'name' => 'required|string|max:255', + 'template_content' => 'required|string', + 'variables' => 'required|array', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $offerTemplate->update([ + 'name' => $request->name, + 'template_content' => $request->template_content, + 'variables' => $variables, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->route('hr.recruitment.offer-templates.index') + ->with('success', __('Offer template updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy(OfferTemplate $offerTemplate) + { + if (Auth::user()->can('delete-offer-templates')) { + if (!in_array($offerTemplate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this template')); + } + try { + $offerTemplate->delete(); + return redirect()->back()->with('success', __('Offer template deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete offer template')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function toggleStatus(OfferTemplate $offerTemplate) + { + if (Auth::user()->can('edit-offer-templates')) { + if (!in_array($offerTemplate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this template')); + } + try { + $offerTemplate->update([ + 'status' => $offerTemplate->status === 'active' ? 'inactive' : 'active', + ]); + return redirect()->back()->with('success', __('Template status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update template status')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function preview(Request $request, OfferTemplate $offerTemplate) + { + if (!in_array($offerTemplate->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to preview this template')); + } + + $variables = $request->get('variables', []); + $generatedContent = $this->generateOfferContent($offerTemplate, $variables); + + return response()->json([ + 'content' => $generatedContent, + 'variables' => $offerTemplate->variables, + ]); + } + + public function generate(Request $request, OfferTemplate $offerTemplate) + { + + $variables = $request->variables ?? []; + + if (!is_array($variables)) { + $variables = []; + } + + $generatedContent = $this->generateOfferContent($offerTemplate, $variables); + $filename = $request->filename ?? ($offerTemplate->name . '_' . date('Y-m-d')); + + $html = '
' . nl2br($generatedContent) . '
'; + $pdf = Pdf::loadHTML($html); + return $pdf->download($filename . '.pdf'); + } + + private function generateOfferContent(OfferTemplate $offerTemplate, array $variables = []) + { + $content = $offerTemplate->template_content; + + if ($offerTemplate->variables && is_array($offerTemplate->variables)) { + foreach ($offerTemplate->variables as $variable) { + $value = $variables[$variable] ?? '{{' . $variable . '}}'; + $content = str_replace('{{' . $variable . '}}', $value, $content); + } + } + + return $content; + } +} diff --git a/app/Http/Controllers/OnboardingChecklistController.php b/app/Http/Controllers/OnboardingChecklistController.php new file mode 100644 index 000000000..4ebb4833f --- /dev/null +++ b/app/Http/Controllers/OnboardingChecklistController.php @@ -0,0 +1,154 @@ +can('manage-onboarding-checklists')) { + $query = OnboardingChecklist::withCount('checklistItems')->where(function ($q) { + if (Auth::user()->can('manage-any-onboarding-checklists')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-onboarding-checklists')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + if ($request->has('is_default') && $request->is_default !== 'all') { + $query->where('is_default', $request->is_default === 'true'); + } + + // Handle sorting + $sortField = $request->get('sort_field'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['created_at']; + if ($sortField && in_array($sortField, $allowedSortFields)) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('is_default', 'desc')->orderBy('id', 'desc'); + } + $onboardingChecklists = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/recruitment/onboarding-checklists/index', [ + 'onboardingChecklists' => $onboardingChecklists, + 'filters' => $request->all(['search', 'status', 'is_default', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'is_default' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // If setting as default, unset other defaults + if ($request->boolean('is_default')) { + OnboardingChecklist::whereIn('created_by', getCompanyAndUsersId()) + ->where('is_default', true) + ->update(['is_default' => false]); + } + + OnboardingChecklist::create([ + 'name' => $request->name, + 'description' => $request->description, + 'is_default' => $request->boolean('is_default'), + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Onboarding checklist created successfully')); + } + + public function update(Request $request, OnboardingChecklist $onboardingChecklist) + { + if (!in_array($onboardingChecklist->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this checklist')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'is_default' => 'boolean', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // If setting as default, unset other defaults + if ($request->boolean('is_default') && !$onboardingChecklist->is_default) { + OnboardingChecklist::whereIn('created_by', getCompanyAndUsersId()) + ->where('is_default', true) + ->update(['is_default' => false]); + } + + $onboardingChecklist->update([ + 'name' => $request->name, + 'description' => $request->description, + 'is_default' => $request->boolean('is_default'), + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Onboarding checklist updated successfully')); + } + + public function destroy(OnboardingChecklist $onboardingChecklist) + { + if (!in_array($onboardingChecklist->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this checklist')); + } + + if ($onboardingChecklist->candidateOnboardings()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete checklist as it is being used in onboarding processes')); + } + + $onboardingChecklist->delete(); + return redirect()->back()->with('success', __('Onboarding checklist deleted successfully')); + } + + public function toggleStatus(OnboardingChecklist $onboardingChecklist) + { + if (!in_array($onboardingChecklist->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this checklist')); + } + + $onboardingChecklist->update([ + 'status' => $onboardingChecklist->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Checklist status updated successfully')); + } +} diff --git a/app/Http/Controllers/OzowPaymentController.php b/app/Http/Controllers/OzowPaymentController.php new file mode 100644 index 000000000..76d3cd08e --- /dev/null +++ b/app/Http/Controllers/OzowPaymentController.php @@ -0,0 +1,165 @@ + 'required|string', + 'status' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['ozow_site_key'])) { + return back()->withErrors(['error' => __('Ozow not configured')]); + } + + if ($validated['status'] === 'Complete') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'ozow', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['transaction_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'ozow'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['ozow_site_key']) || !isset($settings['payment_settings']['ozow_private_key']) || !isset($settings['payment_settings']['ozow_api_key'])) { + return response()->json(['error' => __('Ozow not configured')], 400); + } + + $siteCode = $settings['payment_settings']['ozow_site_key']; + $privateKey = $settings['payment_settings']['ozow_private_key']; + $apiKey = $settings['payment_settings']['ozow_api_key']; + $isTest = $settings['payment_settings']['ozow_mode'] == 'sandbox' ? 'true' : 'false'; + $amount = $pricing['final_price']; + $cancelUrl = route('plans.index'); + $successUrl = route('ozow.success'); + $bankReference = time() . 'FKU'; + $transactionReference = time(); + $countryCode = 'ZA'; + $currency = 'ZAR'; + + $inputString = $siteCode . $countryCode . $currency . $amount . $transactionReference . $bankReference . $cancelUrl . $successUrl . $successUrl . $successUrl . $isTest . $privateKey; + $hashCheck = hash('sha512', strtolower($inputString)); + + $data = [ + 'countryCode' => $countryCode, + 'amount' => $amount, + 'transactionReference' => $transactionReference, + 'bankReference' => $bankReference, + 'cancelUrl' => $cancelUrl, + 'currencyCode' => $currency, + 'errorUrl' => $successUrl, + 'isTest' => $isTest, + 'notifyUrl' => $successUrl, + 'siteCode' => $siteCode, + 'successUrl' => $successUrl, + 'hashCheck' => $hashCheck, + ]; + + $curl = curl_init(); + curl_setopt_array($curl, [ + CURLOPT_URL => 'https://api.ozow.com/postpaymentrequest', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => '', + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 0, + CURLOPT_FOLLOWLOCATION => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => json_encode($data), + CURLOPT_HTTPHEADER => [ + 'Accept: application/json', + 'ApiKey: ' . $apiKey, + 'Content-Type: application/json' + ], + ]); + + $response = curl_exec($curl); + curl_close($curl); + $json_attendance = json_decode($response); + + if (isset($json_attendance->url) && $json_attendance->url != null) { + return response()->json([ + 'success' => true, + 'payment_url' => $json_attendance->url, + 'transaction_id' => $transactionReference + ]); + } else { + return response()->json(['error' => __('Payment creation failed')], 500); + } + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + return redirect()->route('plans.index')->with('success', __('Payment completed successfully')); + } + + public function callback(Request $request) + { + try { + $transactionId = $request->input('TransactionReference'); + $status = $request->input('Status'); + + if ($transactionId && $status === 'Complete') { + $parts = explode('_', $transactionId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', + 'payment_method' => 'ozow', + 'payment_id' => $transactionId, + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PaiementPaymentController.php b/app/Http/Controllers/PaiementPaymentController.php new file mode 100644 index 000000000..798ea0d08 --- /dev/null +++ b/app/Http/Controllers/PaiementPaymentController.php @@ -0,0 +1,124 @@ + 'required|string', + 'status' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['paiement_merchant_id'])) { + return back()->withErrors(['error' => __('Paiement Pro not configured')]); + } + + if ($validated['status'] === 'success') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'paiement', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['transaction_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'paiement'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['paiement_merchant_id'])) { + return response()->json(['error' => __('Paiement Pro not configured')], 400); + } + + $user = auth()->user(); + $transactionId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + + $paymentData = [ + 'merchant_id' => $settings['payment_settings']['paiement_merchant_id'], + 'amount' => $pricing['final_price'], + 'currency' => 'XOF', + 'reference' => $transactionId, + 'description' => $plan->name, + 'return_url' => route('paiement.success'), + 'cancel_url' => route('plans.index'), + 'notify_url' => route('paiement.callback'), + ]; + + return response()->json([ + 'success' => true, + 'payment_url' => 'https://www.paiementpro.net/webservice/onlinepayment/init/merchant-payment', + 'payment_data' => $paymentData, + 'transaction_id' => $transactionId + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + return redirect()->route('plans.index')->with('success', __('Payment completed successfully')); + } + + public function callback(Request $request) + { + try { + $transactionId = $request->input('reference'); + $status = $request->input('status'); + + if ($transactionId && $status === 'success') { + $parts = explode('_', $transactionId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', + 'payment_method' => 'paiement', + 'payment_id' => $request->input('transaction_id'), + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PayHerePaymentController.php b/app/Http/Controllers/PayHerePaymentController.php new file mode 100644 index 000000000..ebfd5fe09 --- /dev/null +++ b/app/Http/Controllers/PayHerePaymentController.php @@ -0,0 +1,147 @@ + 'required|string', + 'status_code' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['payhere_merchant_id'])) { + return back()->withErrors(['error' => __('PayHere not configured')]); + } + + if ($validated['status_code'] === '2') { // Success status + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'payhere', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['payment_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'payhere'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['payhere_merchant_id'])) { + return response()->json(['error' => __('PayHere not configured')], 400); + } + + $user = auth()->user(); + $orderId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + + $paymentData = [ + 'merchant_id' => $settings['payment_settings']['payhere_merchant_id'], + 'return_url' => route('payhere.success'), + 'cancel_url' => route('plans.index'), + 'notify_url' => route('payhere.callback'), + 'order_id' => $orderId, + 'items' => $plan->name, + 'currency' => 'LKR', + 'amount' => number_format($pricing['final_price'], 2, '.', ''), + 'first_name' => $user->name ?? 'Customer', + 'last_name' => 'User', + 'email' => $user->email, + 'phone' => '0771234567', + 'address' => 'No.1, Galle Road', + 'city' => 'Colombo', + 'country' => 'Sri Lanka', + ]; + + // Generate hash + $hashString = strtoupper( + md5( + $paymentData['merchant_id'] . + $paymentData['order_id'] . + number_format($paymentData['amount'], 2, '.', '') . + $paymentData['currency'] . + strtoupper(md5($settings['payment_settings']['payhere_merchant_secret'])) + ) + ); + $paymentData['hash'] = $hashString; + + $baseUrl = $settings['payment_settings']['payhere_mode'] === 'live' + ? 'https://www.payhere.lk' + : 'https://sandbox.payhere.lk'; + + return response()->json([ + 'success' => true, + 'payment_url' => $baseUrl . '/pay/checkout', + 'payment_data' => $paymentData, + 'order_id' => $orderId + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + return redirect()->route('plans.index')->with('success', __('Payment completed successfully')); + } + + public function callback(Request $request) + { + try { + $orderId = $request->input('order_id'); + $statusCode = $request->input('status_code'); + + if ($orderId && $statusCode === '2') { + $parts = explode('_', $orderId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', + 'payment_method' => 'payhere', + 'payment_id' => $request->input('payment_id'), + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PayPalPaymentController.php b/app/Http/Controllers/PayPalPaymentController.php new file mode 100644 index 000000000..61ebbd60f --- /dev/null +++ b/app/Http/Controllers/PayPalPaymentController.php @@ -0,0 +1,40 @@ + 'required|string', + 'payment_id' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'paypal', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['payment_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + + } catch (\Exception $e) { + return handlePaymentError($e, 'paypal'); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PayTRPaymentController.php b/app/Http/Controllers/PayTRPaymentController.php new file mode 100644 index 000000000..6671080ab --- /dev/null +++ b/app/Http/Controllers/PayTRPaymentController.php @@ -0,0 +1,162 @@ + $settings['payment_settings']['paytr_merchant_id'] ?? null, + 'merchant_key' => $settings['payment_settings']['paytr_merchant_key'] ?? null, + 'merchant_salt' => $settings['payment_settings']['paytr_merchant_salt'] ?? null, + 'currency' => $settings['general_settings']['defaultCurrency'] ?? 'TRY' + ]; + } + + public function createPaymentToken(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'user_name' => 'required|string', + 'user_email' => 'required|email', + 'user_phone' => 'required|string', + 'user_address' => 'nullable|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $credentials = $this->getPayTRCredentials(); + + if (!$credentials['merchant_id'] || !$credentials['merchant_key'] || !$credentials['merchant_salt']) { + throw new \Exception(__('PayTR credentials not configured')); + } + + $merchant_oid = 'plan_' . $plan->id . '_' . time() . '_' . uniqid(); + $payment_amount = intval($pricing['final_price'] * 100); // Convert to kuruş + $user_basket = json_encode([[ + $plan->name . ' - ' . ucfirst($validated['billing_cycle']), + number_format($pricing['final_price'], 2), + 1 + ]]); + + // Create pending order + createPlanOrder([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'paytr', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $merchant_oid, + 'status' => 'pending' + ]); + + // Generate hash according to PayTR documentation + $hashStr = $credentials['merchant_id'] . + $request->ip() . + $merchant_oid . + $validated['user_email'] . + $payment_amount . + $user_basket . + '1' . // no_installment + '0' . // max_installment + $credentials['currency'] . + '1' . // test_mode + $credentials['merchant_salt']; + + $paytr_token = base64_encode(hash_hmac('sha256', $hashStr, $credentials['merchant_key'], true)); + + $post_data = [ + 'merchant_id' => $credentials['merchant_id'], + 'user_ip' => $request->ip(), + 'merchant_oid' => $merchant_oid, + 'email' => $validated['user_email'], + 'payment_amount' => $payment_amount, + 'paytr_token' => $paytr_token, + 'user_basket' => $user_basket, + 'no_installment' => 1, + 'max_installment' => 0, + 'user_name' => $validated['user_name'], + 'user_address' => $validated['user_address'] ?? 'Turkey', + 'user_phone' => $validated['user_phone'], + 'merchant_ok_url' => route('paytr.success'), + 'merchant_fail_url' => route('paytr.failure'), + 'timeout_limit' => 30, + 'currency' => $credentials['currency'], + 'test_mode' => 1 + ]; + + $response = Http::asForm()->timeout(40)->post('https://www.paytr.com/odeme/api/get-token', $post_data); + + if ($response->successful()) { + $result = $response->json(); + if ($result['status'] == 'success') { + return response()->json([ + 'success' => true, + 'token' => $result['token'], + 'iframe_url' => 'https://www.paytr.com/odeme/guvenli/' . $result['token'] + ]); + } else { + throw new \Exception($result['reason'] ?? __('Token generation failed')); + } + } else { + throw new \Exception(__('PayTR API connection failed')); + } + } catch (\Exception $e) { + return response()->json(['error' => $e->getMessage()], 500); + } + } + + public function success(Request $request) + { + return redirect()->route('plans.index')->with('success', __('Payment completed successfully!')); + } + + public function failure(Request $request) + { + return redirect()->route('plans.index')->with('error', __('Payment failed. Please try again.')); + } + + public function callback(Request $request) + { + try { + $merchant_oid = $request->input('merchant_oid'); + $status = $request->input('status'); + $total_amount = $request->input('total_amount'); + $hash = $request->input('hash'); + + $credentials = $this->getPayTRCredentials(); + + // Verify hash for security + $hashStr = $merchant_oid . $credentials['merchant_salt'] . $status . $total_amount; + $calculatedHash = base64_encode(hash_hmac('sha256', $hashStr, $credentials['merchant_key'], true)); + + if ($hash === $calculatedHash && $status === 'success') { + $planOrder = \App\Models\PlanOrder::where('payment_id', $merchant_oid)->first(); + + if ($planOrder && $planOrder->status === 'pending') { + processPaymentSuccess([ + 'user_id' => $planOrder->user_id, + 'plan_id' => $planOrder->plan_id, + 'billing_cycle' => $planOrder->billing_cycle, + 'payment_method' => 'paytr', + 'coupon_code' => $planOrder->coupon_code, + 'payment_id' => $merchant_oid, + ]); + } + } + + return response('OK', 200); + } catch (\Exception $e) { + return response('ERROR', 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PayTabsPaymentController.php b/app/Http/Controllers/PayTabsPaymentController.php new file mode 100644 index 000000000..fd83fc3d7 --- /dev/null +++ b/app/Http/Controllers/PayTabsPaymentController.php @@ -0,0 +1,192 @@ + 'required|string', + 'transaction_id' => 'required|string', + ]); + + try { + $superAdmin = User::where('type', 'superadmin')->first(); + $settings = getPaymentMethodConfig('paytabs', $superAdmin->id); + + if (empty($settings['profile_id']) || empty($settings['server_key'])) { + return response()->json([ + 'success' => false, + 'message' => __('PayTabs configuration incomplete.') + ], 400); + } + + $plan = Plan::findOrFail($validated['plan_id']); + $user = auth()->user(); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $cartId = 'PT_' . time() . '_' . $user->id; + + createPlanOrder([ + 'user_id' => $user->id, + 'plan_id' => $validated['plan_id'], + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'paytabs', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $cartId, + 'status' => 'pending' + ]); + + // Force PayTabs configuration + config([ + 'paytabs.profile_id' => $settings['profile_id'], + 'paytabs.server_key' => $settings['server_key'], + 'paytabs.region' => $settings['region'], + 'paytabs.currency' => 'INR' + ]); + + $pay = paypage::sendPaymentCode('all') + ->sendTransaction('sale', 'ecom') + ->sendCart($cartId, $pricing['final_price'], "Plan Subscription - {$plan->name}") + ->sendCustomerDetails( + $user->name, + $user->email, + $user->phone ?? '1234567890', + 'Address', + 'City', + 'State', + 'SA', + '12345', + request()->ip() + ) + ->sendURLs( + route('paytabs.success') . '?cart_id=' . $cartId, + route('paytabs.callback') + ) + ->sendLanguage('en') + ->sendFramed(false) + ->create_pay_page(); + + if ($pay) { + // Extract redirect URL from PayTabs response + $redirectUrl = method_exists($pay, 'getTargetUrl') ? $pay->getTargetUrl() : (string)$pay; + + return response()->json([ + 'success' => true, + 'redirect_url' => $redirectUrl + ]); + } + + return response()->json([ + 'success' => false, + 'message' => __('Payment initialization failed.') + ], 400); + + } catch (\Exception $e) { + return response()->json([ + 'success' => false, + 'message' => __('Payment processing failed.') + ], 500); + } + } + + public function callback(Request $request) + { + try { + $cartId = $request->input('cartId') ?? $request->input('cart_id'); + $respStatus = $request->input('respStatus') ?? $request->input('resp_status'); + $tranRef = $request->input('tranRef') ?? $request->input('tran_ref'); + + if (!$cartId) { + return response(__('Missing cart ID'), 400); + } + + $planOrder = PlanOrder::where('payment_id', $cartId)->first(); + + if (!$planOrder) { + return response(__('Order not found'), 404); + } + + if ($respStatus === 'A') { + if ($planOrder->status === 'pending') { + $updateData = ['status' => 'approved']; + if ($tranRef) { + $updateData['payment_id'] = $tranRef; + } + + $planOrder->update($updateData); + + $user = User::find($planOrder->user_id); + $plan = Plan::find($planOrder->plan_id); + + if ($user && $plan) { + assignPlanToUser($user, $plan, $planOrder->billing_cycle); + } + } + } else { + $planOrder->update(['status' => 'failed']); + } + + return response('OK', 200); + + } catch (\Exception $e) { + return response(__('Callback processing failed'), 500); + } + } + + public function success(Request $request) + { + // Try different parameter names PayTabs might use + $cartId = $request->input('cart_id') + ?? $request->input('cartId') + ?? $request->input('merchant_reference') + ?? $request->input('reference') + ?? $request->input('order_id'); + if ($cartId) { + $planOrder = PlanOrder::where('payment_id', $cartId)->first(); + + if ($planOrder) { + // Verify payment status with PayTabs before assigning plan + if ($planOrder->status === 'pending') { + try { + $superAdmin = User::where('type', 'superadmin')->first(); + $settings = getPaymentMethodConfig('paytabs', $superAdmin->id); + + config([ + 'paytabs.profile_id' => $settings['profile_id'], + 'paytabs.server_key' => $settings['server_key'], + 'paytabs.region' => $settings['region'], + 'paytabs.currency' => 'INR' + ]); + + // PayTabs only redirects to success URL on successful payment + $planOrder->update(['status' => 'approved']); + + $user = User::find($planOrder->user_id); + $plan = Plan::find($planOrder->plan_id); + + if ($user && $plan) { + assignPlanToUser($user, $plan, $planOrder->billing_cycle); + } + + return redirect()->route('plans.index')->with('success', __('Payment completed successfully!')); + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment verification failed.')); + } + } + + return redirect()->route('plans.index')->with('success', __('Payment completed successfully!')); + } + } + + // No fallback - only assign plan with proper payment verification + return redirect()->route('plans.index')->with('error', __('Payment verification failed.')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PayfastPaymentController.php b/app/Http/Controllers/PayfastPaymentController.php new file mode 100644 index 000000000..712db1f38 --- /dev/null +++ b/app/Http/Controllers/PayfastPaymentController.php @@ -0,0 +1,225 @@ +validate([ + 'plan_id' => 'required|exists:plans,id', + 'billing_cycle' => 'required|in:monthly,yearly', + 'coupon_code' => 'nullable|string', + 'customer_details' => 'required|array', + 'customer_details.firstName' => 'required|string', + 'customer_details.lastName' => 'required|string', + 'customer_details.email' => 'required|email', + ]); + + try { + $settings = getPaymentMethodConfig('payfast'); + $isLive = ($settings['mode'] ?? 'sandbox') === 'live'; + + if (!$settings['merchant_id'] || !$settings['merchant_key']) { + return response()->json(['success' => false, 'error' => __('PayFast not configured')]); + } + + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + + if ($pricing['final_price'] < 5.00) { + return response()->json(['success' => false, 'error' => __('Minimum amount is R5.00')]); + } + + $paymentId = 'pf_' . $plan->id . '_' . time() . '_' . uniqid(); + + createPlanOrder([ + 'user_id' => auth()->id(), + 'plan_id' => $validated['plan_id'], + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'payfast', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $paymentId, + 'status' => 'pending' + ]); + + $data = [ + 'merchant_id' => $settings['merchant_id'], + 'merchant_key' => $settings['merchant_key'], + 'return_url' => route('payfast.success'), + 'cancel_url' => route('plans.index'), + 'notify_url' => route('payfast.callback'), + 'name_first' => $validated['customer_details']['firstName'], + 'name_last' => $validated['customer_details']['lastName'], + 'email_address' => $validated['customer_details']['email'], + 'm_payment_id' => $paymentId, + 'amount' => number_format($pricing['final_price'], 2, '.', ''), + 'item_name' => $plan->name, + ]; + + $passphrase = $settings['passphrase'] ?? ''; + $signature = $this->generateSignature($data, $passphrase); + $data['signature'] = $signature; + + $htmlForm = ''; + foreach ($data as $name => $value) { + $htmlForm .= ''; + } + + $endpoint = $isLive + ? 'https://www.payfast.co.za/eng/process' + : 'https://sandbox.payfast.co.za/eng/process'; + + return response()->json([ + 'success' => true, + 'inputs' => $htmlForm, + 'action' => $endpoint + ]); + + } catch (\Exception $e) { + return response()->json(['success' => false, 'error' => __('Payment failed')]); + } + } + + public function generateSignature($data, $passPhrase = null) + { + $pfOutput = ''; + foreach ($data as $key => $val) { + if ($val !== '') { + $pfOutput .= $key . '=' . urlencode(trim($val)) . '&'; + } + } + + $getString = substr($pfOutput, 0, -1); + if ($passPhrase !== null) { + $getString .= '&passphrase=' . urlencode(trim($passPhrase)); + } + return md5($getString); + } + + public function callback(Request $request) + { + try { + // Validate IP address (only for live mode) + $settings = getPaymentMethodConfig('payfast'); + + // Get callback data + $pfData = $request->all(); + $paymentId = $pfData['m_payment_id'] ?? null; + $paymentStatus = $pfData['payment_status'] ?? null; + + if (!$paymentId) { + return response(__('Missing payment ID'), 400); + } + + // Find the plan order + $planOrder = PlanOrder::where('payment_id', $paymentId)->first(); + + if (!$planOrder) { + return response(__('Order not found'), 404); + } + + // Verify signature + if (!$this->verifyPayfastSignature($pfData, $settings['passphrase'] ?? '')) { + return response(__('Invalid signature'), 400); + } + + // Verify amount + if (!$this->verifyAmount($pfData, $planOrder)) { + return response(__('Amount mismatch'), 400); + } + + // Process payment based on status + if ($paymentStatus === 'COMPLETE') { + if ($planOrder->status === 'pending') { + // Update order status + $planOrder->update([ + 'status' => 'approved', + 'processed_at' => now() + ]); + + // Assign plan to user + $user = $planOrder->user; + $plan = $planOrder->plan; + $expiresAt = $planOrder->billing_cycle === 'yearly' ? now()->addYear() : now()->addMonth(); + + $user->update([ + 'plan_id' => $plan->id, + 'plan_expires_at' => $expiresAt, + ]); + } + } else { + if (in_array($paymentStatus, ['CANCELLED', 'FAILED'])) { + $planOrder->update(['status' => 'rejected']); + } + } + + return response('OK', 200); + } catch (\Exception $e) { + return response('ERROR', 500); + } + } + + + private function verifyPayfastSignature($pfData, $passphrase = '') + { + $signature = $pfData['signature'] ?? ''; + unset($pfData['signature']); + + $expectedSignature = $this->generateSignature($pfData, $passphrase); + + return hash_equals($expectedSignature, $signature); + } + + private function verifyAmount($pfData, $planOrder) + { + $receivedAmount = floatval($pfData['amount_gross'] ?? 0); + $expectedAmount = floatval($planOrder->final_price); + + // Allow small floating point differences + return abs($receivedAmount - $expectedAmount) < 0.01; + } + + public function success(Request $request) + { + // Try different parameter names PayFast might use + $paymentId = $request->get('m_payment_id') ?? $request->get('pf_payment_id') ?? $request->get('payment_id'); + + if (!$paymentId && auth()->check()) { + // If no payment ID, find the most recent pending order for this user + $planOrder = PlanOrder::where('user_id', auth()->id()) + ->where('payment_method', 'payfast') + ->where('status', 'pending') + ->orderBy('created_at', 'desc') + ->first(); + } else { + $planOrder = PlanOrder::where('payment_id', $paymentId)->first(); + } + + if ($planOrder) { + // Always process the payment on success return + $planOrder->update([ + 'status' => 'approved', + 'processed_at' => now() + ]); + + // Assign plan to user + $user = $planOrder->user; + $plan = $planOrder->plan; + $expiresAt = $planOrder->billing_cycle === 'yearly' ? now()->addYear() : now()->addMonth(); + + $user->update([ + 'plan_id' => $plan->id, + 'plan_expires_at' => $expiresAt, + ]); + + return redirect()->route('plans.index')->with('success', __('Payment completed and plan activated!')); + } + + return redirect()->route('plans.index')->with('error', __('Payment verification failed')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PaymentWallPaymentController.php b/app/Http/Controllers/PaymentWallPaymentController.php new file mode 100644 index 000000000..fd3b6a74c --- /dev/null +++ b/app/Http/Controllers/PaymentWallPaymentController.php @@ -0,0 +1,243 @@ +validate([ + 'plan_id' => 'required|exists:plans,id', + 'billing_cycle' => 'required|in:monthly,yearly', + 'coupon_code' => 'nullable|string', + 'brick_token' => 'required|string', + 'brick_fingerprint' => 'required|string', + ]); + + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['paymentwall_private_key'])) { + return back()->withErrors(['error' => __('PaymentWall not configured')]); + } + + $user = auth()->user(); + $currency = $settings['general_settings']['currency'] ?? 'USD'; + $isTestMode = ($settings['payment_settings']['paymentwall_mode'] ?? 'sandbox') === 'sandbox'; + + // Prepare charge data for PaymentWall Brick API + $chargeData = [ + 'token' => $validated['brick_token'], + 'fingerprint' => $validated['brick_fingerprint'], + 'amount' => $pricing['final_price'], + 'currency' => $currency, + 'email' => $user->email, + 'history[registration_date]' => $user->created_at->timestamp, + 'description' => 'Plan: ' . $plan->name, + 'uid' => $user->id, + 'test_mode' => $isTestMode ? 1 : 0, + ]; + + // Make API call to PaymentWall to process the charge + $response = $this->processCharge($chargeData, $settings['payment_settings']['paymentwall_private_key']); + + if ($response && isset($response['type']) && $response['type'] === 'Charge' && $response['captured']) { + // Payment successful + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'paymentwall', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $response['id'] ?? 'brick_' . time(), + ]); + + return redirect()->route('plans.index')->with('success', __('Payment successful and plan activated')); + } else { + $errorMessage = $response['error'] ?? __('Payment processing failed'); + return back()->withErrors(['error' => $errorMessage]); + } + + } catch (\Exception $e) { + return handlePaymentError($e, 'paymentwall'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['paymentwall_public_key'])) { + return response()->json(['error' => __('PaymentWall not configured')], 400); + } + + $user = auth()->user(); + $currency = $settings['general_settings']['currency'] ?? 'USD'; + + $isTestMode = ($settings['payment_settings']['paymentwall_mode'] ?? 'sandbox') === 'sandbox'; + + // Return Brick.js configuration + return response()->json([ + 'success' => true, + 'brick_config' => [ + 'public_key' => $settings['payment_settings']['paymentwall_public_key'], + 'amount' => $pricing['final_price'], + 'currency' => $currency, + 'plan_name' => $plan->name, + 'success_url' => route('paymentwall.success'), + 'action_url' => route('paymentwall.process'), + 'plan_id' => $plan->id, + 'coupon_code' => $validated['coupon_code'] ?? null, + 'billing_cycle' => $validated['billing_cycle'], + 'test_mode' => $isTestMode + ] + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + return redirect()->route('plans.index')->with('success', __('Payment completed successfully')); + } + + public function callback(Request $request) + { + try { + $settings = getPaymentGatewaySettings(); + $privateKey = $settings['payment_settings']['paymentwall_private_key'] ?? ''; + + // Validate pingback signature + if (!$this->validatePingback($request->all(), $privateKey)) { + return response('Invalid signature', 400); + } + + $userId = $request->input('uid'); + $type = $request->input('type'); + $ref = $request->input('ref'); + $externalId = $request->input('goodsid'); + + // Type 0 = payment successful, Type 1 = payment pending, Type 2 = payment failed + if ($userId && $type === '0') { + $user = \App\Models\User::find($userId); + + if ($user && $externalId) { + // Extract plan ID from external_id (format: plan_X_timestamp) + if (preg_match('/^plan_(\d+)_/', $externalId, $matches)) { + $planId = $matches[1]; + $plan = Plan::find($planId); + + if ($plan) { + // Check if this payment was already processed + $existingOrder = \App\Models\PlanOrder::where('payment_id', $ref) + ->where('user_id', $user->id) + ->first(); + + if (!$existingOrder) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', // Default to monthly + 'payment_method' => 'paymentwall', + 'payment_id' => $ref, + ]); + } + } + } + } + } + + return response('OK'); + + } catch (\Exception $e) { + return response(__('Error processing callback'), 500); + } + } + + private function generateSignatureV2($params, $secretKey) + { + $str = ''; + ksort($params); + foreach ($params as $key => $value) { + if ($key !== 'sign') { + $str .= $key . '=' . $value; + } + } + $str .= $secretKey; + return md5($str); + } + + private function getSignatureString($params, $secretKey) + { + $str = ''; + ksort($params); + foreach ($params as $key => $value) { + if ($key !== 'sign') { + $str .= $key . '=' . $value; + } + } + $str .= $secretKey; + return $str; + } + + private function validatePingback($params, $secretKey) + { + $signature = $params['sig'] ?? ''; + unset($params['sig']); + + $str = ''; + ksort($params); + foreach ($params as $key => $value) { + $str .= $key . '=' . $value; + } + $str .= $secretKey; + + return md5($str) === $signature; + } + + private function processCharge($chargeData, $privateKey) + { + try { + $url = 'https://api.paymentwall.com/api/brick/charge'; + + // Add private key to the data + $chargeData['key'] = $privateKey; + + // Make HTTP request to PaymentWall Brick API + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($chargeData)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + return null; + } + + $responseData = json_decode($response, true); + + return $responseData; + + } catch (\Exception $e) { + return null; + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PayrollRunController.php b/app/Http/Controllers/PayrollRunController.php new file mode 100644 index 000000000..232db31dc --- /dev/null +++ b/app/Http/Controllers/PayrollRunController.php @@ -0,0 +1,426 @@ +can('manage-payroll-runs')) { + $query = PayrollRun::with(['creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-payroll-runs')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-payroll-runs')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('title', 'like', '%'.$request->search.'%') + ->orWhere('notes', 'like', '%'.$request->search.'%'); + }); + } + + // Handle status filter + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && ! empty($request->date_from)) { + $query->where('pay_period_start', '>=', $request->date_from); + } + if ($request->has('date_to') && ! empty($request->date_to)) { + $query->where('pay_period_end', '<=', $request->date_to); + } + + // Handle sorting + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'pay_date') { + $query->orderBy('pay_date', $sortDirection); + } else { + $query->orderBy('pay_period_start', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $payrollRuns = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/payroll-runs/index', [ + 'payrollRuns' => $payrollRuns, + 'hasSampleFile' => file_exists(storage_path('uploads/sample/sample-payroll-run.xlsx')), + 'filters' => $request->all(['search', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function show($payrollRunId) + { + if (Auth::user()->can('view-payroll-runs')) { + $payrollRun = PayrollRun::where('id', $payrollRunId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->with(['payrollEntries.employee']) + ->first(); + + if (! $payrollRun) { + return redirect()->back()->with('error', __('Payroll run not found.')); + } + + return Inertia::render('hr/payroll-runs/show', [ + 'payrollRun' => $payrollRun, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + if (Auth::user()->can('create-payroll-runs')) { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'payroll_frequency' => 'required|in:weekly,biweekly,monthly', + 'pay_period_start' => 'required|date', + 'pay_period_end' => 'required|date|after:pay_period_start', + 'pay_date' => 'required|date|after_or_equal:pay_period_end', + 'notes' => 'nullable|string', + ]); + + $validated['created_by'] = creatorId(); + $validated['status'] = 'draft'; + + // Check if payroll run already exists for this period + $exists = PayrollRun::where('pay_period_start', $validated['pay_period_start']) + ->where('pay_period_end', $validated['pay_period_end']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Payroll run already exists for this period.')); + } + + PayrollRun::create($validated); + + return redirect()->back()->with('success', __('Payroll run created successfully.')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(Request $request, $payrollRunId) + { + if (Auth::user()->can('edit-payroll-runs')) { + $payrollRun = PayrollRun::where('id', $payrollRunId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($payrollRun) { + try { + $validated = $request->validate([ + 'title' => 'required|string|max:255', + 'payroll_frequency' => 'required|in:weekly,biweekly,monthly', + 'pay_period_start' => 'required|date', + 'pay_period_end' => 'required|date|after:pay_period_start', + 'pay_date' => 'required|date|after_or_equal:pay_period_end', + 'notes' => 'nullable|string', + ]); + + // Only allow updates if status is draft + if ($payrollRun->status !== 'draft') { + return redirect()->back()->with('error', __('Cannot update processed payroll run.')); + } + + $payrollRun->update($validated); + + return redirect()->back()->with('success', __('Payroll run updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update payroll run')); + } + } else { + return redirect()->back()->with('error', __('Payroll run Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroy($payrollRunId) + { + if (Auth::user()->can('delete-payroll-runs')) { + $payrollRun = PayrollRun::where('id', $payrollRunId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($payrollRun) { + try { + // Only allow deletion if status is draft + if ($payrollRun->status !== 'draft') { + return redirect()->back()->with('error', __('Cannot delete processed payroll run.')); + } + + $payrollRun->delete(); + + return redirect()->back()->with('success', __('Payroll run deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete payroll run')); + } + } else { + return redirect()->back()->with('error', __('Payroll run Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function process($payrollRunId) + { + if (Auth::user()->can('process-payroll-runs')) { + $payrollRun = PayrollRun::where('id', $payrollRunId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($payrollRun) { + try { + if ($payrollRun->status !== 'draft') { + return redirect()->back()->with('error', __('Payroll run is not in draft status.')); + } + + $success = $payrollRun->processPayroll(); + + if ($success) { + return redirect()->back()->with('success', __('Payroll run processed successfully')); + } else { + return redirect()->back()->with('error', __('Failed to process payroll run')); + } + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to process payroll run')); + } + } else { + return redirect()->back()->with('error', __('Payroll run Not Found.')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function destroyEntry($payrollEntryId) + { + $payrollEntry = PayrollEntry::where('id', $payrollEntryId) + ->whereHas('payrollRun', function ($q) { + $q->whereIn('created_by', getCompanyAndUsersId()); + }) + ->with('payrollRun') + ->first(); + + if (! $payrollEntry) { + return redirect()->back()->with('error', __('Payroll entry not found.')); + } + + try { + $payrollRun = $payrollEntry->payrollRun; + + // Delete associated payslip if exists + Payslip::where('payroll_entry_id', $payrollEntry->id)->delete(); + + $payrollEntry->delete(); + + if ($payrollRun) { + $payrollRun->calculateTotals(); + $payrollRun->update(['status' => 'draft']); + } + + return redirect()->back()->with('success', __('Payroll entry deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to delete payroll entry')); + } + } + + public function export() + { + if (Auth::user()->can('export-payroll-runs')) { + try { + $payrollRuns = PayrollRun::whereIn('created_by', getCompanyAndUsersId())->get(); + + $fileName = 'payroll_runs_'.date('Y-m-d_His').'.csv'; + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="'.$fileName.'"', + ]; + + $callback = function () use ($payrollRuns) { + $file = fopen('php://output', 'w'); + fputcsv($file, ['Title', 'Payroll Frequency', 'Pay Period Start', 'Pay Period End', 'Pay Date', 'Status', 'Notes']); + + foreach ($payrollRuns as $run) { + fputcsv($file, [ + $run->title, + $run->payroll_frequency, + \Carbon\Carbon::parse($run->pay_period_start)->format('Y-m-d'), + \Carbon\Carbon::parse($run->pay_period_end)->format('Y-m-d'), + \Carbon\Carbon::parse($run->pay_date)->format('Y-m-d'), + $run->status, + $run->notes ?? '', + ]); + } + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to export payroll runs')], 500); + } + } else { + return response()->json(['message' => __('Permission Denied.')], 403); + } + } + + public function downloadTemplate() + { + $filePath = storage_path('uploads/sample/sample-payroll-run.xlsx'); + if (! file_exists($filePath)) { + return response()->json(['error' => __('Template file not available')], 404); + } + + return response()->download($filePath, 'sample-payroll-run.xlsx'); + } + + public function parseFile(Request $request) + { + if (Auth::user()->can('import-payroll-runs')) { + $rules = ['file' => 'required|mimes:csv,txt,xlsx,xls']; + $validator = \Illuminate\Support\Facades\Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return response()->json(['message' => $validator->getMessageBag()->first()]); + } + + try { + $file = $request->file('file'); + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file->getRealPath()); + $worksheet = $spreadsheet->getActiveSheet(); + $highestColumn = $worksheet->getHighestColumn(); + $highestRow = $worksheet->getHighestRow(); + $headers = []; + + for ($col = 'A'; $col <= $highestColumn; $col++) { + $value = $worksheet->getCell($col.'1')->getValue(); + if ($value) { + $headers[] = (string) $value; + } + } + + $previewData = []; + for ($row = 2; $row <= $highestRow; $row++) { + $rowData = []; + $colIndex = 0; + for ($col = 'A'; $col <= $highestColumn; $col++) { + if ($colIndex < count($headers)) { + $rowData[$headers[$colIndex]] = (string) $worksheet->getCell($col.$row)->getValue(); + } + $colIndex++; + } + $previewData[] = $rowData; + } + + return response()->json(['excelColumns' => $headers, 'previewData' => $previewData]); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to parse file')]); + } + } else { + return response()->json(['message' => __('Permission denied.')], 403); + } + } + + public function fileImport(Request $request) + { + if (Auth::user()->can('import-payroll-runs')) { + $rules = ['data' => 'required|array']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return redirect()->back()->with('error', $validator->getMessageBag()->first()); + } + + try { + $data = $request->data; + $imported = 0; + $skipped = 0; + + foreach ($data as $row) { + try { + if (empty($row['title']) || empty($row['pay_period_start']) || empty($row['pay_period_end']) || empty($row['pay_date'])) { + $skipped++; + continue; + } + + // Validate dates + if ($row['pay_period_end'] <= $row['pay_period_start']) { + $skipped++; + continue; + } + + if ($row['pay_date'] < $row['pay_period_end']) { + $skipped++; + continue; + } + + // Check if payroll run already exists for this period + $exists = PayrollRun::where('pay_period_start', $row['pay_period_start']) + ->where('pay_period_end', $row['pay_period_end']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + $skipped++; + continue; + } + + PayrollRun::create([ + 'title' => $row['title'], + 'payroll_frequency' => $row['payroll_frequency'] ?? 'monthly', + 'pay_period_start' => $row['pay_period_start'], + 'pay_period_end' => $row['pay_period_end'], + 'pay_date' => $row['pay_date'], + 'status' => $row['status'] ?? 'draft', + 'notes' => $row['notes'] ?? null, + 'created_by' => creatorId(), + ]); + + $imported++; + } catch (\Exception $e) { + $skipped++; + } + } + + return redirect()->back()->with('success', + __('Import completed: :added payroll runs added, :skipped payroll runs skipped', [ + 'added' => $imported, + 'skipped' => $skipped, + ]) + ); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to import')); + } + } else { + return redirect()->back()->with('error', __('Permission denied.')); + } + } +} diff --git a/app/Http/Controllers/PayslipController.php b/app/Http/Controllers/PayslipController.php new file mode 100644 index 000000000..f11bb4a0c --- /dev/null +++ b/app/Http/Controllers/PayslipController.php @@ -0,0 +1,259 @@ +can('manage-payslips')) { + $query = Payslip::with(['employee', 'payrollEntry.payrollRun', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-payslips')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-payslips')) { + $q->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('payslip_number', 'like', '%'.$request->search.'%') + ->orWhereHas('employee', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%'.$request->search.'%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && ! empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + // Handle status filter + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && ! empty($request->date_from)) { + $query->where('pay_period_start', '>=', $request->date_from); + } + if ($request->has('date_to') && ! empty($request->date_to)) { + $query->where('pay_period_end', '<=', $request->date_to); + } + + // Handle sorting + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if (in_array($sortField, ['pay_date', 'created_at'])) { + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('pay_date', 'desc'); + } + } else { + $query->orderBy('pay_date', 'desc'); + } + + $payslips = $query->paginate($request->per_page ?? 10); + + // Get employees for filter dropdown + $employees = User::where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name']); + + return Inertia::render('hr/payslips/index', [ + 'payslips' => $payslips, + 'employees' => $employees, + 'filters' => $request->all(['search', 'employee_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function generate(Request $request) + { + $validated = $request->validate([ + 'payroll_entry_ids' => 'required|array', + 'payroll_entry_ids.*' => 'exists:payroll_entries,id', + ]); + + $generatedCount = 0; + $errors = []; + + foreach ($validated['payroll_entry_ids'] as $entryId) { + try { + $payrollEntry = PayrollEntry::whereIn('created_by', getCompanyAndUsersId()) + ->find($entryId); + + if (! $payrollEntry) { + continue; + } + + // Check if payslip already exists + $exists = Payslip::where('payroll_entry_id', $entryId)->exists(); + if ($exists) { + continue; + } + + $payslipNumber = Payslip::generatePayslipNumber( + $payrollEntry->employee_id, + $payrollEntry->payrollRun->pay_date + ); + + $payslip = Payslip::create([ + 'payroll_entry_id' => $entryId, + 'employee_id' => $payrollEntry->employee_id, + 'payslip_number' => $payslipNumber, + 'pay_period_start' => $payrollEntry->payrollRun->pay_period_start, + 'pay_period_end' => $payrollEntry->payrollRun->pay_period_end, + 'pay_date' => $payrollEntry->payrollRun->pay_date, + 'status' => 'generated', + 'created_by' => creatorId(), + ]); + + // Generate PDF + $payslip->generatePDF(); + $generatedCount++; + } catch (\Exception $e) { + $errors[] = "Failed to generate payslip for entry ID {$entryId}: ".$e->getMessage(); + } + } + + if ($generatedCount > 0) { + $message = "Generated {$generatedCount} payslip(s) successfully."; + if (! empty($errors)) { + $message .= ' Some errors occurred: '.implode(', ', $errors); + } + + return redirect()->back()->with('success', __($message)); + } else { + return redirect()->back()->with('error', __('No payslips were generated. :errors', ['errors' => implode(', ', $errors)])); + } + } + + // public function download($payslipId) + // { + // $payslip = Payslip::where('id', $payslipId) + // ->whereIn('created_by', getCompanyAndUsersId()) + // ->first(); + + // if (!$payslip) { + // return redirect()->back()->with('error', __('Payslip not found.')); + // } + + // if (!$payslip->file_path || !Storage::disk('public')->exists($payslip->file_path)) { + // // Generate PDF if not exists + // try { + // $payslip->generatePDF(); + // } catch (\Exception $e) { + // return redirect()->back()->with('error', __('Failed to generate payslip PDF: :message', ['message' => $e->getMessage()])); + // } + // } + + // $payslip->markAsDownloaded(); + + // return Storage::disk('public')->download($payslip->file_path, 'payslip-' . $payslip->payslip_number . '.pdf'); + // } + + public function download($payslipId) + { + try { + $payslip = Payslip::with(['employee', 'payrollEntry.payrollRun', 'payrollEntry.employee.employee']) + ->where('id', $payslipId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if (!$payslip) { + return response()->json(['error' => __('Payslip not found.')], 404); + } + + $payrollEntry = $payslip->payrollEntry; + $companySettings = settings(); + $companyUser = User::find(getCompanyId(Auth::user()->id)); + + if ($companyUser) { + $companySettings = array_merge($companySettings, [ + 'companyEmail' => $companyUser->email ?? null, + ]); + } + + $data = [ + 'payslip' => $payslip, + 'payrollEntry' => $payrollEntry, + 'employee' => $payrollEntry->employee, + 'payrollRun' => $payrollEntry->payrollRun, + 'earnings' => $payrollEntry->earnings_breakdown ?? [], + 'deductions' => $payrollEntry->deductions_breakdown ?? [], + 'employeeData' => $payrollEntry->employee->employee, + 'companySettings' => $companySettings, + ]; + + $payslip->markAsDownloaded(); + + return view('payslips.template', $data); + } catch (\Exception $e) { + return response()->json(['error' => __('Failed to download payslip: :message', ['message' => $e->getMessage()])], 500); + } + } + + public function bulkGenerate(Request $request) + { + $validated = $request->validate([ + 'payroll_run_id' => 'required|exists:payroll_runs,id', + ]); + + try { + $payrollEntries = PayrollEntry::where('payroll_run_id', $validated['payroll_run_id']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(); + + $generatedCount = 0; + + foreach ($payrollEntries as $entry) { + // Check if payslip already exists + $exists = Payslip::where('payroll_entry_id', $entry->id)->exists(); + if ($exists) { + continue; + } + + $payslipNumber = Payslip::generatePayslipNumber( + $entry->employee_id, + $entry->payrollRun->pay_date + ); + + Payslip::create([ + 'payroll_entry_id' => $entry->id, + 'employee_id' => $entry->employee_id, + 'payslip_number' => $payslipNumber, + 'pay_period_start' => $entry->payrollRun->pay_period_start, + 'pay_period_end' => $entry->payrollRun->pay_period_end, + 'pay_date' => $entry->payrollRun->pay_date, + 'status' => 'generated', + 'created_by' => creatorId(), + ]); + + // Generate PDF + // $payslip->generatePDF(); + $generatedCount++; + } + + return redirect()->back()->with('success', __('Generated :count payslips successfully.', ['count' => $generatedCount])); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to generate payslips: :message', ['message' => $e->getMessage()])); + } + } +} diff --git a/app/Http/Controllers/PaystackPaymentController.php b/app/Http/Controllers/PaystackPaymentController.php new file mode 100644 index 000000000..77053c14b --- /dev/null +++ b/app/Http/Controllers/PaystackPaymentController.php @@ -0,0 +1,59 @@ + 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['paystack_secret_key'])) { + return back()->withErrors(['error' => __('Paystack not configured')]); + } + + // Verify payment with Paystack API + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.paystack.co/transaction/verify/" . $validated['payment_id'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + "Authorization: Bearer " . $settings['payment_settings']['paystack_secret_key'], + "Cache-Control: no-cache", + ], + )); + + $response = curl_exec($curl); + curl_close($curl); + + $result = json_decode($response, true); + + if ($result['status'] && $result['data']['status'] === 'success') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'paystack', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['payment_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment verification failed')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'paystack'); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PerformanceIndicatorCategoryController.php b/app/Http/Controllers/PerformanceIndicatorCategoryController.php new file mode 100644 index 000000000..5f10e1841 --- /dev/null +++ b/app/Http/Controllers/PerformanceIndicatorCategoryController.php @@ -0,0 +1,168 @@ +can('manage-performance-indicator-categories')) { + $query = PerformanceIndicatorCategory::where(function ($q) { + if (Auth::user()->can('manage-any-performance-indicator-categories')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-performance-indicator-categories')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $categories = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/performance/indicator-categories/index', [ + 'categories' => $categories, + 'filters' => $request->all(['search', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-performance-indicator-categories')) { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + PerformanceIndicatorCategory::create([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Performance indicator category created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, PerformanceIndicatorCategory $indicatorCategory) + { + if (Auth::user()->can('edit-performance-indicator-categories')) { + // Check if category belongs to current company + if (!in_array($indicatorCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this category')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $indicatorCategory->update([ + 'name' => $request->name, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Performance indicator category updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(PerformanceIndicatorCategory $indicatorCategory) + { + if (Auth::user()->can('delete-performance-indicator-categories')) { + // Check if category belongs to current company + if (!in_array($indicatorCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this category')); + } + + // Check if category is being used in indicators + if ($indicatorCategory->indicators()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete category as it is being used in indicators')); + } + + $indicatorCategory->delete(); + + return redirect()->back()->with('success', __('Performance indicator category deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Toggle the status of the specified resource. + */ + public function toggleStatus(PerformanceIndicatorCategory $indicatorCategory) + { + if (Auth::user()->can('edit-performance-indicator-categories')) { + // Check if category belongs to current company + if (!in_array($indicatorCategory->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this category')); + } + + $indicatorCategory->update([ + 'status' => $indicatorCategory->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Performance indicator category status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/PerformanceIndicatorController.php b/app/Http/Controllers/PerformanceIndicatorController.php new file mode 100644 index 000000000..e05b62a10 --- /dev/null +++ b/app/Http/Controllers/PerformanceIndicatorController.php @@ -0,0 +1,205 @@ +can('manage-performance-indicators')) { + $query = PerformanceIndicator::with('category')->where(function ($q) { + if (Auth::user()->can('manage-any-performance-indicators')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-performance-indicators')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhere('measurement_unit', 'like', '%' . $request->search . '%') + ->orWhere('target_value', 'like', '%' . $request->search . '%'); + }); + } + + // Handle category filter + if ($request->has('category_id') && !empty($request->category_id)) { + $query->where('category_id', $request->category_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $indicators = $query->paginate($request->per_page ?? 10); + + // Get categories for filter dropdown + $categories = PerformanceIndicatorCategory::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->orderBy('name') + ->get(['id', 'name']); + + return Inertia::render('hr/performance/indicators/index', [ + 'indicators' => $indicators, + 'categories' => $categories, + 'filters' => $request->all(['search', 'category_id', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-performance-indicators')) { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'category_id' => 'required|exists:performance_indicator_categories,id', + 'description' => 'nullable|string', + 'measurement_unit' => 'nullable|string|max:50', + 'target_value' => 'nullable|string|max:50', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Verify category belongs to current company + $category = PerformanceIndicatorCategory::find($request->category_id); + if (!$category || !in_array($category->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid category selected')->withInput(); + } + + PerformanceIndicator::create([ + 'name' => $request->name, + 'category_id' => $request->category_id, + 'description' => $request->description, + 'measurement_unit' => $request->measurement_unit, + 'target_value' => $request->target_value, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', 'Performance indicator created successfully'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, PerformanceIndicator $indicator) + { + if (Auth::user()->can('edit-performance-indicators')) { + // Check if indicator belongs to current company + if (!in_array($indicator->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this indicator')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'category_id' => 'required|exists:performance_indicator_categories,id', + 'description' => 'nullable|string', + 'measurement_unit' => 'nullable|string|max:50', + 'target_value' => 'nullable|string|max:50', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Verify category belongs to current company + $category = PerformanceIndicatorCategory::find($request->category_id); + if (!$category || !in_array($category->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid category selected')->withInput(); + } + + $indicator->update([ + 'name' => $request->name, + 'category_id' => $request->category_id, + 'description' => $request->description, + 'measurement_unit' => $request->measurement_unit, + 'target_value' => $request->target_value, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', 'Performance indicator updated successfully'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(PerformanceIndicator $indicator) + { + if (Auth::user()->can('delete-performance-indicators')) { + // Check if indicator belongs to current company + if (!in_array($indicator->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this indicator'); + } + + $indicator->delete(); + + return redirect()->back()->with('success', 'Performance indicator deleted successfully'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Toggle the status of the specified resource. + */ + public function toggleStatus(PerformanceIndicator $indicator) + { + if (Auth::user()->can('edit-performance-indicators')) { + // Check if indicator belongs to current company + if (!in_array($indicator->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this indicator')); + } + + $indicator->update([ + 'status' => $indicator->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', 'Performance indicator status updated successfully'); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/PermissionController.php b/app/Http/Controllers/PermissionController.php new file mode 100644 index 000000000..a561533b2 --- /dev/null +++ b/app/Http/Controllers/PermissionController.php @@ -0,0 +1,99 @@ +latest()->paginate(10); + return Inertia::render('permissions/index', [ + 'permissions' => $permissions, + ]); + } + + /** + * Show the form for creating a new resource. + */ + public function create() + { + // + } + + /** + * Store a newly created resource in storage. + */ + public function store(PermissionRequest $request) + { + $permission = Permission::create([ + 'module' => $request->module, + 'label' => $request->label, + 'name' => Str::slug($request->label), + 'description' => $request->description, + 'created_by' => Auth::id(), + ]); + + if ($permission) { + return redirect()->route('permissions.index')->with('success', __('Permission created successfully!')); + } + return redirect()->back()->with('error', __('Unable to create Permission. Please try again!')); + } + + /** + * Display the specified resource. + */ + public function show(string $id) + { + // + } + + /** + * Show the form for editing the specified resource. + */ + public function edit(string $id) + { + // + } + + /** + * Update the specified resource in storage. + */ + public function update(PermissionRequest $request, Permission $permission) + { + if ($permission) { + $permission->module = $request->module; + $permission->label = $request->label; + $permission->name = Str::slug($request->label); + $permission->description = $request->description; + + $permission->save(); + return redirect()->route('permissions.index')->with('success', __('Permission updated successfully!')); + } + return redirect()->back()->with('error', __('Unable to update Permission. Please try again!')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Permission $permission) + { + if ($permission) { + $permission->delete(); + return redirect()->route('permissions.index')->with('success', __('Permission deleted successfully!')); + } + + return redirect()->back()->with('error', __('Unable to delete Permission. Please try again!')); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/PlanController.php b/app/Http/Controllers/PlanController.php new file mode 100644 index 000000000..3eb70c9b5 --- /dev/null +++ b/app/Http/Controllers/PlanController.php @@ -0,0 +1,375 @@ +user(); + + // Company users see only active plans + if ($user->type !== 'superadmin') { + return $this->companyPlansView($request); + } + + // Admin view + $billingCycle = $request->input('billing_cycle', 'monthly'); + + $dbPlans = Plan::all(); + $hasDefaultPlan = $dbPlans->where('is_default', true)->count() > 0; + $settings = settings(); + + + // Always use super admin currency for plan pricing + $superAdmin = User::where('type', 'superadmin')->first(); + $superAdminSettings = settings($superAdmin->id); + $currency = $superAdminSettings ? ($superAdminSettings['defaultCurrency'] ?? 'USD') : 'USD'; + $currencySymbol = '$'; + if (!empty($currency)) { + $currencyData = Currency::where('code', $currency)->first(); + $currencySymbol = $currencyData ? $currencyData->symbol : '$'; + } + + $plans = $dbPlans->map(function ($plan) use ($billingCycle) { + // Determine features based on plan attributes + $features = []; + if ($plan->enable_chatgpt === 'on') $features[] = 'AI Integration'; + + // Get price based on billing cycle + $price = $billingCycle === 'yearly' ? $plan->yearly_price : $plan->price; + + // Format price with currency symbol + $formattedPrice = '$' . number_format($price, 2); + + + // Set duration based on billing cycle + $duration = $billingCycle === 'yearly' ? 'Yearly' : 'Monthly'; + + return [ + 'id' => $plan->id, + 'name' => $plan->name, + 'price' => $price, + 'formattedPrice' => $formattedPrice, + 'duration' => $duration, + 'description' => $plan->description, + 'trial_days' => $plan->trial_day, + 'features' => $features, + 'stats' => [ + 'users' => $plan->max_users, + 'employees' => $plan->max_employees, + 'storage' => $plan->storage_limit . ' GB' + ], + 'status' => $plan->is_plan_enable === 'on', + 'is_default' => $plan->is_default, + 'recommended' => false // Default to false + ]; + })->toArray(); + + // Mark the plan with most subscribers as recommended + $planSubscriberCounts = Plan::withCount('users')->get()->pluck('users_count', 'id'); + $mostSubscribedPlanId = $planSubscriberCounts->keys()->first(); + if ($planSubscriberCounts->isNotEmpty()) { + $mostSubscribedPlanId = $planSubscriberCounts->keys()->sortByDesc(function ($planId) use ($planSubscriberCounts) { + return $planSubscriberCounts[$planId]; + })->first(); + } + + foreach ($plans as &$plan) { + if ($plan['id'] == $mostSubscribedPlanId && $plan['price'] != '0') { + $plan['recommended'] = true; + break; + } + } + + return Inertia::render('plans/index', [ + 'plans' => $plans, + 'billingCycle' => $billingCycle, + 'hasDefaultPlan' => $hasDefaultPlan, + 'isAdmin' => true, + 'currency' => $currency, + 'currencySymbol' => $currencySymbol + ]); + } + + /** + * Toggle plan status + */ + public function toggleStatus(Plan $plan) + { + $plan->is_plan_enable = $plan->is_plan_enable === 'on' ? 'off' : 'on'; + $plan->save(); + + $status = $plan->is_plan_enable === 'on' ? 'activated' : 'deactivated'; + return back()->with('success', __('Plan :status successfully', ['status' => $status])); + } + + /** + * Show the form for creating a new plan + */ + public function create() + { + $hasDefaultPlan = Plan::where('is_default', true)->exists(); + + return Inertia::render('plans/create', [ + 'hasDefaultPlan' => $hasDefaultPlan + ]); + } + + /** + * Store a newly created plan + */ + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:100|unique:plans', + 'price' => 'required|numeric|min:0', + 'yearly_price' => 'nullable|numeric|min:0', + 'duration' => 'required|string', + 'description' => 'nullable|string', + 'max_users' => 'required|integer|min:0', + 'max_employees' => 'required|integer|min:0', + 'storage_limit' => 'required|numeric|min:0', + 'enable_chatgpt' => 'nullable|in:on,off', + 'is_trial' => 'nullable|in:on,off', + 'trial_day' => 'nullable|integer|min:0', + 'is_plan_enable' => 'nullable|in:on,off', + 'is_default' => 'nullable|boolean', + ]); + + // Set default values for nullable fields + $validated['enable_chatgpt'] = $validated['enable_chatgpt'] ?? 'off'; + $validated['is_trial'] = $validated['is_trial'] ?? null; + $validated['is_plan_enable'] = $validated['is_plan_enable'] ?? 'on'; + $validated['is_default'] = $validated['is_default'] ?? false; + + // If yearly_price is not provided, calculate it as 80% of monthly price * 12 + if (!isset($validated['yearly_price']) || $validated['yearly_price'] === null) { + $validated['yearly_price'] = $validated['price'] * 12 * 0.8; + } + + // If this plan is set as default, remove default status from other plans + if ($validated['is_default']) { + Plan::where('is_default', true)->update(['is_default' => false]); + } + + // Create the plan + Plan::create($validated); + + return redirect()->route('plans.index')->with('success', __('Plan created successfully.')); + } + + /** + * Show the form for editing a plan + */ + public function edit(Plan $plan) + { + $otherDefaultPlanExists = Plan::where('is_default', true) + ->where('id', '!=', $plan->id) + ->exists(); + + return Inertia::render('plans/edit', [ + 'plan' => $plan, + 'otherDefaultPlanExists' => $otherDefaultPlanExists + ]); + } + + /** + * Update a plan + */ + public function update(Request $request, Plan $plan) + { + $validated = $request->validate([ + 'name' => 'required|string|max:100|unique:plans,name,' . $plan->id, + 'price' => 'required|numeric|min:0', + 'yearly_price' => 'nullable|numeric|min:0', + 'duration' => 'required|string', + 'description' => 'nullable|string', + 'max_users' => 'required|integer|min:0', + 'max_employees' => 'required|integer|min:0', + 'storage_limit' => 'required|numeric|min:0', + 'enable_chatgpt' => 'nullable|in:on,off', + 'is_trial' => 'nullable|in:on,off', + 'trial_day' => 'nullable|integer|min:0', + 'is_plan_enable' => 'nullable|in:on,off', + 'is_default' => 'nullable|boolean', + ]); + + // Set default values for nullable fields + $validated['enable_chatgpt'] = $validated['enable_chatgpt'] ?? 'off'; + $validated['is_trial'] = $validated['is_trial'] ?? null; + $validated['is_plan_enable'] = $validated['is_plan_enable'] ?? 'on'; + $validated['is_default'] = $validated['is_default'] ?? false; + + // If yearly_price is not provided, calculate it as 80% of monthly price * 12 + if (!isset($validated['yearly_price']) || $validated['yearly_price'] === null) { + $validated['yearly_price'] = $validated['price'] * 12 * 0.8; + } + + // If this plan is set as default, remove default status from other plans + if ($validated['is_default'] && !$plan->is_default) { + Plan::where('is_default', true)->update(['is_default' => false]); + } + + // Update the plan + $plan->update($validated); + + return redirect()->route('plans.index')->with('success', __('Plan updated successfully.')); + } + + /** + * Delete a plan + */ + public function destroy(Plan $plan) + { + // Don't allow deleting the default plan + if ($plan->is_default) { + return back()->with('error', __('Cannot delete the default plan.')); + } + + $plan->delete(); + + return redirect()->route('plans.index')->with('success', __('Plan deleted successfully.')); + } + + private function companyPlansView(Request $request) + { + $user = auth()->user(); + $billingCycle = $request->input('billing_cycle', 'monthly'); + + $dbPlans = Plan::where('is_plan_enable', 'on')->get(); + + + // Always use super admin currency for plan pricing + $superAdmin = User::where('type', 'superadmin')->first(); + $superAdminSettings = settings($superAdmin->id); + $currency = $superAdminSettings ? ($superAdminSettings['defaultCurrency'] ?? 'USD') : 'USD'; + $currencySymbol = '$'; + if (!empty($currency)) { + $currencyData = Currency::where('code', $currency)->first(); + $currencySymbol = $currencyData ? $currencyData->symbol : '$'; + } + + $plans = $dbPlans->map(function ($plan) use ($billingCycle, $user) { + $price = $billingCycle === 'yearly' ? $plan->yearly_price : $plan->price; + $features = []; + if ($plan->enable_chatgpt === 'on') $features[] = 'AI Integration'; + + return [ + 'id' => $plan->id, + 'name' => $plan->name, + 'price' => $price, + 'formatted_price' => number_format($price, 2), + 'duration' => $billingCycle === 'yearly' ? 'Yearly' : 'Monthly', + 'description' => $plan->description, + 'trial_days' => $plan->trial_day, + 'features' => $features, + 'stats' => [ + 'users' => $plan->max_users, + 'employees' => $plan->max_employees, + 'storage' => $plan->storage_limit . ' GB' + ], + 'is_current' => $user->plan_id == $plan->id, + 'is_trial_available' => $plan->is_trial === 'on' && !$user->is_trial, + 'is_default' => $plan->is_default, + 'recommended' => false // Default to false + ]; + }); + + // Mark the plan with most subscribers as recommended + $planSubscriberCounts = Plan::withCount('users')->get()->pluck('users_count', 'id'); + if ($planSubscriberCounts->isNotEmpty()) { + $mostSubscribedPlanId = $planSubscriberCounts->keys()->sortByDesc(function ($planId) use ($planSubscriberCounts) { + return $planSubscriberCounts[$planId]; + })->first(); + + $plans = $plans->map(function ($plan) use ($mostSubscribedPlanId) { + if ($plan['id'] == $mostSubscribedPlanId) { + $plan['recommended'] = true; + } + return $plan; + }); + } + + return Inertia::render('plans/index', [ + 'plans' => $plans, + 'billingCycle' => $billingCycle, + 'currentPlan' => $user->plan, + 'userTrialUsed' => $user->is_trial, + 'currency' => $currency, + 'currencySymbol' => $currencySymbol + ]); + } + + + public function requestPlan(Request $request) + { + $request->validate([ + 'plan_id' => 'required|exists:plans,id', + 'billing_cycle' => 'required|in:monthly,yearly' + ]); + + $user = auth()->user(); + $plan = Plan::findOrFail($request->plan_id); + + \App\Models\PlanRequest::create([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'duration' => $request->billing_cycle, + 'status' => 'pending' + ]); + + return back()->with('success', __('Plan request submitted successfully')); + } + + public function startTrial(Request $request) + { + $request->validate([ + 'plan_id' => 'required|exists:plans,id' + ]); + + $user = auth()->user(); + $plan = Plan::findOrFail($request->plan_id); + + if ($user->is_trial || $plan->is_trial !== 'on') { + return back()->with('error', __('Trial not available')); + } + + $user->update([ + 'plan_id' => $plan->id, + 'is_trial' => 1, + 'trial_day' => $plan->trial_day, + 'trial_expire_date' => now()->addDays($plan->trial_day) + ]); + + return back()->with('success', __('Trial started successfully')); + } + + public function subscribe(Request $request) + { + $request->validate([ + 'plan_id' => 'required|exists:plans,id', + 'billing_cycle' => 'required|in:monthly,yearly' + ]); + + $user = auth()->user(); + $plan = Plan::findOrFail($request->plan_id); + $price = $request->billing_cycle === 'yearly' ? $plan->yearly_price : $plan->price; + + \App\Models\PlanOrder::create([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'original_price' => $price, + 'final_price' => $price, + 'status' => 'pending' + ]); + + return back()->with('success', __('Subscription request submitted successfully')); + } +} diff --git a/app/Http/Controllers/PlanOrderController.php b/app/Http/Controllers/PlanOrderController.php new file mode 100644 index 000000000..bbc136a37 --- /dev/null +++ b/app/Http/Controllers/PlanOrderController.php @@ -0,0 +1,133 @@ +hasRole('company')) { + $query->where('user_id', Auth::user()->id); + } + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->where('order_number', 'like', "%{$search}%") + ->orWhereHas('user', function ($userQuery) use ($search) { + $userQuery->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + }) + ->orWhereHas('plan', function ($planQuery) use ($search) { + $planQuery->where('name', 'like', "%{$search}%"); + }); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('ordered_at', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('ordered_at', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'ordered_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['id', 'ordered_at', 'status', 'final_price']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'ordered_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $perPage = $request->get('per_page', 10); + $planOrders = $query->paginate($perPage); + + // Transform data for frontend + $planOrders->getCollection()->transform(function ($planOrder) { + return [ + 'id' => $planOrder->id, + 'order_number' => $planOrder->order_number, + 'user' => [ + 'id' => $planOrder->user->id, + 'name' => $planOrder->user->name, + 'email' => $planOrder->user->email, + 'avatar' => check_file($planOrder->user->avatar) ? get_file($planOrder->user->avatar) : get_file('avatars/avatar.png'), + ], + 'plan' => [ + 'id' => $planOrder->plan->id, + 'name' => $planOrder->plan->name, + ], + 'original_price' => $planOrder->original_price, + 'discount_amount' => $planOrder->discount_amount, + 'final_price' => $planOrder->final_price, + 'status' => $planOrder->status, + 'ordered_at' => $planOrder->ordered_at, + 'processed_at' => $planOrder->processed_at, + ]; + }); + + // Always use super admin currency for plan pricing + $superAdmin = User::where('type', 'superadmin')->first(); + $superAdminSettings = settings($superAdmin->id); + $currency = $superAdminSettings ? ($superAdminSettings['defaultCurrency'] ?? 'USD') : 'USD'; + $currencySymbol = '$'; + if (!empty($currency)) { + $currencyData = Currency::where('code', $currency)->first(); + $currencySymbol = $currencyData ? $currencyData->symbol : '$'; + } + + return Inertia::render('plans/plan-orders', [ + 'planOrders' => $planOrders, + 'filters' => $request->all(['search', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + 'currency' => $currency, + 'currencySymbol' => $currencySymbol + ]); + } + + public function approve(PlanOrder $planOrder) + { + try { + $planOrder->approve(Auth::id()); + + return redirect()->back()->with('success', __('Plan order approved successfully!')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to approve plan order')); + } + } + + public function reject(Request $request, PlanOrder $planOrder) + { + try { + $request->validate([ + 'notes' => 'nullable|string|max:500' + ]); + + $planOrder->reject(Auth::id(), $request->notes); + + return redirect()->back()->with('success', __('Plan order rejected successfully!')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to reject plan order')); + } + } +} diff --git a/app/Http/Controllers/PlanRequestController.php b/app/Http/Controllers/PlanRequestController.php new file mode 100644 index 000000000..356ceee50 --- /dev/null +++ b/app/Http/Controllers/PlanRequestController.php @@ -0,0 +1,127 @@ +hasRole('company')) { + $query->where('user_id', Auth::user()->id); + } + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $search = $request->search; + $query->where(function ($q) use ($search) { + $q->whereHas('user', function ($userQuery) use ($search) { + $userQuery->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + }) + ->orWhereHas('plan', function ($planQuery) use ($search) { + $planQuery->where('name', 'like', "%{$search}%"); + }); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('created_at', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('created_at', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'id'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['id', 'created_at', 'status']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'id'; + } + + $query->orderBy($sortField, $sortDirection); + + $perPage = $request->get('per_page', 10); + $planRequests = $query->paginate($perPage); + + // Transform data for frontend + $planRequests->getCollection()->transform(function ($planRequest) { + return [ + 'id' => $planRequest->id, + 'user' => [ + 'id' => $planRequest->user->id, + 'name' => $planRequest->user->name, + 'email' => $planRequest->user->email, + 'avatar' => check_file($planRequest->user->avatar) ? get_file($planRequest->user->avatar) : get_file('avatars/avatar.png'), + ], + 'plan' => [ + 'id' => $planRequest->plan->id, + 'name' => $planRequest->plan->name, + 'duration' => $planRequest->plan->duration, + ], + 'status' => $planRequest->status, + 'created_at' => $planRequest->created_at, + 'approved_at' => $planRequest->approved_at, + 'rejected_at' => $planRequest->rejected_at, + ]; + }); + + return Inertia::render('plans/plan-request', [ + 'planRequests' => $planRequests, + 'filters' => $request->all(['search', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']) + ]); + } + + public function approve(PlanRequest $planRequest) + { + $planRequest->update([ + 'status' => 'approved', + 'approved_at' => now(), + 'approved_by' => Auth::id(), + ]); + + // Assign the plan to the user + $planRequest->user->update([ + 'plan_id' => $planRequest->plan_id + ]); + + // Create plan order for history + // \App\Models\PlanOrder::create([ + // 'user_id' => $planRequest->user_id, + // 'plan_id' => $planRequest->plan_id, + // 'original_price' => 0, + // 'final_price' => 0, + // 'status' => 'approved', + // 'ordered_at' => now() + // ]); + + return redirect()->route('plan-requests.index')->with('success', __('Plan request approved successfully!')); + } + + public function reject(PlanRequest $planRequest) + { + $planRequest->update([ + 'status' => 'rejected', + 'rejected_at' => now(), + 'rejected_by' => Auth::id(), + ]); + + return redirect()->route('plan-requests.index')->with('success', __('Plan request rejected successfully!')); + } +} diff --git a/app/Http/Controllers/PromotionController.php b/app/Http/Controllers/PromotionController.php new file mode 100644 index 000000000..045b1f387 --- /dev/null +++ b/app/Http/Controllers/PromotionController.php @@ -0,0 +1,341 @@ +can('manage-promotions')) { + $query = Promotion::with(['employee', 'designation'])->where(function ($q) { + if (Auth::user()->can('manage-any-promotions')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-promotions')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }) + ->orWhereHas('designation', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%'); + }) + ->orWhere('previous_designation', 'like', '%' . $request->search . '%') + ->orWhere('reason', 'like', '%' . $request->search . '%'); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle designation filter + if ($request->has('designation_id') && !empty($request->designation_id)) { + $query->where('designation_id', $request->designation_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('promotion_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('promotion_date', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'promotion_date'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['promotion_date', 'effective_date', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'promotion_date'; + } + + $query->orderBy($sortField, $sortDirection); + + $promotions = $query->paginate($request->per_page ?? 10); + + $promotions->getCollection()->transform(function ($promotion) { + if ($promotion->employee) { + $rawAvatar = $promotion->employee->getRawOriginal('avatar'); + $promotion->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $promotion; + }); + + // Get designations for filter dropdown + $designations = Designation::with(['department.branch']) + ->whereHas('department', function ($q) { + $q->whereHas('branch', function ($q) { + $q->whereIn('created_by', getCompanyAndUsersId()); + }); + }) + ->where('status', 'active') + ->select('id', 'name', 'department_id') + ->get(); + + return Inertia::render('hr/promotions/index', [ + 'promotions' => $promotions, + 'employees' => $this->getFilteredEmployees(), + 'designations' => $designations, + 'filters' => $request->all(['search', 'employee_id', 'designation_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-promotions') && !Auth::user()->can('manage-any-promotions')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + return $employees; + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-promotions')) { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'designation_id' => 'required|exists:designations,id', + 'previous_designation' => 'required|string|max:255', + 'promotion_date' => 'required|date', + 'effective_date' => 'required|date|after_or_equal:promotion_date', + 'salary_adjustment' => 'nullable|numeric|min:0', + 'reason' => 'nullable|string', + 'document' => 'nullable', + 'status' => 'nullable|string|in:pending,approved,rejected', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Check if designation belongs to current company + $designation = Designation::find($request->designation_id); + if (!$designation || !$designation->department || !$designation->department->branch || !in_array($designation->department->branch->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid designation selected')); + } + + $promotionData = [ + 'employee_id' => $request->employee_id, + 'previous_designation' => $request->previous_designation, + 'designation_id' => $request->designation_id, + 'promotion_date' => $request->promotion_date, + 'effective_date' => $request->effective_date, + 'salary_adjustment' => $request->salary_adjustment, + 'reason' => $request->reason, + 'status' => $request->status ?? 'pending', + 'created_by' => creatorId(), + ]; + + // Handle document upload + if ($request->has('document')) { + $documentPath = $request->document; + $promotionData['document'] = $documentPath; + } + + Promotion::create($promotionData); + + return redirect()->back()->with('success', __('Promotion created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Promotion $promotion) + { + if (Auth::user()->can('edit-promotions')) { + // Check if promotion belongs to current company + if (!in_array($promotion->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this promotion')); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'designation_id' => 'required|exists:designations,id', + 'previous_designation' => 'required|string|max:255', + 'promotion_date' => 'required|date', + 'effective_date' => 'required|date|after_or_equal:promotion_date', + 'salary_adjustment' => 'nullable|numeric|min:0', + 'reason' => 'nullable|string', + 'document' => 'nullable', + 'status' => 'nullable|string|in:pending,approved,rejected', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Check if designation belongs to current company + $designation = Designation::find($request->designation_id); + if (!$designation || !$designation->department || !$designation->department->branch || !in_array($designation->department->branch->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid designation selected')); + } + + $promotionData = [ + 'employee_id' => $request->employee_id, + 'previous_designation' => $request->previous_designation, + 'designation_id' => $request->designation_id, + 'promotion_date' => $request->promotion_date, + 'effective_date' => $request->effective_date, + 'salary_adjustment' => $request->salary_adjustment, + 'reason' => $request->reason, + 'status' => $request->status ?? 'pending', + ]; + + // Handle document upload + if ($request->has('document')) { + $documentPath = $request->document; + $promotionData['document'] = $documentPath; + } + + $promotion->update($promotionData); + + return redirect()->back()->with('success', __('Promotion updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Promotion $promotion) + { + if (Auth::user()->can('delete-promotions')) { + // Check if promotion belongs to current company + if (!in_array($promotion->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this promotion')); + } + + $promotion->delete(); + + return redirect()->back()->with('success', __('Promotion deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Download document file. + */ + public function downloadDocument(Promotion $promotion) + { + if (Auth::user()->can('view-promotions')) { + // Check if promotion belongs to current company + if (!in_array($promotion->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this document')); + } + + if (!$promotion->document) { + return redirect()->back()->with('error', __('Document file not found')); + } + + $filePath = getStorageFilePath($promotion->document); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Certificate file not found')); + } + + return response()->download($filePath); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the status of the promotion. + */ + public function updateStatus(Request $request, Promotion $promotion) + { + if (Auth::user()->can('approve-promotions') || Auth::user()->can('reject-promotions') || Auth::user()->can('edit-promotions')) { + // Check if promotion belongs to current company + if (!in_array($promotion->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this promotion')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:pending,approved,rejected', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $promotion->update([ + 'status' => $request->status, + ]); + + return redirect()->back()->with('success', __('Promotion status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/PublicFormController.php b/app/Http/Controllers/PublicFormController.php new file mode 100644 index 000000000..5d5db0337 --- /dev/null +++ b/app/Http/Controllers/PublicFormController.php @@ -0,0 +1,11 @@ + $settings['payment_settings']['razorpay_key'] ?? null, + 'secret' => $settings['payment_settings']['razorpay_secret'] ?? null, + 'currency' => $settings['general_settings']['defaultCurrency'] ?? 'INR' + ]; + } + + /** + * Create a Razorpay order + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function createOrder(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + + $amountInSmallestUnit = $pricing['final_price'] * 100; + + // Get Razorpay credentials + $credentials = $this->getRazorpayCredentials(); + + if (!$credentials['key'] || !$credentials['secret']) { + throw new \Exception(__('Razorpay API credentials not found')); + } + + $api = new Api($credentials['key'], $credentials['secret']); + + $orderData = [ + 'receipt' => 'plan_' . $plan->id . '_' . time(), + 'amount' => (int)$amountInSmallestUnit, + 'currency' => $credentials['currency'], + 'notes' => [ + 'plan_id' => $plan->id, + 'billing_cycle' => $request->billing_cycle, + ] + ]; + + $razorpayOrder = $api->order->create($orderData); + + return response()->json([ + 'order_id' => $razorpayOrder->id, + 'amount' => (int)$amountInSmallestUnit, + ]); + } catch (\Exception $e) { + return response()->json(['error' => __('Failed to create payment order: ') . $e->getMessage()], 500); + } + } + + /** + * Verify Razorpay payment + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\JsonResponse + */ + public function verifyPayment(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'razorpay_payment_id' => 'required|string', + 'razorpay_order_id' => 'required|string', + 'razorpay_signature' => 'required|string', + ]); + + try { + $credentials = $this->getRazorpayCredentials(); + + if (!$credentials['key'] || !$credentials['secret']) { + throw new \Exception(__('Razorpay API credentials not found')); + } + + $api = new Api($credentials['key'], $credentials['secret']); + $api->utility->verifyPaymentSignature([ + 'razorpay_order_id' => $validated['razorpay_order_id'], + 'razorpay_payment_id' => $validated['razorpay_payment_id'], + 'razorpay_signature' => $validated['razorpay_signature'] + ]); + + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $validated['plan_id'], + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'razorpay', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['razorpay_payment_id'], + ]); + + return response()->json(['success' => true]); + } catch (\Exception $e) { + return response()->json(['error' => __('Payment verification failed: ') . $e->getMessage()], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ReferralController.php b/app/Http/Controllers/ReferralController.php new file mode 100644 index 000000000..0cea5b9ea --- /dev/null +++ b/app/Http/Controllers/ReferralController.php @@ -0,0 +1,364 @@ +isSuperAdmin()) { + return $this->superAdminView($settings); + } else { + return $this->companyView($user, $settings); + } + } + + private function superAdminView($settings) + { + $totalReferralUsers = User::whereNotNull('used_referral_code') + ->where('used_referral_code', '!=', 0) + ->count(); + + $pendingPayouts = PayoutRequest::where('status', 'pending')->count(); + $totalCommissionPaid = PayoutRequest::where('status', 'approved')->sum('amount'); + + if (isDemo()) { + // For demo mode, get total counts without month filtering + $monthlyReferrals = User::whereNotNull('used_referral_code') + ->where('used_referral_code', '!=', 0) + ->count(); + + $monthlyPayouts = PayoutRequest::where('status', 'approved') + ->sum('amount') ?: 0; + } else { + // For normal mode, get monthly data and sum it + $monthlyReferralsData = User::whereNotNull('used_referral_code') + ->selectRaw('MONTH(created_at) as month, COUNT(*) as count') + ->whereYear('created_at', date('Y')) + ->groupBy('month') + ->pluck('count', 'month') + ->toArray(); + + $monthlyReferrals = array_sum($monthlyReferralsData); + + $monthlyPayoutsData = PayoutRequest::where('status', 'approved') + ->selectRaw('MONTH(created_at) as month, SUM(amount) as total') + ->whereYear('created_at', date('Y')) + ->groupBy('month') + ->pluck('total', 'month') + ->toArray(); + + $monthlyPayouts = array_sum($monthlyPayoutsData); + } + + $topCompanies = User::select('users.id', 'users.name', 'users.email', 'users.referral_code') + ->selectRaw('COUNT(referrals.id) as referral_count, SUM(referrals.amount) as total_earned') + ->leftJoin('referrals', 'users.id', '=', 'referrals.company_id') + ->where('users.type', 'company') + ->whereNotNull('users.referral_code') + ->groupBy('users.id', 'users.name', 'users.email', 'users.referral_code') + ->orderByDesc('referral_count') + ->limit(10) + ->get(); + + $payoutRequests = PayoutRequest::with('company') + ->orderBy('created_at', 'desc') + ->paginate(10); + + // Get all referred users for superadmin with pagination + $referredUsers = User::whereNotNull('used_referral_code') + ->with(['plan', 'referrals', 'planOrders' => function ($query) { + $query->where('status', 'approved')->orderBy('created_at', 'desc')->limit(1); + }]) + ->where('used_referral_code', '!=', 0) + ->orderBy('created_at', 'desc') + ->paginate(5) + ->withQueryString(); + + // Always use super admin currency for plan pricing + $superAdmin = User::where('type', 'superadmin')->first(); + $superAdminSettings = settings($superAdmin->id); + $currency = $superAdminSettings ? ($superAdminSettings['defaultCurrency'] ?? 'USD') : 'USD'; + $currencySymbol = '$'; + if (! empty($currency)) { + $currencyData = Currency::where('code', $currency)->first(); + $currencySymbol = $currencyData ? $currencyData->symbol : '$'; + } + + return Inertia::render('referral/index', [ + 'userType' => 'superadmin', + 'settings' => $settings, + 'stats' => [ + 'totalReferralUsers' => $totalReferralUsers, + 'pendingPayouts' => $pendingPayouts, + 'totalCommissionPaid' => $totalCommissionPaid, + 'monthlyReferrals' => $monthlyReferrals, + 'monthlyPayouts' => $monthlyPayouts, + 'topCompanies' => $topCompanies, + ], + 'payoutRequests' => $payoutRequests, + 'referredUsers' => $referredUsers, + 'currency' => $currency, + 'currencySymbol' => $currencySymbol, + ]); + } + + private function companyView($user, $settings) + { + $totalReferrals = Referral::where('company_id', $user->id)->count(); + + $totalEarned = Referral::where('company_id', $user->id)->sum('amount'); + $totalPayoutRequests = PayoutRequest::where('company_id', $user->id)->count(); + $pendingAmount = PayoutRequest::where('company_id', $user->id) + ->where('status', 'pending') + ->sum('amount'); + $availableBalance = $totalEarned - PayoutRequest::where('company_id', $user->id) + ->whereIn('status', ['pending', 'approved']) + ->sum('amount'); + + $payoutRequests = PayoutRequest::where('company_id', $user->id) + ->orderBy('created_at', 'desc') + ->paginate(10); + + // Get referred users count (users who used this company's referral code) + $referredUsersCount = User::where('used_referral_code', $user->referral_code)->count(); + + // Get recent referred users + $recentReferredUsers = User::where('used_referral_code', $user->referral_code) + ->with(['plan', 'planOrders' => function ($query) { + $query->where('status', 'approved')->orderBy('created_at', 'desc')->limit(1); + }]) + ->orderBy('created_at', 'desc') + ->limit(5) + ->get() + ->map(function ($referredUser) { + return [ + 'id' => $referredUser->id, + 'name' => $referredUser->name, + 'email' => $referredUser->email, + 'avatar' => check_file($referredUser->avatar) ? get_file($referredUser->avatar) : get_file('avatars/avatar.png'), + 'plan' => $referredUser->plan, + 'plan_orders' => $referredUser->planOrders, + ]; + }); + + // Get all referred users for the company with pagination + $referredUsers = User::where('used_referral_code', $user->referral_code) + ->with(['plan', 'referrals', 'planOrders' => function ($query) { + $query->where('status', 'approved')->orderBy('created_at', 'desc')->limit(1); + }]) + ->orderBy('created_at', 'desc') + ->paginate(5) + ->withQueryString(); + + // Generate referral code if not exists + if (! $user->referral_code) { + $user->referral_code = 'REF'.str_pad($user->id, 6, '0', STR_PAD_LEFT); + $user->save(); + } + + $referralLink = url('/register?ref='.$user->referral_code); + + // Always use super admin currency for plan pricing + $superAdmin = User::where('type', 'superadmin')->first(); + $superAdminSettings = settings($superAdmin->id); + $currency = $superAdminSettings ? ($superAdminSettings['defaultCurrency'] ?? 'USD') : 'USD'; + $currencySymbol = '$'; + if (! empty($currency)) { + $currencyData = Currency::where('code', $currency)->first(); + $currencySymbol = $currencyData ? $currencyData->symbol : '$'; + } + + return Inertia::render('referral/index', [ + 'userType' => 'company', + 'settings' => $settings, + 'stats' => [ + 'totalReferrals' => $totalReferrals, + 'totalEarned' => $totalEarned, + 'totalPayoutRequests' => $totalPayoutRequests, + 'availableBalance' => $availableBalance, + 'referredUsersCount' => $referredUsersCount, + ], + 'payoutRequests' => $payoutRequests, + 'referralLink' => $referralLink, + 'recentReferredUsers' => $recentReferredUsers, + 'referredUsers' => $referredUsers, + 'currency' => $currency, + 'currencySymbol' => $currencySymbol, + ]); + } + + public function updateSettings(Request $request) + { + $request->validate([ + 'commission_percentage' => 'required|numeric|min:0|max:100', + 'threshold_amount' => 'required|numeric|min:0', + 'guidelines' => 'nullable|string', + 'is_enabled' => 'boolean', + ]); + + $settings = ReferralSetting::current(); + $settings->update($request->all()); + + return back()->with('success', __('Referral settings updated successfully')); + } + + public function createPayoutRequest(Request $request) + { + $user = Auth::user(); + $settings = ReferralSetting::current(); + + $request->validate([ + 'amount' => 'required|numeric|min:1', + ]); + + $totalEarned = Referral::where('company_id', $user->id)->sum('amount'); + $totalRequested = PayoutRequest::where('company_id', $user->id) + ->whereIn('status', ['pending', 'approved']) + ->sum('amount'); + $availableBalance = $totalEarned - $totalRequested; + + if ($request->amount > $availableBalance) { + return back()->withErrors(['amount' => __('Insufficient balance')]); + } + + if ($request->amount < $settings->threshold_amount) { + return back()->withErrors(['amount' => __('Amount must be at least $ :amount', ['amount' => $settings->threshold_amount])]); + } + + PayoutRequest::create([ + 'company_id' => $user->id, + 'amount' => $request->amount, + 'status' => 'pending', + ]); + + return back()->with('success', __('Payout request submitted successfully')); + } + + public function approvePayoutRequest(PayoutRequest $payoutRequest) + { + $payoutRequest->update(['status' => 'approved']); + + return back()->with('success', __('Payout request approved')); + } + + public function rejectPayoutRequest(PayoutRequest $payoutRequest, Request $request) + { + $payoutRequest->update([ + 'status' => 'rejected', + 'notes' => $request->notes, + ]); + + return back()->with('success', __('Payout request rejected')); + } + + public function getReferredUsers(Request $request) + { + $user = Auth::user(); + // Always use super admin currency for plan pricing + $superAdmin = User::where('type', 'superadmin')->first(); + $superAdminSettings = settings($superAdmin->id); + $currency = $superAdminSettings ? ($superAdminSettings['defaultCurrency'] ?? 'USD') : 'USD'; + $currencySymbol = '$'; + if (! empty($currency)) { + $currencyData = Currency::where('code', $currency)->first(); + $currencySymbol = $currencyData ? $currencyData->symbol : '$'; + } + if ($user->isSuperAdmin()) { + // Super admin can see all referred users + $referredUsers = User::whereNotNull('used_referral_code') + ->with(['plan', 'referrals', 'planOrders' => function ($query) { + $query->where('status', 'approved')->orderBy('created_at', 'desc')->limit(1); + }]) + ->where('used_referral_code', '!=', 0) + ->orderBy('created_at', 'desc') + ->paginate(15) + ->withQueryString(); + + } else { + // Company can see users who used their referral code + $referredUsers = User::where('used_referral_code', $user->referral_code) + ->with(['plan', 'referrals', 'planOrders' => function ($query) { + $query->where('status', 'approved')->orderBy('created_at', 'desc')->limit(1); + }]) + ->orderBy('created_at', 'desc') + ->paginate(15) + ->withQueryString(); + } + + return Inertia::render('referral/referred-users', [ + 'referredUsers' => $referredUsers, + 'userType' => $user->isSuperAdmin() ? 'superadmin' : 'company', + 'currency' => $currency, + 'currencySymbol' => $currencySymbol, + ]); + } + + /** + * Create referral record when user purchases a plan + */ + public static function createReferralRecord(User $user, $billingCycle = null) + { + $settings = ReferralSetting::current(); + + if (! $settings->is_enabled || ! $user->used_referral_code || ! $user->plan) { + return; + } + + // Check if referral record already exists + $existingReferral = Referral::where('user_id', $user->id) + ->where('plan_id', $user->plan_id) + ->first(); + + if ($existingReferral) { + return; // Already created + } + + $referrer = User::where('referral_code', $user->used_referral_code) + ->where('type', 'company') + ->first(); + + if (! $referrer) { + return; + } + + // Get the actual paid amount from the most recent plan order + $planOrder = \App\Models\PlanOrder::where('user_id', $user->id) + ->where('plan_id', $user->plan_id) + ->where('status', 'approved') + ->orderBy('created_at', 'desc') + ->first(); + + // Use the actual paid amount if available, otherwise use plan price based on billing cycle + if ($planOrder && $planOrder->final_price > 0) { + $planPrice = $planOrder->final_price; + } elseif ($planOrder && $planOrder->billing_cycle === 'yearly' && $user->plan->yearly_price) { + $planPrice = $user->plan->yearly_price; + } else { + $planPrice = $user->plan->price ?? 0; + } + $commissionAmount = ($planPrice * $settings->commission_percentage) / 100; + + if ($commissionAmount > 0) { + Referral::create([ + 'user_id' => $user->id, + 'company_id' => $referrer->id, + 'commission_percentage' => $settings->commission_percentage, + 'amount' => $commissionAmount, + 'plan_id' => $user->plan_id, + ]); + } + } +} diff --git a/app/Http/Controllers/ResignationController.php b/app/Http/Controllers/ResignationController.php new file mode 100644 index 000000000..552764931 --- /dev/null +++ b/app/Http/Controllers/ResignationController.php @@ -0,0 +1,366 @@ +can('manage-resignations')) { + $query = Resignation::with(['employee:id,name,email,avatar', 'approver'])->where(function ($q) { + if (Auth::user()->can('manage-any-resignations')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-resignations')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }) + ->orWhere('reason', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('resignation_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('resignation_date', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['resignation_date', 'last_working_day', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $resignations = $query->paginate($request->per_page ?? 10); + + $resignations->getCollection()->transform(function ($resignation) { + if ($resignation->employee) { + $rawAvatar = $resignation->employee->getRawOriginal('avatar'); + $resignation->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $resignation; + }); + + return Inertia::render('hr/resignations/index', [ + 'resignations' => $resignations, + 'employees' => $this->getFilteredEmployees(), + 'filters' => $request->all(['search', 'employee_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-resignations') && !Auth::user()->can('manage-any-resignations')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + return $employees; + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-resignations')) { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'resignation_date' => 'required|date', + 'last_working_day' => 'required|date|after_or_equal:resignation_date', + 'notice_period' => 'nullable|string|max:255', + 'reason' => 'nullable|string|max:255', + 'description' => 'nullable|string', + 'documents' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $user = User::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (!$user) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + $resignationData = [ + 'employee_id' => $request->employee_id, + 'resignation_date' => $request->resignation_date, + 'last_working_day' => $request->last_working_day, + 'notice_period' => $request->notice_period, + 'reason' => $request->reason, + 'description' => $request->description, + 'status' => 'pending', + 'created_by' => creatorId(), + ]; + + // Handle document from media library + if ($request->has('documents')) { + $resignationData['documents'] = $request->documents; + } + + Resignation::create($resignationData); + + return redirect()->back()->with('success', __('Resignation created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Resignation $resignation) + { + if (Auth::user()->can('edit-resignations')) { + // Check if resignation belongs to current company + if (!in_array($resignation->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this resignation')); + } + + // Convert checkbox values to proper booleans before validation + if ($request->has('exit_interview_conducted')) { + $request->merge([ + 'exit_interview_conducted' => $request->exit_interview_conducted === 'true' || + $request->exit_interview_conducted === '1' || + $request->exit_interview_conducted === 1 || + $request->exit_interview_conducted === true + ]); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'resignation_date' => 'required|date', + 'last_working_day' => 'required|date|after_or_equal:resignation_date', + 'notice_period' => 'nullable|string|max:255', + 'reason' => 'nullable|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:pending,approved,rejected,completed', + 'documents' => 'nullable|string', + 'exit_feedback' => 'nullable|string', + 'exit_interview_conducted' => 'nullable|boolean', + 'exit_interview_date' => 'nullable|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $user = User::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (!$user) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + $resignationData = [ + 'employee_id' => $request->employee_id, + 'resignation_date' => $request->resignation_date, + 'last_working_day' => $request->last_working_day, + 'notice_period' => $request->notice_period, + 'reason' => $request->reason, + 'description' => $request->description, + 'exit_feedback' => $request->exit_feedback, + 'exit_interview_conducted' => $request->exit_interview_conducted ?? false, + 'exit_interview_date' => $request->exit_interview_date, + ]; + + // Update status if provided and different from current + if ($request->has('status') && $request->status !== $resignation->status) { + $resignationData['status'] = $request->status; + + // If status is being set to approved or completed, set approved_by and approved_at + if (in_array($request->status, ['approved', 'completed']) && !$resignation->approved_by) { + $resignationData['approved_by'] = auth()->id(); + $resignationData['approved_at'] = now(); + } + } + + // Handle document from media library + if ($request->has('documents')) { + $resignationData['documents'] = $request->documents; + } + + $resignation->update($resignationData); + + return redirect()->back()->with('success', __('Resignation updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Resignation $resignation) + { + if (Auth::user()->can('delete-resignations')) { + // Check if resignation belongs to current company + if (!in_array($resignation->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this resignation')); + } + + // Delete associated files + if ($resignation->documents) { + Storage::disk('public')->delete($resignation->documents); + } + + $resignation->delete(); + + return redirect()->back()->with('success', __('Resignation deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Change the status of the resignation. + */ + public function changeStatus(Request $request, Resignation $resignation) + { + if (Auth::user()->can('approve-resignations') || Auth::user()->can('reject-resignations') || Auth::user()->can('edit-resignations')) { + // Check if resignation belongs to current company + if (!in_array($resignation->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this resignation')); + } + + // Convert checkbox values to proper booleans before validation + if ($request->has('exit_interview_conducted')) { + $request->merge([ + 'exit_interview_conducted' => $request->exit_interview_conducted === 'true' || + $request->exit_interview_conducted === '1' || + $request->exit_interview_conducted === 1 || + $request->exit_interview_conducted === true + ]); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:pending,approved,rejected,completed', + 'exit_feedback' => 'nullable|string|required_if:status,completed', + 'exit_interview_conducted' => 'nullable|boolean', + 'exit_interview_date' => 'nullable|date', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $updateData = [ + 'status' => $request->status, + ]; + + // If status is being set to approved or completed, set approved_by and approved_at + if (in_array($request->status, ['approved', 'completed']) && !$resignation->approved_by) { + $updateData['approved_by'] = auth()->id(); + $updateData['approved_at'] = now(); + } + + // If status is completed, update exit interview details + if ($request->status === 'completed') { + $updateData['exit_feedback'] = $request->exit_feedback; + $updateData['exit_interview_conducted'] = $request->exit_interview_conducted ?? false; + $updateData['exit_interview_date'] = $request->exit_interview_date; + } + + $resignation->update($updateData); + + return redirect()->back()->with('success', __('Resignation status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Download document file. + */ + public function downloadDocument(Resignation $resignation) + { + if (Auth::user()->can('view-resignations')) { + // Check if resignation belongs to current company + if (!in_array($resignation->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this document')); + } + + if (!$resignation->documents) { + return redirect()->back()->with('error', __('Document file not found')); + } + + $filePath = getStorageFilePath($resignation->documents); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Certificate file not found')); + } + + return response()->download($filePath); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/ReviewCycleController.php b/app/Http/Controllers/ReviewCycleController.php new file mode 100644 index 000000000..9d28d09cb --- /dev/null +++ b/app/Http/Controllers/ReviewCycleController.php @@ -0,0 +1,181 @@ +can('manage-review-cycles')) { + $query = ReviewCycle::where(function ($q) { + if (Auth::user()->can('manage-any-review-cycles')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-review-cycles')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('frequency', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle frequency filter + if ($request->has('frequency') && !empty($request->frequency) && $request->frequency !== 'all') { + $query->where('frequency', $request->frequency); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['name', 'frequency', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $reviewCycles = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/performance/review-cycles/index', [ + 'reviewCycles' => $reviewCycles, + 'filters' => $request->all(['search', 'status', 'frequency', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-review-cycles')) { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:100', + 'frequency' => 'required|string|max:50', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + ReviewCycle::create([ + 'name' => $request->name, + 'frequency' => $request->frequency, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Review cycle created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, ReviewCycle $reviewCycle) + { + if (Auth::user()->can('edit-review-cycles')) { + // Check if review cycle belongs to current company + if (!in_array($reviewCycle->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this review cycle')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:100', + 'frequency' => 'required|string|max:50', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:active,inactive', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $reviewCycle->update([ + 'name' => $request->name, + 'frequency' => $request->frequency, + 'description' => $request->description, + 'status' => $request->status ?? 'active', + ]); + + return redirect()->back()->with('success', __('Review cycle updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(ReviewCycle $reviewCycle) + { + if (Auth::user()->can('delete-review-cycles')) { + // Check if review cycle belongs to current company + if (!in_array($reviewCycle->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this review cycle')); + } + + // Check if review cycle is being used in reviews + if ($reviewCycle->reviews()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete review cycle as it has associated reviews')); + } + + $reviewCycle->delete(); + + return redirect()->back()->with('success', __('Review cycle deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Toggle the status of the specified resource. + */ + public function toggleStatus(ReviewCycle $reviewCycle) + { + if (Auth::user()->can('edit-review-cycles')) { + // Check if review cycle belongs to current company + if (!in_array($reviewCycle->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this review cycle')); + } + + $reviewCycle->update([ + 'status' => $reviewCycle->status === 'active' ? 'inactive' : 'active', + ]); + + return redirect()->back()->with('success', __('Review cycle status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/RoleController.php b/app/Http/Controllers/RoleController.php new file mode 100644 index 000000000..6e08ba79c --- /dev/null +++ b/app/Http/Controllers/RoleController.php @@ -0,0 +1,238 @@ +can('manage-roles')) { + // $roles = Role::withPermissionCheck()->with(['permissions', 'creator'])->latest()->paginate(10); + $roles = Role::with(['permissions', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-roles')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-roles')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + })->latest()->paginate(10); + + // Add is_editable attribute to each role + $roles->getCollection()->transform(function ($role) { + $role->is_editable = !in_array($role->name, isNotEditableRoles()); + + return $role; + }); + + $permissions = $this->getFilteredPermissions(); + + return Inertia::render('roles/index', [ + 'roles' => $roles, + 'permissions' => $permissions, + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + + } + + private function getFilteredPermissions() + { + $user = Auth::user(); + $userType = $user->type ?? 'company'; + + // Superadmin can see all permissions + if ($userType === 'superadmin' || $userType === 'super admin') { + return Permission::all()->groupBy('module'); + } + + // Get allowed modules for current user role + $allowedModules = config('role-permissions.' . $userType, config('role-permissions.company')); + + // Filter permissions by allowed modules + $query = Permission::whereIn('module', $allowedModules); + + // For company users, filter specific settings permissions + if ($userType === 'company') { + // When in settings module, only show email, system and brand settings permissions + $query->where(function ($q) { + $q->where('module', '!=', 'settings') + ->orWhereIn('name', [ + 'manage-email-settings', + 'manage-system-settings', + 'manage-brand-settings', + ]); + }); + } + + $permissions = $query->get()->groupBy('module'); + + return $permissions; + } + + private function validatePermissions(array $permissions, $role = null) + { + $user = Auth::user(); + if (!$user) { + throw new \Exception('User not authenticated'); + } + + $userType = $user->type ?? 'company'; + + // Superadmin can assign any permission + if (in_array($userType, ['superadmin', 'super admin'])) { + return $permissions; + } + + // Get allowed modules for current user role + $allowedModules = config('role-permissions.' . $userType, config('role-permissions.company')); + if (!is_array($allowedModules)) { + $allowedModules = []; + } + + // Get existing permissions if updating a role + $existingPermissions = []; + if ($role) { + $existingPermissions = $role->permissions->pluck('name')->toArray(); + } + + // Build query to get valid permissions from allowed modules + $query = Permission::whereIn('module', $allowedModules) + ->whereIn('name', array_filter($permissions)); + + // For company users, restrict settings permissions + if ($userType === 'company') { + $query->where(function ($q) { + $q->where('module', '!=', 'settings') + ->orWhereIn('name', [ + 'manage-email-settings', + 'manage-system-settings', + 'manage-brand-settings', + ]); + }); + } + + $validPermissions = $query->pluck('name')->toArray(); + + // Remove permissions from disallowed modules automatically + if ($role) { + $permissionsFromDisallowedModules = Permission::whereNotIn('module', $allowedModules) + ->whereIn('name', $existingPermissions) + ->pluck('name') + ->toArray(); + + if (!empty($permissionsFromDisallowedModules)) { + $role->revokePermissionTo($permissionsFromDisallowedModules); + } + } + + return $validPermissions; + } + + + public function store(RoleRequest $request) + { + if (Auth::user()->can('create-roles')) { + // Validate permissions against user's allowed modules + $validatedPermissions = $this->validatePermissions($request->permissions ?? []); + + $checkRoleExist = Role::where('name', Str::slug($request->label))->whereIn('created_by', getCompanyAndUsersId())->exists(); + if (!$checkRoleExist) { + // Use direct model creation to bypass Spatie's duplicate check + $role = new Role; + $role->label = $request->label; + $role->name = Str::slug($request->label); + $role->description = $request->description; + $role->created_by = Auth::id(); + $role->guard_name = 'web'; + $role->save(); + + if ($role) { + $role->syncPermissions($validatedPermissions); + + return redirect()->route('roles.index')->with('success', __('Role created successfully with Permissions!')); + } + + return redirect()->back()->with('error', __('Unable to create Role with permissions. Please try again!')); + } else { + return redirect()->back()->with('error', __('Role already exists!')); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function update(RoleRequest $request, Role $role) + { + if (Auth::user()->can('edit-roles')) { + if ($role) { + // Validate permissions (will keep existing ones from commented modules) + $validatedPermissions = $this->validatePermissions($request->permissions ?? [], $role); + + $newSlug = Str::slug($request->label); + + // Check if role name already exists (excluding current role) + $checkRoleExist = Role::where('name', $newSlug) + ->where('id', '!=', $role->id) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($checkRoleExist) { + return redirect()->back()->with('error', __('Role already exists!')); + } + + // Only update name if it's different to avoid duplicate key error + if ($role->name !== $newSlug) { + $role->name = $newSlug; + } + + $role->label = $request->label; + $role->description = $request->description; + + $role->save(); + + // Update the permissions + $role->syncPermissions($validatedPermissions); + + return redirect()->route('roles.index')->with('success', __('Role updated successfully with Permissions!')); + } + + return redirect()->back()->with('error', __('Unable to update Role with permissions. Please try again!')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + public function destroy(Role $role) + { + if (Auth::user()->can('delete-roles')) { + if ($role) { + // Prevent deletion of system roles + // if ($role->is_system_role) { + // return redirect()->back()->with('error', __('System roles cannot be deleted!')); + // } + + if (in_array($role->name, isNotDeletableRoles())) { + return redirect()->back()->with('error', __('System roles cannot be deleted!')); + } + + $role->delete(); + + return redirect()->route('roles.index')->with('success', __('Role deleted successfully!')); + } + + return redirect()->back()->with('error', __('Unable to delete Role. Please try again!')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/SSPayPaymentController.php b/app/Http/Controllers/SSPayPaymentController.php new file mode 100644 index 000000000..4565f4540 --- /dev/null +++ b/app/Http/Controllers/SSPayPaymentController.php @@ -0,0 +1,134 @@ + 'required|string', + 'order_id' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['sspay_secret_key'])) { + return back()->withErrors(['error' => __('SSPay not configured')]); + } + + if ($validated['status_id'] === '1') { // Success status + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'sspay', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['order_id'], + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed or cancelled')]); + + } catch (\Exception $e) { + return handlePaymentError($e, 'sspay'); + } + } + + public function createPayment(Request $request) + { + $validated = validatePaymentRequest($request); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['sspay_secret_key'])) { + return response()->json(['error' => __('SSPay not configured')], 400); + } + + $user = auth()->user(); + $orderId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + + $paymentData = [ + 'userSecretKey' => $settings['payment_settings']['sspay_secret_key'], + 'categoryCode' => $settings['payment_settings']['sspay_category_code'], + 'billName' => $plan->name, + 'billDescription' => 'Plan: ' . $plan->name, + 'billPriceSetting' => 1, + 'billPayorInfo' => 1, + 'billAmount' => $pricing['final_price'] * 100, // Convert to cents + 'billReturnUrl' => route('sspay.success'), + 'billCallbackUrl' => route('sspay.callback'), + 'billExternalReferenceNo' => $orderId, + 'billTo' => $user->email, + 'billEmail' => $user->email, + 'billPhone' => '60123456789', + 'billAddrLine1' => 'Address Line 1', + 'billAddrLine2' => 'Address Line 2', + 'billPostcode' => '12345', + 'billCity' => 'Kuala Lumpur', + 'billState' => 'Selangor', + 'billCountry' => 'MY', + ]; + + return response()->json([ + 'success' => true, + 'payment_url' => 'https://sspay.my/index.php/api/createBill', + 'payment_data' => $paymentData, + 'order_id' => $orderId + ]); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + return redirect()->route('plans.index')->with('success', __('Payment completed successfully')); + } + + public function callback(Request $request) + { + try { + $orderId = $request->input('billExternalReferenceNo'); + $statusId = $request->input('status_id'); + + if ($orderId && $statusId === '1') { + $parts = explode('_', $orderId); + + if (count($parts) >= 3) { + $planId = $parts[1]; + $userId = $parts[2]; + + $plan = Plan::find($planId); + $user = \App\Models\User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => 'monthly', + 'payment_method' => 'sspay', + 'payment_id' => $request->input('billcode'), + ]); + } + } + } + + return response()->json(['status' => 'success']); + + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/SalaryComponentController.php b/app/Http/Controllers/SalaryComponentController.php new file mode 100644 index 000000000..333c779e4 --- /dev/null +++ b/app/Http/Controllers/SalaryComponentController.php @@ -0,0 +1,198 @@ +can('manage-salary-components')) { + $query = SalaryComponent::with(['creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-salary-components')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-salary-components')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle type filter + if ($request->has('type') && !empty($request->type) && $request->type !== 'all') { + $query->where('type', $request->type); + } + + // Handle calculation type filter + if ($request->has('calculation_type') && !empty($request->calculation_type) && $request->calculation_type !== 'all') { + $query->where('calculation_type', $request->calculation_type); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'name') { + $query->orderBy('name', $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $salaryComponents = $query->paginate($request->per_page ?? 10); + + return Inertia::render('hr/salary-components/index', [ + 'salaryComponents' => $salaryComponents, + 'filters' => $request->all(['search', 'type', 'calculation_type', 'status', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'type' => 'required|in:earning,deduction', + 'calculation_type' => 'required|in:fixed,percentage', + 'default_amount' => 'required_if:calculation_type,fixed|nullable|numeric|min:0', + 'percentage_of_basic' => 'required_if:calculation_type,percentage|nullable|numeric|min:0|max:100', + 'is_taxable' => 'boolean', + 'is_mandatory' => 'boolean', + 'status' => 'nullable|in:active,inactive', + ]); + + $validated['created_by'] = creatorId(); + $validated['status'] = $validated['status'] ?? 'active'; + $validated['is_taxable'] = $validated['is_taxable'] ?? true; + $validated['is_mandatory'] = $validated['is_mandatory'] ?? false; + + // Set default values based on calculation type + if ($validated['calculation_type'] === 'fixed') { + $validated['percentage_of_basic'] = null; + } else { + $validated['default_amount'] = 0; + } + + // Check if component with same name already exists + $exists = SalaryComponent::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Salary component with this name already exists.')); + } + + SalaryComponent::create($validated); + + return redirect()->back()->with('success', __('Salary component created successfully.')); + } + + public function update(Request $request, $salaryComponentId) + { + $salaryComponent = SalaryComponent::where('id', $salaryComponentId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($salaryComponent) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'type' => 'required|in:earning,deduction', + 'calculation_type' => 'required|in:fixed,percentage', + 'default_amount' => 'required_if:calculation_type,fixed|nullable|numeric|min:0', + 'percentage_of_basic' => 'required_if:calculation_type,percentage|nullable|numeric|min:0|max:100', + 'is_taxable' => 'boolean', + 'is_mandatory' => 'boolean', + 'status' => 'nullable|in:active,inactive', + ]); + + // Set default values based on calculation type + if ($validated['calculation_type'] === 'fixed') { + $validated['percentage_of_basic'] = null; + } else { + $validated['default_amount'] = 0; + } + + // Check if component with same name already exists (excluding current) + $exists = SalaryComponent::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('id', '!=', $salaryComponentId) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Salary component with this name already exists.')); + } + + $salaryComponent->update($validated); + + return redirect()->back()->with('success', __('Salary component updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update salary component')); + } + } else { + return redirect()->back()->with('error', __('Salary component Not Found.')); + } + } + + public function destroy($salaryComponentId) + { + $salaryComponent = SalaryComponent::where('id', $salaryComponentId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($salaryComponent) { + try { + $salaryComponent->delete(); + return redirect()->back()->with('success', __('Salary component deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete salary component')); + } + } else { + return redirect()->back()->with('error', __('Salary component Not Found.')); + } + } + + public function toggleStatus($salaryComponentId) + { + $salaryComponent = SalaryComponent::where('id', $salaryComponentId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($salaryComponent) { + try { + $salaryComponent->status = $salaryComponent->status === 'active' ? 'inactive' : 'active'; + $salaryComponent->save(); + + return redirect()->back()->with('success', __('Salary component status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update salary component status')); + } + } else { + return redirect()->back()->with('error', __('Salary component Not Found.')); + } + } +} diff --git a/app/Http/Controllers/Settings/CurrencySettingController.php b/app/Http/Controllers/Settings/CurrencySettingController.php new file mode 100644 index 000000000..adcf1d6f1 --- /dev/null +++ b/app/Http/Controllers/Settings/CurrencySettingController.php @@ -0,0 +1,39 @@ +validate([ + 'decimalFormat' => 'required|string|in:0,1,2,3,4', + 'defaultCurrency' => 'required|string|exists:currencies,code', + 'decimalSeparator' => ['required', 'string', Rule::in(['.', ','])], + 'thousandsSeparator' => 'required|string', + 'floatNumber' => 'required|boolean', + 'currencySymbolSpace' => 'required|boolean', + 'currencySymbolPosition' => 'required|string|in:before,after', + ]); + + // Update settings using helper function + foreach ($validated as $key => $value) { + updateSetting($key, $value); + } + + return redirect()->back()->with('success', __('Currency settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update currency settings: :error', ['error' => $e->getMessage()])); + } + } +} diff --git a/app/Http/Controllers/Settings/EmailSettingController.php b/app/Http/Controllers/Settings/EmailSettingController.php new file mode 100644 index 000000000..683b1d23c --- /dev/null +++ b/app/Http/Controllers/Settings/EmailSettingController.php @@ -0,0 +1,143 @@ + getSetting('email_provider', 'smtp'), + 'driver' => getSetting('email_driver', 'smtp'), + 'host' => getSetting('email_host', 'smtp.example.com'), + 'port' => getSetting('email_port', '587'), + 'username' => getSetting('email_username', 'user@example.com'), + 'password' => getSetting('email_password', ''), + 'encryption' => getSetting('email_encryption', 'tls'), + 'fromAddress' => getSetting('email_from_address', 'noreply@example.com'), + 'fromName' => getSetting('email_from_name', 'WorkDo System') + ]; + + // Mask password if it exists + if (!empty($settings['password'])) { + $settings['password'] = '••••••••••••'; + } + + return response()->json($settings); + } + + /** + * Update email settings for the authenticated user. + * + * @param \Illuminate\Http\Request $request + */ + public function updateEmailSettings(Request $request) + { + $user = Auth::user(); + $validated = $request->validate([ + 'provider' => 'required|string', + 'driver' => 'required|string', + 'host' => 'required|string', + 'port' => 'required|string', + 'username' => 'required|string', + 'password' => 'nullable|string', + 'encryption' => 'required|string', + 'fromAddress' => 'required|email', + 'fromName' => 'required|string', + ]); + + updateSetting('email_provider', $validated['provider']); + updateSetting('email_driver', $validated['driver']); + updateSetting('email_host', $validated['host']); + updateSetting('email_port', $validated['port']); + updateSetting('email_username', $validated['username']); + + // Only update password if provided and not masked + if (!empty($validated['password']) && $validated['password'] !== '••••••••••••') { + updateSetting('email_password', $validated['password']); + } + + updateSetting('email_encryption', $validated['encryption']); + updateSetting('email_from_address', $validated['fromAddress']); + updateSetting('email_from_name', $validated['fromName']); + + return redirect()->back()->with('success', __('Email settings updated successfully')); + } + + /** + * Send a test email. + * + * @param \Illuminate\Http\Request $request + */ + public function sendTestEmail(Request $request) + { + $validator = Validator::make( + $request->all(), + [ + 'email' => 'required|email', + ] + ); + + if ($validator->fails()) { + return redirect()->back()->with('error', $validator->errors()->first()); + } + + $settings = [ + 'provider' => getSetting('email_provider', 'smtp'), + 'driver' => getSetting('email_driver', 'smtp'), + 'host' => getSetting('email_host', 'smtp.example.com'), + 'port' => getSetting('email_port', '587'), + 'username' => getSetting('email_username', 'user@example.com'), + 'encryption' => getSetting('email_encryption', 'tls'), + 'fromAddress' => getSetting('email_from_address', 'noreply@example.com'), + 'fromName' => getSetting('email_from_name', 'WorkDo System') + ]; + + // Get the actual password (not masked) + $password = getSetting('email_password', ''); + + try { + // Configure mail settings for this request only + config([ + 'mail.default' => $settings['driver'], + 'mail.mailers.smtp.host' => $settings['host'], + 'mail.mailers.smtp.port' => $settings['port'], + 'mail.mailers.smtp.encryption' => $settings['encryption'] === 'none' ? null : $settings['encryption'], + 'mail.mailers.smtp.username' => $settings['username'], + 'mail.mailers.smtp.password' => $password, + 'mail.from.address' => $settings['fromAddress'], + 'mail.from.name' => $settings['fromName'], + ]); + + // Send test email + Mail::to($request->email)->send(new TestMail()); + + return redirect()->back()->with('success', __('Test email sent successfully to :email', ["email" => $request->email])); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to send test email: :message' , ["message" => $e->getMessage()])); + } + } + + /** + * Get a setting value for a user. + * + * @param int $userId + * @param string $key + * @param mixed $default + * @return mixed + */ + +} \ No newline at end of file diff --git a/app/Http/Controllers/Settings/PasswordController.php b/app/Http/Controllers/Settings/PasswordController.php new file mode 100644 index 000000000..38fb96ebf --- /dev/null +++ b/app/Http/Controllers/Settings/PasswordController.php @@ -0,0 +1,43 @@ + $request->user() instanceof MustVerifyEmail, + 'status' => $request->session()->get('status'), + ]); + } + + /** + * Update the user's password. + */ + public function update(Request $request): RedirectResponse + { + $validated = $request->validate([ + 'current_password' => ['required', 'current_password'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]); + + $request->user()->update([ + 'password' => Hash::make($validated['password']), + ]); + + return back()->with('success', __('Password updated successfully.')); + } +} diff --git a/app/Http/Controllers/Settings/PaymentSettingController.php b/app/Http/Controllers/Settings/PaymentSettingController.php new file mode 100644 index 000000000..7f61c3c63 --- /dev/null +++ b/app/Http/Controllers/Settings/PaymentSettingController.php @@ -0,0 +1,603 @@ + $paymentSettings, + ]); + } + + public function getPaymentMethods() + { + $superAdminId = \App\Models\User::where('type', 'superadmin')->first()?->id; + + if (!$superAdminId) { + return response()->json([]); + } + + $paymentSettings = getPaymentSettings($superAdminId); + + // Filter out sensitive credentials and only return safe configuration + $safeSettings = $this->filterSensitiveData($paymentSettings); + + // Add default currency to payment settings + $settings = settings($superAdminId); + $safeSettings['defaultCurrency'] = $settings['defaultCurrency'] ?? 'usd'; + + return response()->json($safeSettings); + } + public function store(Request $request) + { + try { + $validatedData = $request->validate([ + 'stripe_key' => 'nullable|string', + 'stripe_secret' => 'nullable|string', + 'paypal_client_id' => 'nullable|string', + 'paypal_secret_key' => 'nullable|string', + 'paypal_mode' => 'in:sandbox,live', + 'bank_detail' => 'nullable|string', + 'razorpay_key' => 'nullable|string', + 'razorpay_secret' => 'nullable|string', + 'mercadopago_mode' => 'in:sandbox,live', + 'mercadopago_access_token' => 'nullable|string', + 'paystack_public_key' => 'nullable|string', + 'paystack_secret_key' => 'nullable|string', + 'flutterwave_public_key' => 'nullable|string', + 'flutterwave_secret_key' => 'nullable|string', + 'paytabs_profile_id' => 'nullable|string', + 'paytabs_server_key' => 'nullable|string', + 'paytabs_region' => 'nullable|string', + 'paytabs_mode' => 'in:sandbox,live', + 'skrill_merchant_id' => 'nullable|string', + 'skrill_secret_word' => 'nullable|string', + 'coingate_api_token' => 'nullable|string', + 'coingate_mode' => 'in:sandbox,live', + 'payfast_merchant_id' => 'nullable|string', + 'payfast_merchant_key' => 'nullable|string', + 'payfast_passphrase' => 'nullable|string', + 'payfast_mode' => 'in:sandbox,live', + 'tap_secret_key' => 'nullable|string', + 'xendit_api_key' => 'nullable|string', + 'paytr_merchant_id' => 'nullable|string', + 'paytr_merchant_key' => 'nullable|string', + 'paytr_merchant_salt' => 'nullable|string', + 'mollie_api_key' => 'nullable|string', + 'toyyibpay_category_code' => 'nullable|string', + 'toyyibpay_secret_key' => 'nullable|string', + 'paymentwall_public_key' => 'nullable|string', + 'paymentwall_private_key' => 'nullable|string', + 'sspay_secret_key' => 'nullable|string', + 'sspay_category_code' => 'nullable|string', + 'benefit_mode' => 'in:sandbox,live', + 'benefit_secret_key' => 'nullable|string', + 'benefit_public_key' => 'nullable|string', + 'iyzipay_mode' => 'in:sandbox,live', + 'iyzipay_secret_key' => 'nullable|string', + 'iyzipay_public_key' => 'nullable|string', + 'aamarpay_store_id' => 'nullable|string', + 'aamarpay_signature' => 'nullable|string', + 'midtrans_mode' => 'in:sandbox,live', + 'midtrans_secret_key' => 'nullable|string', + 'yookassa_shop_id' => 'nullable|string', + 'yookassa_secret_key' => 'nullable|string', + 'nepalste_mode' => 'in:sandbox,live', + 'nepalste_secret_key' => 'nullable|string', + 'nepalste_public_key' => 'nullable|string', + 'paiement_merchant_id' => 'nullable|string', + 'cinetpay_site_id' => 'nullable|string', + 'cinetpay_api_key' => 'nullable|string', + 'cinetpay_secret_key' => 'nullable|string', + 'payhere_mode' => 'in:sandbox,live', + 'payhere_merchant_id' => 'nullable|string', + 'payhere_merchant_secret' => 'nullable|string', + 'payhere_app_id' => 'nullable|string', + 'payhere_app_secret' => 'nullable|string', + 'fedapay_mode' => 'in:sandbox,live', + 'fedapay_secret_key' => 'nullable|string', + 'fedapay_public_key' => 'nullable|string', + 'authorizenet_mode' => 'in:sandbox,live', + 'authorizenet_merchant_id' => 'nullable|string', + 'authorizenet_transaction_key' => 'nullable|string', + 'khalti_secret_key' => 'nullable|string', + 'khalti_public_key' => 'nullable|string', + 'easebuzz_merchant_key' => 'nullable|string', + 'easebuzz_salt_key' => 'nullable|string', + 'easebuzz_environment' => 'nullable|string', + 'ozow_mode' => 'in:sandbox,live', + 'ozow_site_key' => 'nullable|string', + 'ozow_private_key' => 'nullable|string', + 'ozow_api_key' => 'nullable|string', + 'cashfree_mode' => 'in:sandbox,live', + 'cashfree_secret_key' => 'nullable|string', + 'cashfree_public_key' => 'nullable|string', + ]); + + $settings = $this->preparePaymentSettings($request, $validatedData); + $this->validateEnabledPaymentMethods($request, $validatedData); + $this->savePaymentSettings($settings); + + return back()->with('success', __('Payment settings saved successfully.')); + } catch (\Illuminate\Validation\ValidationException $e) { + return back()->withErrors($e->errors()); + } catch (\Exception $e) { + return back()->withErrors(['error' => __('Failed to save payment settings: :message', ['message' => $e->getMessage()])]); + } + } + + private function preparePaymentSettings(Request $request, array $validatedData): array + { + return [ + 'is_manually_enabled' => $request->boolean('is_manually_enabled'), + 'is_bank_enabled' => $request->boolean('is_bank_enabled'), + 'is_stripe_enabled' => $request->boolean('is_stripe_enabled'), + 'is_paypal_enabled' => $request->boolean('is_paypal_enabled'), + 'is_razorpay_enabled' => $request->boolean('is_razorpay_enabled'), + 'is_mercadopago_enabled' => $request->boolean('is_mercadopago_enabled'), + 'is_paystack_enabled' => $request->boolean('is_paystack_enabled'), + 'is_flutterwave_enabled' => $request->boolean('is_flutterwave_enabled'), + 'is_paytabs_enabled' => $request->boolean('is_paytabs_enabled'), + 'is_skrill_enabled' => $request->boolean('is_skrill_enabled'), + 'is_coingate_enabled' => $request->boolean('is_coingate_enabled'), + 'is_payfast_enabled' => $request->boolean('is_payfast_enabled'), + 'is_tap_enabled' => $request->boolean('is_tap_enabled'), + 'is_xendit_enabled' => $request->boolean('is_xendit_enabled'), + 'is_paytr_enabled' => $request->boolean('is_paytr_enabled'), + 'is_mollie_enabled' => $request->boolean('is_mollie_enabled'), + 'is_toyyibpay_enabled' => $request->boolean('is_toyyibpay_enabled'), + 'is_paymentwall_enabled' => $request->boolean('is_paymentwall_enabled'), + 'is_sspay_enabled' => $request->boolean('is_sspay_enabled'), + 'is_benefit_enabled' => $request->boolean('is_benefit_enabled'), + 'is_iyzipay_enabled' => $request->boolean('is_iyzipay_enabled'), + 'is_aamarpay_enabled' => $request->boolean('is_aamarpay_enabled'), + 'is_midtrans_enabled' => $request->boolean('is_midtrans_enabled'), + 'is_yookassa_enabled' => $request->boolean('is_yookassa_enabled'), + 'is_nepalste_enabled' => $request->boolean('is_nepalste_enabled'), + 'is_paiement_enabled' => $request->boolean('is_paiement_enabled'), + 'is_cinetpay_enabled' => $request->boolean('is_cinetpay_enabled'), + 'is_payhere_enabled' => $request->boolean('is_payhere_enabled'), + 'is_fedapay_enabled' => $request->boolean('is_fedapay_enabled'), + 'is_authorizenet_enabled' => $request->boolean('is_authorizenet_enabled'), + 'is_khalti_enabled' => $request->boolean('is_khalti_enabled'), + 'is_easebuzz_enabled' => $request->boolean('is_easebuzz_enabled'), + 'is_ozow_enabled' => $request->boolean('is_ozow_enabled'), + 'is_cashfree_enabled' => $request->boolean('is_cashfree_enabled'), + 'paypal_mode' => $validatedData['paypal_mode'] ?? 'sandbox', + 'mercadopago_mode' => $validatedData['mercadopago_mode'] ?? 'sandbox', + 'bank_detail' => $validatedData['bank_detail'], + 'stripe_key' => $validatedData['stripe_key'], + 'stripe_secret' => $validatedData['stripe_secret'], + 'paypal_client_id' => $validatedData['paypal_client_id'], + 'paypal_secret_key' => $validatedData['paypal_secret_key'], + 'razorpay_key' => $validatedData['razorpay_key'], + 'razorpay_secret' => $validatedData['razorpay_secret'], + 'mercadopago_access_token' => $validatedData['mercadopago_access_token'], + 'paystack_public_key' => $validatedData['paystack_public_key'], + 'paystack_secret_key' => $validatedData['paystack_secret_key'], + 'flutterwave_public_key' => $validatedData['flutterwave_public_key'], + 'flutterwave_secret_key' => $validatedData['flutterwave_secret_key'], + 'paytabs_profile_id' => $validatedData['paytabs_profile_id'], + 'paytabs_server_key' => $validatedData['paytabs_server_key'], + 'paytabs_region' => $validatedData['paytabs_region'], + 'paytabs_mode' => $validatedData['paytabs_mode'] ?? 'sandbox', + 'skrill_merchant_id' => $validatedData['skrill_merchant_id'], + 'skrill_secret_word' => $validatedData['skrill_secret_word'], + 'coingate_api_token' => $validatedData['coingate_api_token'], + 'coingate_mode' => $validatedData['coingate_mode'] ?? 'sandbox', + 'payfast_merchant_id' => $validatedData['payfast_merchant_id'], + 'payfast_merchant_key' => $validatedData['payfast_merchant_key'], + 'payfast_passphrase' => $validatedData['payfast_passphrase'], + 'payfast_mode' => $validatedData['payfast_mode'] ?? 'sandbox', + 'tap_secret_key' => $validatedData['tap_secret_key'], + 'xendit_api_key' => $validatedData['xendit_api_key'], + 'paytr_merchant_id' => $validatedData['paytr_merchant_id'], + 'paytr_merchant_key' => $validatedData['paytr_merchant_key'], + 'paytr_merchant_salt' => $validatedData['paytr_merchant_salt'], + 'mollie_api_key' => $validatedData['mollie_api_key'], + 'toyyibpay_category_code' => $validatedData['toyyibpay_category_code'], + 'toyyibpay_secret_key' => $validatedData['toyyibpay_secret_key'], + 'paymentwall_public_key' => $validatedData['paymentwall_public_key'], + 'paymentwall_private_key' => $validatedData['paymentwall_private_key'], + 'sspay_secret_key' => $validatedData['sspay_secret_key'], + 'sspay_category_code' => $validatedData['sspay_category_code'], + 'benefit_mode' => $validatedData['benefit_mode'] ?? 'sandbox', + 'benefit_secret_key' => $validatedData['benefit_secret_key'], + 'benefit_public_key' => $validatedData['benefit_public_key'], + 'iyzipay_mode' => $validatedData['iyzipay_mode'] ?? 'sandbox', + 'iyzipay_secret_key' => $validatedData['iyzipay_secret_key'], + 'iyzipay_public_key' => $validatedData['iyzipay_public_key'], + 'aamarpay_store_id' => $validatedData['aamarpay_store_id'], + 'aamarpay_signature' => $validatedData['aamarpay_signature'], + 'midtrans_mode' => $validatedData['midtrans_mode'] ?? 'sandbox', + 'midtrans_secret_key' => $validatedData['midtrans_secret_key'], + 'yookassa_shop_id' => $validatedData['yookassa_shop_id'], + 'yookassa_secret_key' => $validatedData['yookassa_secret_key'], + 'nepalste_mode' => $validatedData['nepalste_mode'] ?? 'sandbox', + 'nepalste_secret_key' => $validatedData['nepalste_secret_key'], + 'nepalste_public_key' => $validatedData['nepalste_public_key'], + 'paiement_merchant_id' => $validatedData['paiement_merchant_id'], + 'cinetpay_site_id' => $validatedData['cinetpay_site_id'], + 'cinetpay_api_key' => $validatedData['cinetpay_api_key'], + 'cinetpay_secret_key' => $validatedData['cinetpay_secret_key'], + 'payhere_mode' => $validatedData['payhere_mode'] ?? 'sandbox', + 'payhere_merchant_id' => $validatedData['payhere_merchant_id'], + 'payhere_merchant_secret' => $validatedData['payhere_merchant_secret'], + 'payhere_app_id' => $validatedData['payhere_app_id'], + 'payhere_app_secret' => $validatedData['payhere_app_secret'], + 'fedapay_mode' => $validatedData['fedapay_mode'] ?? 'sandbox', + 'fedapay_secret_key' => $validatedData['fedapay_secret_key'], + 'fedapay_public_key' => $validatedData['fedapay_public_key'], + 'authorizenet_mode' => $validatedData['authorizenet_mode'] ?? 'sandbox', + 'authorizenet_merchant_id' => $validatedData['authorizenet_merchant_id'], + 'authorizenet_transaction_key' => $validatedData['authorizenet_transaction_key'], + 'khalti_secret_key' => $validatedData['khalti_secret_key'], + 'khalti_public_key' => $validatedData['khalti_public_key'], + 'easebuzz_merchant_key' => $validatedData['easebuzz_merchant_key'], + 'easebuzz_salt_key' => $validatedData['easebuzz_salt_key'], + 'easebuzz_environment' => $validatedData['easebuzz_environment'], + 'ozow_mode' => $validatedData['ozow_mode'] ?? 'sandbox', + 'ozow_site_key' => $validatedData['ozow_site_key'], + 'ozow_private_key' => $validatedData['ozow_private_key'], + 'ozow_api_key' => $validatedData['ozow_api_key'], + 'cashfree_mode' => $validatedData['cashfree_mode'] ?? 'sandbox', + 'cashfree_secret_key' => $validatedData['cashfree_secret_key'], + 'cashfree_public_key' => $validatedData['cashfree_public_key'], + ]; + } + + private function validateEnabledPaymentMethods(Request $request, array $validatedData): void + { + $errors = []; + + if ($request->boolean('is_stripe_enabled')) { + $config = ['key' => $validatedData['stripe_key'], 'secret' => $validatedData['stripe_secret']]; + $validation = validatePaymentMethodConfig('stripe', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_paypal_enabled')) { + $config = ['client_id' => $validatedData['paypal_client_id'], 'secret' => $validatedData['paypal_secret_key']]; + $validation = validatePaymentMethodConfig('paypal', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_razorpay_enabled')) { + $config = ['key' => $validatedData['razorpay_key'], 'secret' => $validatedData['razorpay_secret']]; + $validation = validatePaymentMethodConfig('razorpay', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_mercadopago_enabled')) { + $config = ['access_token' => $validatedData['mercadopago_access_token']]; + $validation = validatePaymentMethodConfig('mercadopago', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_paystack_enabled')) { + $config = ['public_key' => $validatedData['paystack_public_key'], 'secret_key' => $validatedData['paystack_secret_key']]; + $validation = validatePaymentMethodConfig('paystack', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_flutterwave_enabled')) { + $config = ['public_key' => $validatedData['flutterwave_public_key'], 'secret_key' => $validatedData['flutterwave_secret_key']]; + $validation = validatePaymentMethodConfig('flutterwave', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_bank_enabled')) { + $config = ['details' => $validatedData['bank_detail']]; + $validation = validatePaymentMethodConfig('bank', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_paytabs_enabled')) { + $config = ['server_key' => $validatedData['paytabs_server_key'], 'profile_id' => $validatedData['paytabs_profile_id'], 'region' => $validatedData['paytabs_region']]; + $validation = validatePaymentMethodConfig('paytabs', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_skrill_enabled')) { + $config = ['merchant_id' => $validatedData['skrill_merchant_id'], 'secret_word' => $validatedData['skrill_secret_word']]; + $validation = validatePaymentMethodConfig('skrill', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_coingate_enabled')) { + $config = ['api_token' => $validatedData['coingate_api_token']]; + $validation = validatePaymentMethodConfig('coingate', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_payfast_enabled')) { + $config = ['merchant_id' => $validatedData['payfast_merchant_id'], 'merchant_key' => $validatedData['payfast_merchant_key']]; + $validation = validatePaymentMethodConfig('payfast', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_tap_enabled')) { + $config = ['secret_key' => $validatedData['tap_secret_key']]; + $validation = validatePaymentMethodConfig('tap', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_xendit_enabled')) { + $config = ['api_key' => $validatedData['xendit_api_key']]; + $validation = validatePaymentMethodConfig('xendit', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_paytr_enabled')) { + $config = ['merchant_id' => $validatedData['paytr_merchant_id'], 'merchant_key' => $validatedData['paytr_merchant_key'], 'merchant_salt' => $validatedData['paytr_merchant_salt']]; + $validation = validatePaymentMethodConfig('paytr', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_mollie_enabled')) { + $config = ['api_key' => $validatedData['mollie_api_key']]; + $validation = validatePaymentMethodConfig('mollie', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_toyyibpay_enabled')) { + $config = ['category_code' => $validatedData['toyyibpay_category_code'], 'secret_key' => $validatedData['toyyibpay_secret_key']]; + $validation = validatePaymentMethodConfig('toyyibpay', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_cashfree_enabled')) { + $config = ['public_key' => $validatedData['cashfree_public_key'], 'secret_key' => $validatedData['cashfree_secret_key']]; + $validation = validatePaymentMethodConfig('cashfree', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_ozow_enabled')) { + $config = ['site_key' => $validatedData['ozow_site_key'], 'private_key' => $validatedData['ozow_private_key']]; + $validation = validatePaymentMethodConfig('ozow', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_easebuzz_enabled')) { + $config = ['merchant_key' => $validatedData['easebuzz_merchant_key'], 'salt_key' => $validatedData['easebuzz_salt_key']]; + $validation = validatePaymentMethodConfig('easebuzz', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_khalti_enabled')) { + $config = ['public_key' => $validatedData['khalti_public_key'], 'secret_key' => $validatedData['khalti_secret_key']]; + $validation = validatePaymentMethodConfig('khalti', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_authorizenet_enabled')) { + $config = ['merchant_id' => $validatedData['authorizenet_merchant_id'], 'transaction_key' => $validatedData['authorizenet_transaction_key']]; + $validation = validatePaymentMethodConfig('authorizenet', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_fedapay_enabled')) { + $config = ['public_key' => $validatedData['fedapay_public_key'], 'secret_key' => $validatedData['fedapay_secret_key']]; + $validation = validatePaymentMethodConfig('fedapay', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_payhere_enabled')) { + $config = ['merchant_id' => $validatedData['payhere_merchant_id'], 'merchant_secret' => $validatedData['payhere_merchant_secret']]; + $validation = validatePaymentMethodConfig('payhere', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_cinetpay_enabled')) { + $config = ['site_id' => $validatedData['cinetpay_site_id'], 'api_key' => $validatedData['cinetpay_api_key']]; + $validation = validatePaymentMethodConfig('cinetpay', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_paiement_enabled')) { + $config = ['merchant_id' => $validatedData['paiement_merchant_id']]; + $validation = validatePaymentMethodConfig('paiement', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_nepalste_enabled')) { + $config = ['public_key' => $validatedData['nepalste_public_key'], 'secret_key' => $validatedData['nepalste_secret_key']]; + $validation = validatePaymentMethodConfig('nepalste', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_yookassa_enabled')) { + $config = ['shop_id' => $validatedData['yookassa_shop_id'], 'secret_key' => $validatedData['yookassa_secret_key']]; + $validation = validatePaymentMethodConfig('yookassa', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_midtrans_enabled')) { + $config = ['secret_key' => $validatedData['midtrans_secret_key']]; + $validation = validatePaymentMethodConfig('midtrans', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_aamarpay_enabled')) { + $config = ['store_id' => $validatedData['aamarpay_store_id'], 'signature' => $validatedData['aamarpay_signature']]; + $validation = validatePaymentMethodConfig('aamarpay', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_iyzipay_enabled')) { + $config = ['public_key' => $validatedData['iyzipay_public_key'], 'secret_key' => $validatedData['iyzipay_secret_key']]; + $validation = validatePaymentMethodConfig('iyzipay', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_paymentwall_enabled')) { + $config = ['public_key' => $validatedData['paymentwall_public_key'], 'private_key' => $validatedData['paymentwall_private_key']]; + $validation = validatePaymentMethodConfig('paymentwall', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_sspay_enabled')) { + $config = ['secret_key' => $validatedData['sspay_secret_key']]; + $validation = validatePaymentMethodConfig('sspay', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if ($request->boolean('is_benefit_enabled')) { + $config = ['public_key' => $validatedData['benefit_public_key'], 'secret_key' => $validatedData['benefit_secret_key']]; + $validation = validatePaymentMethodConfig('benefit', $config); + if (!$validation['valid']) { + $errors = array_merge($errors, $validation['errors']); + } + } + + if (!empty($errors)) { + throw \Illuminate\Validation\ValidationException::withMessages([ + 'payment_methods' => $errors + ]); + } + } + + private function savePaymentSettings(array $settings): void + { + foreach ($settings as $key => $value) { + updatePaymentSetting($key, $value); + } + } + + public function getEnabledMethods() + { + $enabledMethods = getEnabledPaymentMethods(); + + return response()->json($enabledMethods); + } + + /** + * Filter out sensitive payment gateway credentials + * + * @param array $settings + * @return array + */ + private function filterSensitiveData(array $settings): array + { + $safeSettings = []; + + // Only include enabled status and safe configuration + $enabledKeys = [ + 'is_manually_enabled', 'is_bank_enabled', 'is_stripe_enabled', 'is_paypal_enabled', + 'is_razorpay_enabled', 'is_mercadopago_enabled', 'is_paystack_enabled', 'is_flutterwave_enabled', + 'is_paytabs_enabled', 'is_skrill_enabled', 'is_coingate_enabled', 'is_payfast_enabled', + 'is_tap_enabled', 'is_xendit_enabled', 'is_paytr_enabled', 'is_mollie_enabled', + 'is_toyyibpay_enabled', 'is_paymentwall_enabled', 'is_sspay_enabled', 'is_benefit_enabled', + 'is_iyzipay_enabled', 'is_aamarpay_enabled', 'is_midtrans_enabled', 'is_yookassa_enabled', + 'is_nepalste_enabled', 'is_paiement_enabled', 'is_cinetpay_enabled', 'is_payhere_enabled', + 'is_fedapay_enabled', 'is_authorizenet_enabled', 'is_khalti_enabled', 'is_easebuzz_enabled', + 'is_ozow_enabled', 'is_cashfree_enabled' + ]; + + $modeKeys = [ + 'paypal_mode', 'mercadopago_mode', 'paytabs_mode', 'coingate_mode', 'payfast_mode', + 'benefit_mode', 'iyzipay_mode', 'midtrans_mode', 'nepalste_mode', 'payhere_mode', + 'fedapay_mode', 'authorizenet_mode', 'ozow_mode', 'cashfree_mode' + ]; + + // Keys needed by frontend payment components (safe to expose) + $frontendKeys = [ + // Public keys for SDK initialization + 'stripe_key', 'razorpay_key', 'paystack_public_key', 'flutterwave_public_key', + 'khalti_public_key', 'cashfree_public_key', 'iyzipay_public_key', 'benefit_public_key', + 'fedapay_public_key', 'nepalste_public_key', 'paymentwall_public_key', + + // Client/Merchant IDs and category codes (non-sensitive identifiers) + 'paypal_client_id', 'toyyibpay_category_code', 'aamarpay_store_id', + 'authorizenet_merchant_id', 'cinetpay_site_id', 'easebuzz_merchant_key', + 'ozow_site_key', 'paiement_merchant_id', 'payfastMerchantId', + 'payhere_merchant_id', 'paytr_merchant_id', 'skrill_merchant_id', + 'yookassa_shop_id', + + // Bank details (non-sensitive display info) + 'bank_detail' + ]; + + // Include enabled status, modes, and frontend keys only + foreach (array_merge($enabledKeys, $modeKeys, $frontendKeys) as $key) { + if (isset($settings[$key])) { + $safeSettings[$key] = $settings[$key]; + } + } + + return $safeSettings; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Settings/ProfileController.php b/app/Http/Controllers/Settings/ProfileController.php new file mode 100644 index 000000000..fcf052bef --- /dev/null +++ b/app/Http/Controllers/Settings/ProfileController.php @@ -0,0 +1,97 @@ + $request->user() instanceof MustVerifyEmail, + 'status' => $request->session()->get('status'), + ]); + } + + /** + * Update the user's profile settings. + */ + public function update(ProfileUpdateRequest $request): RedirectResponse + { + $validated = $request->validated(); + + // Remove _method from validated data if present + unset($validated['_method']); + + // Remove avatar from validated data if no file is uploaded + // This prevents setting avatar to null in the database + if (!$request->hasFile('avatar')) { + unset($validated['avatar']); + } + + // Handle avatar upload + if ($request->hasFile('avatar')) { + // Delete old avatar if exists + if ($request->user()->avatar && check_file($request->user()->avatar)) { + delete_file($request->user()->avatar); + } + + $filenameWithExt = $request->file('avatar')->getClientOriginalName(); + $filename = pathinfo($filenameWithExt, PATHINFO_FILENAME); + $extension = $request->file('avatar')->getClientOriginalExtension(); + $fileNameToStore = $filename . '_' . time() . '.' . $extension; + + $upload = upload_file($request, 'avatar', $fileNameToStore, 'avatars'); + if ($upload['status'] == true) { + $validated['avatar'] = $upload['url']; + } else { + return redirect()->back() + ->withErrors(['avatar' => $upload['msg']]) + ->withInput(); + } + } + + $request->user()->fill($validated); + + if ($request->user()->isDirty('email')) { + $request->user()->email_verified_at = null; + } + + $request->user()->save(); + + return to_route('profile')->with('success', __('Profile updated successfully.')); + } + + /** + * Delete the user's account. + */ + public function destroy(Request $request): RedirectResponse + { + $request->validate([ + 'password' => ['required', 'current_password'], + ]); + + $user = $request->user(); + + Auth::logout(); + + $user->delete(); + + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect('/'); + } +} diff --git a/app/Http/Controllers/Settings/SettingsController.php b/app/Http/Controllers/Settings/SettingsController.php new file mode 100644 index 000000000..3a26c2936 --- /dev/null +++ b/app/Http/Controllers/Settings/SettingsController.php @@ -0,0 +1,68 @@ +id()); + $webhooks = Webhook::where('user_id', auth()->id())->get(); + $ipRestrictions = IpRestriction::whereIn('created_by', getCompanyAndUsersId())->orderBy('id', 'desc')->get(); + + // Get Zekto settings for company users + $zektoSettings = []; + $zektoSettings = [ + 'zkteco_api_url' => isset($systemSettings['zkteco_api_url']) ? $systemSettings['zkteco_api_url'] : '', + 'zkteco_username' => isset($systemSettings['zkteco_username']) ? $systemSettings['zkteco_username'] : '', + 'zkteco_password' => isset($systemSettings['zkteco_password']) ? $systemSettings['zkteco_password'] : '', + 'zkteco_auth_token' => isset($systemSettings['zkteco_auth_token']) ? $systemSettings['zkteco_auth_token'] : '', + ]; + + // Get NOC templates for company users + $nocTemplates = NocTemplate::where('created_by', Auth::user()->id)->get(); + + // Get Joining Letter templates for company users + $joiningLetterTemplates = JoiningLetterTemplate::where('created_by', Auth::user()->id)->get(); + + // Get Experience Certificate templates for company users + $experienceCertificateTemplates = ExperienceCertificateTemplate::where('created_by', Auth::user()->id)->get(); + + return Inertia::render('settings/index', [ + 'systemSettings' => $systemSettings, + 'settings' => $systemSettings, // For helper functions + 'cacheSize' => getCacheSize(), + 'currencies' => $currencies, + 'timezones' => config('timezones'), + 'dateFormats' => config('dateformat'), + 'timeFormats' => config('timeformat'), + 'paymentSettings' => $paymentSettings, + 'webhooks' => $webhooks, + 'zektoSettings' => $zektoSettings, + 'ipRestrictions' => $ipRestrictions, + 'nocTemplates' => $nocTemplates, + 'joiningLetterTemplates' => $joiningLetterTemplates, + 'experienceCertificateTemplates' => $experienceCertificateTemplates, + ]); + } +} diff --git a/app/Http/Controllers/Settings/SystemSettingsController.php b/app/Http/Controllers/Settings/SystemSettingsController.php new file mode 100644 index 000000000..b715a0225 --- /dev/null +++ b/app/Http/Controllers/Settings/SystemSettingsController.php @@ -0,0 +1,361 @@ + 'required|string', + 'dateFormat' => 'required|string', + 'timeFormat' => 'required|string', + 'calendarStartDay' => 'required|string', + 'defaultTimezone' => 'required|string', + 'emailVerification' => 'boolean', + 'landingPageEnabled' => 'boolean', + 'ipRestrictionEnabled' => 'boolean', + ]; + + if(isSaaS()){ + $rules['termsConditionsUrl'] = 'nullable'; + $rules['userRegistrationEnabled'] = 'boolean'; + } else { + $rules['termsConditionsUrl'] = 'nullable'; + } + + $validated = $request->validate($rules); + + foreach ($validated as $key => $value) { + updateSetting($key, $value); + } + + return redirect()->back()->with('success', __('System settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update system settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Update the brand settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function updateBrand(Request $request) + { + try { + $validated = $request->validate([ + 'settings' => 'required|array', + 'settings.logoDark' => 'nullable|string', + 'settings.logoLight' => 'nullable|string', + 'settings.favicon' => 'nullable|string', + 'settings.titleText' => 'nullable|string|max:255', + 'settings.footerText' => 'nullable|string|max:500', + 'settings.companyMobile' => 'nullable|string|max:20', + 'settings.companyAddress' => 'nullable', + 'settings.themeColor' => 'nullable|string|in:blue,green,purple,orange,red,custom', + 'settings.customColor' => 'nullable|string|regex:/^#[0-9A-Fa-f]{6}$/', + 'settings.sidebarVariant' => 'nullable|string|in:inset,floating,minimal', + 'settings.sidebarStyle' => 'nullable|string|in:plain,colored,gradient', + 'settings.layoutDirection' => 'nullable|string|in:left,right', + 'settings.themeMode' => 'nullable|string|in:light,dark,system', + ]); + + $userId = auth()->id(); + foreach ($validated['settings'] as $key => $value) { + updateSetting($key, $value, $userId); + } + + return redirect()->back()->with('success', __('Brand settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update brand settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Update the recaptcha settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function updateRecaptcha(Request $request) + { + try { + $validated = $request->validate([ + 'recaptchaEnabled' => 'boolean', + 'recaptchaVersion' => 'required|in:v2,v3', + 'recaptchaSiteKey' => 'required|string', + 'recaptchaSecretKey' => 'required|string', + ]); + + foreach ($validated as $key => $value) { + updateSetting($key, $value); + } + + return redirect()->back()->with('success', __('ReCaptcha settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update ReCaptcha settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Update the chatgpt settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function updateChatgpt(Request $request) + { + try { + $validated = $request->validate([ + 'chatgptKey' => 'required|string', + 'chatgptModel' => 'required|string', + ]); + + foreach ($validated as $key => $value) { + updateSetting($key, $value); + } + + return redirect()->back()->with('success', __('Chat GPT settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update Chat GPT settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Update the storage settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function updateStorage(Request $request) + { + try { + $validated = $request->validate([ + 'storage_type' => 'required|in:local,aws_s3,wasabi', + 'allowedFileTypes' => 'required|string', + 'maxUploadSize' => 'required|numeric|min:1', + 'awsAccessKeyId' => 'required_if:storage_type,aws_s3|string', + 'awsSecretAccessKey' => 'required_if:storage_type,aws_s3|string', + 'awsDefaultRegion' => 'required_if:storage_type,aws_s3|string', + 'awsBucket' => 'required_if:storage_type,aws_s3|string', + 'awsUrl' => 'required_if:storage_type,aws_s3|string', + 'awsEndpoint' => 'required_if:storage_type,aws_s3|string', + 'wasabiAccessKey' => 'required_if:storage_type,wasabi|string', + 'wasabiSecretKey' => 'required_if:storage_type,wasabi|string', + 'wasabiRegion' => 'required_if:storage_type,wasabi|string', + 'wasabiBucket' => 'required_if:storage_type,wasabi|string', + 'wasabiUrl' => 'required_if:storage_type,wasabi|string', + 'wasabiRoot' => 'required_if:storage_type,wasabi|string', + ]); + + $userId = Auth::id(); + + $settings = [ + 'storage_type' => $validated['storage_type'], + 'storage_file_types' => $validated['allowedFileTypes'], + 'storage_max_upload_size' => $validated['maxUploadSize'], + ]; + + if ($validated['storage_type'] === 'aws_s3') { + $settings['aws_access_key_id'] = $validated['awsAccessKeyId']; + $settings['aws_secret_access_key'] = $validated['awsSecretAccessKey']; + $settings['aws_default_region'] = $validated['awsDefaultRegion']; + $settings['aws_bucket'] = $validated['awsBucket']; + $settings['aws_url'] = $validated['awsUrl']; + $settings['aws_endpoint'] = $validated['awsEndpoint']; + } + + if ($validated['storage_type'] === 'wasabi') { + $settings['wasabi_access_key'] = $validated['wasabiAccessKey']; + $settings['wasabi_secret_key'] = $validated['wasabiSecretKey']; + $settings['wasabi_region'] = $validated['wasabiRegion']; + $settings['wasabi_bucket'] = $validated['wasabiBucket']; + $settings['wasabi_url'] = $validated['wasabiUrl']; + $settings['wasabi_root'] = $validated['wasabiRoot']; + } + + foreach ($settings as $key => $value) { + updateSetting($key, $value); + } + + StorageConfigService::clearCache(); + + return redirect()->back()->with('success', __('Storage settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update storage settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Update the cookie settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function updateCookie(Request $request) + { + try { + $validated = $request->validate([ + 'enableLogging' => 'required|boolean', + 'strictlyNecessaryCookies' => 'required|boolean', + 'cookieTitle' => 'required|string|max:255', + 'strictlyCookieTitle' => 'required|string|max:255', + 'cookieDescription' => 'required|string', + 'strictlyCookieDescription' => 'required|string', + 'contactUsDescription' => 'required|string', + 'contactUsUrl' => 'required|url', + ]); + + foreach ($validated as $key => $value) { + updateSetting($key, is_bool($value) ? ($value ? '1' : '0') : $value); + } + + return redirect()->back()->with('success', __('Cookie settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update cookie settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Update the SEO settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function updateSeo(Request $request) + { + try { + $validated = $request->validate([ + 'metaKeywords' => 'required|string|max:255', + 'metaDescription' => 'required|string|max:160', + 'metaImage' => 'nullable', + ]); + + if ($request->hasFile('metaImage')) { + $filenameWithExt = $request->file('metaImage')->getClientOriginalName(); + $filename = pathinfo($filenameWithExt, PATHINFO_FILENAME); + $extension = $request->file('metaImage')->getClientOriginalExtension(); + $fileNameToStore = $filename . '_' . time() . '.' . $extension; + + $upload = upload_file($request, 'metaImage', $fileNameToStore, 'seo'); + if ($upload['status'] == true) { + $validated['metaImage'] = $upload['url']; + } else { + return redirect()->back() + ->withErrors(['metaImage' => $upload['msg']]) + ->withInput(); + } + } + + foreach ($validated as $key => $value) { + updateSetting($key, $value); + } + + return redirect()->back()->with('success', __('SEO settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update SEO settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Update the Google Calendar settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function updateGoogleCalendar(Request $request) + { + try { + $validated = $request->validate([ + 'googleCalendarEnabled' => 'boolean', + 'googleCalendarId' => 'nullable|string|max:255', + 'googleCalendarJson' => 'nullable|file|mimes:json|max:2048', + ]); + + $settings = [ + 'googleCalendarEnabled' => $validated['googleCalendarEnabled'] ?? false, + 'googleCalendarId' => $validated['googleCalendarId'] ?? '', + ]; + + // Handle JSON file upload + if ($request->hasFile('googleCalendarJson')) { + $file = $request->file('googleCalendarJson'); + $path = $file->store('google-calendar', 'public'); + $settings['googleCalendarJsonPath'] = $path; + } + + foreach ($settings as $key => $value) { + updateSetting($key, is_bool($value) ? ($value ? '1' : '0') : $value); + } + + return redirect()->back()->with('success', __('Google Calendar settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update Google Calendar settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Update the Google Wallet settings. + * + * @param \Illuminate\Http\Request $request + * @return \Illuminate\Http\RedirectResponse + */ + public function updateGoogleWallet(Request $request) + { + try { + $validated = $request->validate([ + 'googleWalletIssuerId' => 'nullable|string|max:255', + 'googleWalletJson' => 'nullable|file|mimes:json|max:2048', + ]); + + $settings = [ + 'googleWalletIssuerId' => $validated['googleWalletIssuerId'] ?? '', + ]; + + // Handle JSON file upload + if ($request->hasFile('googleWalletJson')) { + $file = $request->file('googleWalletJson'); + $path = $file->store('google-wallet', 'public'); + $settings['googleWalletJsonPath'] = $path; + } + + foreach ($settings as $key => $value) { + updateSetting($key, $value); + } + + return redirect()->back()->with('success', __('Google Wallet settings updated successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to update Google Wallet settings: :error', ['error' => $e->getMessage()])); + } + } + + /** + * Clear application cache. + * + * @return \Illuminate\Http\RedirectResponse + */ + public function clearCache() + { + try { + \Artisan::call('cache:clear'); + \Artisan::call('route:clear'); + \Artisan::call('view:clear'); + \Artisan::call('optimize:clear'); + + return redirect()->back()->with('success', __('Cache cleared successfully.')); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to clear cache: :error', ['error' => $e->getMessage()])); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Settings/WebhookController.php b/app/Http/Controllers/Settings/WebhookController.php new file mode 100644 index 000000000..4caf703c4 --- /dev/null +++ b/app/Http/Controllers/Settings/WebhookController.php @@ -0,0 +1,73 @@ +id())->get(); + return response()->json($webhooks); + } + + public function store(Request $request): JsonResponse + { + $request->validate([ + 'module' => 'required|in:New User,New Appointment', + 'method' => 'required|in:GET,POST', + 'url' => 'required|url', + ]); + + $webhook = Webhook::create([ + 'user_id' => auth()->id(), + 'module' => $request->module, + 'method' => $request->method, + 'url' => $request->url, + ]); + + return response()->json([ + 'webhook' => $webhook, + 'message' => __('Webhook created successfully') + ]); + } + + public function update(Request $request, Webhook $webhook): JsonResponse + { + if ($webhook->user_id !== auth()->id()) { + return response()->json(['message' => 'Unauthorized'], 403); + } + + $request->validate([ + 'module' => 'required|in:New User,New Appointment', + 'method' => 'required|in:GET,POST', + 'url' => 'required|url', + ]); + + $webhook->update([ + 'module' => $request->module, + 'method' => $request->method, + 'url' => $request->url, + ]); + + return response()->json([ + 'webhook' => $webhook, + 'message' => __('Webhook updated successfully') + ]); + } + + public function destroy(Webhook $webhook): JsonResponse + { + if ($webhook->user_id !== auth()->id()) { + return response()->json(['message' => 'Unauthorized'], 403); + } + + $webhook->delete(); + + return response()->json(['message' => __('Webhook deleted successfully')]); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Settings/WorkingDaysSettingController.php b/app/Http/Controllers/Settings/WorkingDaysSettingController.php new file mode 100644 index 000000000..400fdfc45 --- /dev/null +++ b/app/Http/Controllers/Settings/WorkingDaysSettingController.php @@ -0,0 +1,46 @@ +dayOfWeek($dayId)->format('l')); + $settings["working_day_{$dayName}"] = true; + } + + return response()->json($settings); + } + + public function updateWorkingDaysSettings(Request $request) + { + if (Auth::user()->can('update-working-days-settings')) { + $validated = $request->validate([ + 'working_days' => 'required|array', + 'working_days.*' => 'required|string|in:monday,tuesday,wednesday,thursday,friday,saturday,sunday', + ]); + + $workingDayIds = []; + foreach ($validated['working_days'] as $dayName) { + $dayId = \Carbon\Carbon::parse($dayName)->dayOfWeek; + $workingDayIds[] = $dayId; // Convert Sunday from 0 to 7 + } + + updateSetting('working_days', json_encode($workingDayIds)); + + return redirect()->back()->with('success', __('Working days settings updated successfully.')); + }else{ + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/ShiftController.php b/app/Http/Controllers/ShiftController.php new file mode 100644 index 000000000..132c50149 --- /dev/null +++ b/app/Http/Controllers/ShiftController.php @@ -0,0 +1,203 @@ +can('manage-shifts')) { + $query = Shift::with(['creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-shifts')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-shifts')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle shift type filter + if ($request->has('shift_type') && !empty($request->shift_type) && $request->shift_type !== 'all') { + if ($request->shift_type === 'night') { + $query->where('is_night_shift', true); + } else { + $query->where('is_night_shift', false); + } + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'name') { + $query->orderBy('name', $sortDirection); + } else { + $query->orderBy('created_at', 'desc'); + } + } else { + $query->orderBy('created_at', 'desc'); + } + + $shifts = $query->paginate($request->per_page ?? 9); + + // Stats always calculated from ALL records — never affected by filters or pagination + $allShifts = Shift::where(function ($q) { + if (Auth::user()->can('manage-any-shifts')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-shifts')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + $stats = [ + 'total' => (clone $allShifts)->count(), + 'active' => (clone $allShifts)->where('status', 'active')->count(), + 'night' => (clone $allShifts)->where('is_night_shift', true)->count(), + 'day' => (clone $allShifts)->where('is_night_shift', false)->count(), + ]; + + return Inertia::render('hr/shifts/index', [ + 'shifts' => $shifts, + 'stats' => $stats, + 'filters' => $request->all(['search', 'status', 'shift_type', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'start_time' => 'required|date_format:H:i', + 'end_time' => 'required|date_format:H:i', + 'break_duration' => 'required|integer|min:0', + 'break_start_time' => 'nullable|date_format:H:i', + 'break_end_time' => 'nullable|date_format:H:i', + 'grace_period' => 'required|integer|min:0', + 'is_night_shift' => 'boolean', + 'status' => 'nullable|in:active,inactive', + ]); + + $validated['created_by'] = creatorId(); + $validated['status'] = $validated['status'] ?? 'active'; + $validated['is_night_shift'] = $validated['is_night_shift'] ?? false; + + // Check if shift with same name already exists + $exists = Shift::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Shift with this name already exists.')); + } + + Shift::create($validated); + + return redirect()->back()->with('success', __('Shift created successfully.')); + } + + public function update(Request $request, $shiftId) + { + $shift = Shift::where('id', $shiftId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($shift) { + try { + $validated = $request->validate([ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'start_time' => 'required|date_format:H:i', + 'end_time' => 'required|date_format:H:i', + 'break_duration' => 'required|integer|min:0', + 'break_start_time' => 'nullable|date_format:H:i', + 'break_end_time' => 'nullable|date_format:H:i', + 'grace_period' => 'required|integer|min:0', + 'is_night_shift' => 'boolean', + 'status' => 'nullable|in:active,inactive', + ]); + + // Check if shift with same name already exists (excluding current) + $exists = Shift::where('name', $validated['name']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('id', '!=', $shiftId) + ->exists(); + + if ($exists) { + return redirect()->back()->with('error', __('Shift with this name already exists.')); + } + + $shift->update($validated); + + return redirect()->back()->with('success', __('Shift updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update shift')); + } + } else { + return redirect()->back()->with('error', __('Shift Not Found.')); + } + } + + public function destroy($shiftId) + { + $shift = Shift::where('id', $shiftId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($shift) { + try { + $shift->delete(); + return redirect()->back()->with('success', __('Shift deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete shift')); + } + } else { + return redirect()->back()->with('error', __('Shift Not Found.')); + } + } + + public function toggleStatus($shiftId) + { + $shift = Shift::where('id', $shiftId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($shift) { + try { + $shift->status = $shift->status === 'active' ? 'inactive' : 'active'; + $shift->save(); + + return redirect()->back()->with('success', __('Shift status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update shift status')); + } + } else { + return redirect()->back()->with('error', __('Shift Not Found.')); + } + } +} diff --git a/app/Http/Controllers/SkrillPaymentController.php b/app/Http/Controllers/SkrillPaymentController.php new file mode 100644 index 000000000..968c90df5 --- /dev/null +++ b/app/Http/Controllers/SkrillPaymentController.php @@ -0,0 +1,81 @@ + 'required|string', + 'email' => 'required|email', + ]); + + try { + $settings = getPaymentMethodConfig('skrill'); + + createPlanOrder([ + 'user_id' => auth()->id(), + 'plan_id' => $validated['plan_id'], + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'skrill', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['transaction_id'], + 'status' => 'pending' + ]); + + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + + $paymentData = [ + 'pay_to_email' => $settings['merchant_id'], + 'transaction_id' => $validated['transaction_id'], + 'return_url' => route('plans.index'), + 'cancel_url' => route('plans.index'), + 'status_url' => route('skrill.callback'), + 'language' => 'EN', + 'amount' => $pricing['final_price'], + 'currency' => 'USD', + 'detail1_description' => 'Plan Subscription', + 'detail1_text' => $plan->name, + 'pay_from_email' => $validated['email'] + ]; + + // Create form and auto-submit to Skrill + $form = '
'; + foreach ($paymentData as $key => $value) { + $form .= ''; + } + $form .= '
'; + + return response($form); + } catch (\Exception $e) { + return handlePaymentError($e, 'skrill'); + } + } + + public function callback(Request $request) + { + $transactionId = $request->input('transaction_id'); + $status = $request->input('status'); + + if ($status == '2') { // Payment processed + $planOrder = PlanOrder::where('payment_id', $transactionId)->first(); + + if ($planOrder && $planOrder->status === 'pending') { + $planOrder->update(['status' => 'approved']); + $planOrder->activateSubscription(); + } + } + + return response('OK', 200); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/StripePaymentController.php b/app/Http/Controllers/StripePaymentController.php new file mode 100644 index 000000000..1433ab4e7 --- /dev/null +++ b/app/Http/Controllers/StripePaymentController.php @@ -0,0 +1,80 @@ + 'required|string', + 'cardholder_name' => 'required|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null, $validated['billing_cycle']); + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['stripe_secret']) || !isset($settings['payment_settings']['stripe_key'])) { + return back()->withErrors(['error' => __('Stripe not configured')]); + } + + $stripeSecret = $settings['payment_settings']['stripe_secret']; + if (!str_starts_with($stripeSecret, 'sk_')) { + return back()->withErrors(['error' => __('Invalid Stripe secret key format')]); + } + + + Stripe::setApiKey($stripeSecret); + + $user = auth()->user(); + $paymentIntent = PaymentIntent::create([ + 'amount' => $pricing['final_price'] * 100, + 'currency' => $settings['general_settings']['defaultCurrency'] ?? 'usd', + 'payment_method' => $validated['payment_method_id'], + 'confirmation_method' => 'manual', + 'confirm' => true, + 'return_url' => route('plans.index'), + 'description' => 'Subscription to ' . $plan->name . ' plan - ' . ucfirst($validated['billing_cycle']) . ' billing', + 'shipping' => [ + 'name' => $validated['cardholder_name'], + 'address' => [ + 'line1' => $user->address ?? 'Not provided', + 'city' => $user->city ?? 'Not provided', + 'state' => $user->state ?? 'Not provided', + 'postal_code' => $user->postal_code ?? '000000', + 'country' => $user->country ?? 'IN', + ], + ], + ]); + + if ($paymentIntent->status === 'succeeded') { + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $plan->id, + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'stripe', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $paymentIntent->id, + ]); + + return back()->with('success', __('Payment successful and plan activated')); + } + + return back()->withErrors(['error' => __('Payment failed')]); + } catch (\Exception $e) { + return handlePaymentError($e, 'stripe'); + } + } +} diff --git a/app/Http/Controllers/TapPaymentController.php b/app/Http/Controllers/TapPaymentController.php new file mode 100644 index 000000000..6d7b7e573 --- /dev/null +++ b/app/Http/Controllers/TapPaymentController.php @@ -0,0 +1,134 @@ +json(['error' => __('Tap not configured')], 400); + } + + $user = auth()->user(); + $transactionId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + + // Initialize Tap Payment library + require_once app_path('Libraries/Tap/Tap.php'); + require_once app_path('Libraries/Tap/Reference.php'); + require_once app_path('Libraries/Tap/Payment.php'); + $tap = new \App\Package\Payment([ + 'company_tap_secret_key' => $settings['payment_settings']['tap_secret_key'] + ]); + + $chargeData = [ + 'amount' => $pricing['final_price'], + 'currency' => 'USD', + 'threeDSecure' => 'true', + 'description' => 'Plan: ' . $plan->name, + 'statement_descriptor' => 'Plan Subscription', + 'customer' => [ + 'first_name' => $user->name ?? 'Customer', + 'email' => $user->email, + ], + 'source' => ['id' => 'src_card'], + 'post' => ['url' => route('tap.callback')], + 'redirect' => ['url' => route('tap.success', [ + 'plan_id' => $plan->id, + 'user_id' => $user->id, + 'billing_cycle' => $validated['billing_cycle'], + 'coupon_code' => $validated['coupon_code'] ?? '' + ])] + ]; + + return $tap->charge($chargeData, true); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + try { + $chargeId = $request->input('tap_id'); + $planId = $request->input('plan_id'); + $userId = $request->input('user_id'); + $billingCycle = $request->input('billing_cycle', 'monthly'); + $couponCode = $request->input('coupon_code'); + + if ($chargeId && $planId && $userId) { + $plan = Plan::find($planId); + $user = User::find($userId); + + if ($plan && $user) { + // Verify payment status with Tap API + $settings = getPaymentGatewaySettings(); + + if (!isset($settings['payment_settings']['tap_secret_key'])) { + return redirect()->route('plans.index')->with('error', __('Tap not configured')); + } + + // Initialize Tap Payment library + require_once app_path('Libraries/Tap/Tap.php'); + require_once app_path('Libraries/Tap/Reference.php'); + require_once app_path('Libraries/Tap/Payment.php'); + $tap = new \App\Package\Payment([ + 'company_tap_secret_key' => $settings['payment_settings']['tap_secret_key'] + ]); + + // Get charge details from Tap API + $chargeDetails = $tap->getCharge($chargeId); + + if ($chargeDetails && isset($chargeDetails->status) && $chargeDetails->status === 'CAPTURED') { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $billingCycle, + 'payment_method' => 'tap', + 'coupon_code' => $couponCode, + 'payment_id' => $chargeId, + ]); + + // Log the user in if not already authenticated + if (!auth()->check()) { + auth()->login($user); + } + + return redirect()->route('plans.index')->with('success', __('Payment completed successfully and plan activated')); + } else { + return redirect()->route('plans.index')->with('error', __('Payment not captured or failed')); + } + } + } + + return redirect()->route('plans.index')->with('error', __('Payment verification failed')); + + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment processing failed')); + } + } + + public function callback(Request $request) + { + try { + $chargeId = $request->input('tap_id'); + $status = $request->input('status'); + return response('OK', 200); + + } catch (\Exception $e) { + return response('Error', 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/TerminationController.php b/app/Http/Controllers/TerminationController.php new file mode 100644 index 000000000..31b402a44 --- /dev/null +++ b/app/Http/Controllers/TerminationController.php @@ -0,0 +1,386 @@ +can('manage-terminations')) { + $query = Termination::with(['employee:id,name,email,avatar', 'approver'])->where(function ($q) { + if (Auth::user()->can('manage-any-terminations')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-terminations')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }) + ->orWhere('termination_type', 'like', '%' . $request->search . '%') + ->orWhere('reason', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle termination type filter + if ($request->has('termination_type') && !empty($request->termination_type)) { + $query->where('termination_type', $request->termination_type); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('termination_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('termination_date', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['termination_date', 'notice_date', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $terminations = $query->paginate($request->per_page ?? 10); + + $terminations->getCollection()->transform(function ($termination) { + if ($termination->employee) { + $rawAvatar = $termination->employee->getRawOriginal('avatar'); + $termination->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $termination; + }); + + // Get employees for filter dropdown + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + + // Get termination types for filter dropdown + $terminationTypes = Termination::whereIn('created_by', getCompanyAndUsersId()) + ->select('termination_type') + ->distinct() + ->pluck('termination_type') + ->toArray(); + + return Inertia::render('hr/terminations/index', [ + 'terminations' => $terminations, + 'employees' => $this->getFilteredEmployees(), + 'terminationTypes' => $terminationTypes, + 'filters' => $request->all(['search', 'employee_id', 'termination_type', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-terminations') && !Auth::user()->can('manage-any-terminations')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + return $employees; + } + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-terminations')) { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'termination_type' => 'required|string|max:255', + 'termination_date' => 'required|date', + 'notice_date' => 'required|date|before_or_equal:termination_date', + 'notice_period' => 'nullable|string|max:255', + 'reason' => 'nullable|string|max:255', + 'description' => 'nullable|string', + 'documents' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + $terminationData = [ + 'employee_id' => $request->employee_id, + 'termination_type' => $request->termination_type, + 'termination_date' => $request->termination_date, + 'notice_date' => $request->notice_date, + 'notice_period' => $request->notice_period, + 'reason' => $request->reason, + 'description' => $request->description, + 'status' => 'planned', + 'created_by' => creatorId(), + ]; + + // Handle document from media library + if ($request->has('documents')) { + $terminationData['documents'] = $request->documents; + } + + Termination::create($terminationData); + + return redirect()->back()->with('success', __('Termination created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Termination $termination) + { + if (Auth::user()->can('edit-terminations')) { + // Check if termination belongs to current company + if (!in_array($termination->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this termination')); + } + + // Convert checkbox values to proper booleans before validation + if ($request->has('exit_interview_conducted')) { + $request->merge([ + 'exit_interview_conducted' => $request->exit_interview_conducted === 'true' || + $request->exit_interview_conducted === '1' || + $request->exit_interview_conducted === 1 || + $request->exit_interview_conducted === true + ]); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'termination_type' => 'required|string|max:255', + 'termination_date' => 'required|date', + 'notice_date' => 'required|date|before_or_equal:termination_date', + 'notice_period' => 'nullable|string|max:255', + 'reason' => 'nullable|string|max:255', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:planned,in progress,completed', + 'documents' => 'nullable|string', + 'exit_feedback' => 'nullable|string', + 'exit_interview_conducted' => 'nullable|boolean', + 'exit_interview_date' => 'nullable|date|required_if:exit_interview_conducted,true', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $employee = User::find($request->employee_id); + if (!$employee || !in_array($employee->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid employee selected'); + } + + $terminationData = [ + 'employee_id' => $request->employee_id, + 'termination_type' => $request->termination_type, + 'termination_date' => $request->termination_date, + 'notice_date' => $request->notice_date, + 'notice_period' => $request->notice_period, + 'reason' => $request->reason, + 'description' => $request->description, + 'exit_feedback' => $request->exit_feedback, + 'exit_interview_conducted' => $request->exit_interview_conducted ?? false, + 'exit_interview_date' => $request->exit_interview_date, + ]; + + // Update status if provided and different from current + if ($request->has('status') && $request->status !== $termination->status) { + $terminationData['status'] = $request->status; + + // If status is being set to in progress or completed, set approved_by and approved_at + if (in_array($request->status, ['in progress', 'completed']) && !$termination->approved_by) { + $terminationData['approved_by'] = auth()->id(); + $terminationData['approved_at'] = now(); + } + } + + // Handle document from media library + if ($request->has('documents')) { + $terminationData['documents'] = $request->documents; + } + + $termination->update($terminationData); + + return redirect()->back()->with('success', __('Termination updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Termination $termination) + { + if (Auth::user()->can('delete-terminations')) { + // Check if termination belongs to current company + if (!in_array($termination->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this termination')); + } + $termination->delete(); + + return redirect()->route('hr.terminations.index')->with('success', __('Termination deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Change the status of the termination. + */ + public function changeStatus(Request $request, Termination $termination) + { + if (Auth::user()->can('approve-terminations') || Auth::user()->can('reject-terminations') || Auth::user()->can('edit-terminations')) { + // Check if termination belongs to current company + if (!in_array($termination->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this termination')); + } + + // Convert checkbox values to proper booleans before validation + if ($request->has('exit_interview_conducted')) { + $request->merge([ + 'exit_interview_conducted' => $request->exit_interview_conducted === 'true' || + $request->exit_interview_conducted === '1' || + $request->exit_interview_conducted === 1 || + $request->exit_interview_conducted === true + ]); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:planned,in progress,completed', + 'exit_feedback' => 'nullable|string|required_if:status,completed', + 'exit_interview_conducted' => 'nullable|boolean', + 'exit_interview_date' => 'nullable|date|required_if:exit_interview_conducted,true', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $updateData = [ + 'status' => $request->status, + ]; + + // If status is being set to in progress or completed, set approved_by and approved_at + if (in_array($request->status, ['in progress', 'completed']) && !$termination->approved_by) { + $updateData['approved_by'] = auth()->id(); + $updateData['approved_at'] = now(); + } + + // If status is completed, update exit interview details + if ($request->status === 'completed') { + $updateData['exit_feedback'] = $request->exit_feedback; + $updateData['exit_interview_conducted'] = $request->exit_interview_conducted ?? false; + $updateData['exit_interview_date'] = $request->exit_interview_date; + } + + $termination->update($updateData); + + return redirect()->back()->with('success', __('Termination status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Download document file. + */ + public function downloadDocument(Termination $termination) + { + if (Auth::user()->can('view-terminations')) { + // Check if termination belongs to current company + if (!in_array($termination->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this document')); + } + + if (!$termination->documents) { + return redirect()->back()->with('error', __('Document file not found')); + } + + $filePath = getStorageFilePath($termination->documents); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Document file not found')); + } + + return response()->download($filePath); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/TimeEntryController.php b/app/Http/Controllers/TimeEntryController.php new file mode 100644 index 000000000..157805e95 --- /dev/null +++ b/app/Http/Controllers/TimeEntryController.php @@ -0,0 +1,428 @@ +can('manage-time-entries')) { + $query = TimeEntry::with(['employee', 'approver', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-time-entries')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-time-entries')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && ! empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('description', 'like', '%'.$request->search.'%') + ->orWhere('project', 'like', '%'.$request->search.'%') + ->orWhereHas('employee', function ($subQ) use ($request) { + $subQ->where('name', 'like', '%'.$request->search.'%'); + }); + }); + } + + // Handle employee filter + if ($request->has('employee_id') && ! empty($request->employee_id) && $request->employee_id !== 'all') { + $query->where('employee_id', $request->employee_id); + } + + // Handle status filter + if ($request->has('status') && ! empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle project filter + if ($request->has('project') && ! empty($request->project) && $request->project !== 'all') { + $query->where('project', $request->project); + } + + // Handle date range filter + if ($request->has('date_from') && ! empty($request->date_from)) { + $query->where('date', '>=', $request->date_from); + } + if ($request->has('date_to') && ! empty($request->date_to)) { + $query->where('date', '<=', $request->date_to); + } + + // Handle sorting + if ($request->has('sort_field') && ! empty($request->sort_field)) { + $sortField = $request->sort_field; + $sortDirection = $request->sort_direction ?? 'asc'; + + if ($sortField === 'created_at') { + $query->orderBy('created_at', $sortDirection); + } else { + $query->orderBy('date', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + $timeEntries = $query->paginate($request->per_page ?? 10); + + // Avatar transform + $timeEntries->getCollection()->transform(function ($entry) { + if ($entry->employee) { + $rawAvatar = $entry->employee->getRawOriginal('avatar'); + $entry->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $entry; + }); + + // Get employees for filter dropdown + $employees = User::where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(['id', 'name']); + + // Get unique projects for filter dropdown + $projects = TimeEntry::whereIn('created_by', getCompanyAndUsersId()) + ->whereNotNull('project') + ->distinct() + ->pluck('project'); + + return Inertia::render('hr/time-entries/index', [ + 'timeEntries' => $timeEntries, + 'employees' => $this->getFilteredEmployees(), + 'projects' => $projects, + 'hasSampleFile' => file_exists(storage_path('uploads/sample/sample-time-entry.xlsx')), + 'filters' => $request->all(['search', 'employee_id', 'status', 'project', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-time-entries') && ! Auth::user()->can('manage-any-time-entries')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '', + ]; + }); + + return $employees; + } + + public function store(Request $request) + { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'date' => 'required|date', + 'hours' => 'required|numeric|min:0.5|max:24', + 'description' => 'required|string', + 'project' => 'nullable|string|max:255', + ]); + + $validated['created_by'] = creatorId(); + + TimeEntry::create($validated); + + return redirect()->back()->with('success', __('Time entry created successfully.')); + } + + public function update(Request $request, $timeEntryId) + { + $timeEntry = TimeEntry::where('id', $timeEntryId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($timeEntry) { + try { + $validated = $request->validate([ + 'employee_id' => 'required|exists:users,id', + 'date' => 'required|date', + 'hours' => 'required|numeric|min:0.5|max:24', + 'description' => 'required|string', + 'project' => 'nullable|string|max:255', + ]); + + // Only allow updates if status is pending + if ($timeEntry->status !== 'pending') { + return redirect()->back()->with('error', __('Cannot update processed time entry.')); + } + + $timeEntry->update($validated); + + return redirect()->back()->with('success', __('Time entry updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update time entry')); + } + } else { + return redirect()->back()->with('error', __('Time entry Not Found.')); + } + } + + public function destroy($timeEntryId) + { + $timeEntry = TimeEntry::where('id', $timeEntryId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($timeEntry) { + try { + // Only allow deletion if status is pending + if ($timeEntry->status !== 'pending') { + return redirect()->back()->with('error', __('Cannot delete processed time entry.')); + } + + $timeEntry->delete(); + + return redirect()->back()->with('success', __('Time entry deleted successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to delete time entry')); + } + } else { + return redirect()->back()->with('error', __('Time entry Not Found.')); + } + } + + public function updateStatus(Request $request, $timeEntryId) + { + $validated = $request->validate([ + 'status' => 'required|in:approved,rejected', + 'manager_comments' => 'nullable|string', + ]); + + $timeEntry = TimeEntry::where('id', $timeEntryId) + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + + if ($timeEntry) { + try { + $timeEntry->update([ + 'status' => $validated['status'], + 'manager_comments' => $validated['manager_comments'], + 'approved_by' => Auth::id(), + 'approved_at' => now(), + ]); + + return redirect()->back()->with('success', __('Time entry status updated successfully')); + } catch (\Exception $e) { + return redirect()->back()->with('error', $e->getMessage() ?: __('Failed to update time entry status')); + } + } else { + return redirect()->back()->with('error', __('Time entry Not Found.')); + } + } + + public function export() + { + if (Auth::user()->can('export-time-entry')) { + try { + $timeEntries = TimeEntry::with(['employee', 'approver']) + ->where(function ($q) { + if (Auth::user()->can('manage-any-time-entries')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-time-entries')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id())->orWhere('approved_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + })->orderBy('date', 'desc')->get(); + + $fileName = 'time_entries_'.date('Y-m-d_His').'.csv'; + $headers = [ + 'Content-Type' => 'text/csv', + 'Content-Disposition' => 'attachment; filename="'.$fileName.'"', + ]; + + $callback = function () use ($timeEntries) { + $file = fopen('php://output', 'w'); + fputcsv($file, [ + 'Employee', + 'Date', + 'Hours', + 'Project', + 'Description', + 'Status', + 'Approved By', + 'Approved At', + 'Submitted On', + ]); + + foreach ($timeEntries as $entry) { + fputcsv($file, [ + $entry->employee->name ?? '', + $entry->date ? date('Y-m-d', strtotime($entry->date)) : '', + $entry->hours ?? '', + $entry->project ?? '', + $entry->description ?? '', + $entry->status ?? '', + $entry->approver->name ?? '', + $entry->approved_at ?? '', + $entry->created_at ?? '', + ]); + } + fclose($file); + }; + + return response()->stream($callback, 200, $headers); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to export time entries: :message', ['message' => $e->getMessage()])], 500); + } + } else { + return response()->json(['message' => __('Permission Denied.')], 403); + } + } + + public function downloadTemplate() + { + $filePath = storage_path('uploads/sample/sample-time-entry.xlsx'); + if (! file_exists($filePath)) { + return response()->json(['error' => __('Template file not available')], 404); + } + + return response()->download($filePath, 'sample-time-entry.xlsx'); + } + + public function parseFile(Request $request) + { + if (Auth::user()->can('import-time-entry')) { + $rules = ['file' => 'required|mimes:csv,txt,xlsx,xls']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return response()->json(['message' => $validator->getMessageBag()->first()]); + } + + try { + $file = $request->file('file'); + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file->getRealPath()); + $worksheet = $spreadsheet->getActiveSheet(); + $highestColumn = $worksheet->getHighestColumn(); + $highestRow = $worksheet->getHighestRow(); + $headers = []; + + for ($col = 'A'; $col <= $highestColumn; $col++) { + $value = $worksheet->getCell($col.'1')->getValue(); + if ($value) { + $headers[] = (string) $value; + } + } + + $previewData = []; + for ($row = 2; $row <= $highestRow; $row++) { + $rowData = []; + $colIndex = 0; + for ($col = 'A'; $col <= $highestColumn; $col++) { + if ($colIndex < count($headers)) { + $rowData[$headers[$colIndex]] = (string) $worksheet->getCell($col.$row)->getValue(); + } + $colIndex++; + } + $previewData[] = $rowData; + } + + return response()->json(['excelColumns' => $headers, 'previewData' => $previewData]); + } catch (\Exception $e) { + return response()->json(['message' => __('Failed to parse file: :error', ['error' => $e->getMessage()])]); + } + } else { + return response()->json(['message' => __('Permission denied.')], 403); + } + } + + public function fileImport(Request $request) + { + if (Auth::user()->can('import-time-entry')) { + $rules = ['data' => 'required|array']; + $validator = Validator::make($request->all(), $rules); + + if ($validator->fails()) { + return redirect()->back()->with('error', $validator->getMessageBag()->first()); + } + + try { + $data = $request->data; + $imported = 0; + $skipped = 0; + + foreach ($data as $row) { + try { + if (empty($row['employee']) || empty($row['date']) || empty($row['hours'])) { + $skipped++; + + continue; + } + + $employee = User::where('name', $row['employee']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('type', 'employee') + ->first(); + + if (! $employee) { + $skipped++; + + continue; + } + + // Check if time entry already exists for this employee and date + $exists = TimeEntry::where('employee_id', $employee->id) + ->whereDate('date', $row['date']) + ->exists(); + + if ($exists) { + $skipped++; + continue; + } + + TimeEntry::create([ + 'employee_id' => $employee->id, + 'date' => $row['date'], + 'hours' => $row['hours'], + 'project' => $row['project'] ?? null, + 'description' => $row['description'] ?? '', + 'status' => 'pending', + 'created_by' => creatorId(), + ]); + + $imported++; + } catch (\Exception $e) { + $skipped++; + } + } + + return redirect()->back()->with('success', __('Import completed: :added time entries added, :skipped time entries skipped', ['added' => $imported, 'skipped' => $skipped])); + } catch (\Exception $e) { + return redirect()->back()->with('error', __('Failed to import: :error', ['error' => $e->getMessage()])); + } + } else { + return redirect()->back()->with('error', __('Permission denied.')); + } + } +} diff --git a/app/Http/Controllers/ToyyibPayPaymentController.php b/app/Http/Controllers/ToyyibPayPaymentController.php new file mode 100644 index 000000000..3d7426661 --- /dev/null +++ b/app/Http/Controllers/ToyyibPayPaymentController.php @@ -0,0 +1,180 @@ +secretKey = $settings['secret_key'] ?? ''; + $this->categoryCode = $settings['category_code'] ?? ''; + } + + public function processPayment(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'billName' => 'required|string', + 'billAmount' => 'required|numeric|min:0.01', + 'billTo' => 'required|string', + 'billEmail' => 'required|email', + 'billPhone' => 'required|string', + 'billDescription' => 'nullable|string', + ]); + + try { + $plan = Plan::findOrFail($validated['plan_id']); + $user = Auth::user(); + + if (!$this->secretKey || !$this->categoryCode) { + return back()->withErrors(['error' => __('ToyyibPay payment gateway not configured properly')]); + } + + // Calculate final amount with coupon + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + $finalAmount = $pricing['final_price']; + + // Set callback and return URLs + $this->callBackUrl = route('toyyibpay.callback'); + $this->returnUrl = route('toyyibpay.success'); + + // Generate unique payment reference + $paymentId = 'toyyib_' . $plan->id . '_' . time() . '_' . uniqid(); + + // Create plan order before payment + createPlanOrder([ + 'user_id' => $user->id, + 'plan_id' => $validated['plan_id'], + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'toyyibpay', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $paymentId, + 'status' => 'pending' + ]); + + // Format phone number for Malaysian format + $phone = preg_replace('/[^0-9]/', '', $validated['billPhone']); + if (!str_starts_with($phone, '60')) { + $phone = '60' . ltrim($phone, '0'); + } + + // Prepare bill data + $billData = [ + 'userSecretKey' => $this->secretKey, + 'categoryCode' => $this->categoryCode, + 'billName' => $validated['billName'], + 'billDescription' => $validated['billDescription'] ?? $plan->description ?? $plan->name, + 'billPriceSetting' => 1, + 'billPayorInfo' => 1, + 'billAmount' => intval($finalAmount * 100), // Convert to cents + 'billReturnUrl' => $this->returnUrl, + 'billCallbackUrl' => $this->callBackUrl, + 'billExternalReferenceNo' => $paymentId, + 'billTo' => $validated['billTo'], + 'billEmail' => $validated['billEmail'], + 'billPhone' => $phone, + 'billSplitPayment' => 0, + 'billSplitPaymentArgs' => '', + 'billPaymentChannel' => '0', + 'billContentEmail' => 'Thank you for your subscription!', + 'billChargeToCustomer' => 1, + 'billExpiryDate' => date('d-m-Y', strtotime('+3 days')), + 'billExpiryDays' => 3 + ]; + + // Make API call to ToyyibPay + $curl = curl_init(); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_URL, 'https://toyyibpay.com/index.php/api/createBill'); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curl, CURLOPT_POSTFIELDS, $billData); + curl_setopt($curl, CURLOPT_TIMEOUT, 30); + + $result = curl_exec($curl); + $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $curlError = curl_error($curl); + curl_close($curl); + + if ($curlError) { + throw new \Exception('cURL Error: ' . $curlError); + } + + if ($httpCode !== 200) { + throw new \Exception('HTTP Error: ' . $httpCode); + } + + // Handle response + if (str_contains($result, 'KEY-DID-NOT-EXIST-OR-USER-IS-NOT-ACTIVE')) { + throw new \Exception(__('Invalid ToyyibPay credentials or inactive account')); + } + + $responseData = json_decode($result, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + throw new \Exception(__('Invalid JSON response from ToyyibPay')); + } + + if (isset($responseData[0]['BillCode'])) { + $redirectUrl = 'https://toyyibpay.com/' . $responseData[0]['BillCode']; + Log::info('Redirecting to ToyyibPay', ['url' => $redirectUrl]); + return redirect()->away($redirectUrl); + } else { + $errorMsg = $responseData[0]['msg'] ?? __('Failed to create payment bill'); + throw new \Exception($errorMsg); + } + + } catch (\Exception $e) { + return handlePaymentError($e, 'ToyyibPay'); + } + } + + public function callback(Request $request) + { + try { + $billcode = $request->input('billcode'); + $status_id = $request->input('status_id'); + $order_id = $request->input('order_id'); + $transaction_id = $request->input('transaction_id'); + + if ($status_id == '1') { // Payment successful + $planOrder = \App\Models\PlanOrder::where('payment_id', $order_id)->first(); + + if ($planOrder && $planOrder->status === 'pending') { + processPaymentSuccess([ + 'user_id' => $planOrder->user_id, + 'plan_id' => $planOrder->plan_id, + 'billing_cycle' => $planOrder->billing_cycle, + 'payment_method' => 'toyyibpay', + 'coupon_code' => $planOrder->coupon_code, + 'payment_id' => $order_id, + ]); + } + } + return response('OK', 200); + } catch (\Exception $e) { + return response('ERROR', 500); + } + } + + public function success(Request $request) + { + $status_id = $request->input('status_id'); + $order_id = $request->input('order_id'); + + if ($status_id == '1') { + return redirect()->route('plans.index')->with('success', __('Payment completed successfully!')); + } else { + return redirect()->route('plans.index')->with('error', __('Payment was not completed. Please try again.')); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/TrainingAssessmentController.php b/app/Http/Controllers/TrainingAssessmentController.php new file mode 100644 index 000000000..f876a19ca --- /dev/null +++ b/app/Http/Controllers/TrainingAssessmentController.php @@ -0,0 +1,204 @@ +whereHas('trainingProgram', function ($q) { + $q->where('created_by', createdBy()); + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%') + ->orWhereHas('trainingProgram', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle program filter + if ($request->has('training_program_id') && !empty($request->training_program_id)) { + $query->where('training_program_id', $request->training_program_id); + } + + // Handle type filter + if ($request->has('type') && !empty($request->type)) { + $query->where('type', $request->type); + } + + // Handle sorting + if ($request->has('sort_field') && !empty($request->sort_field)) { + if ($request->sort_field === 'program_name') { + $query->join('training_programs', 'training_assessments.training_program_id', '=', 'training_programs.id') + ->select('training_assessments.*') + ->orderBy('training_programs.name', $request->sort_direction ?? 'asc'); + } else { + $query->orderBy($request->sort_field, $request->sort_direction ?? 'asc'); + } + } else { + $query->orderBy('name', 'asc'); + } + + // Add employee results count + $query->withCount(['employeeResults']); + + $trainingAssessments = $query->paginate($request->per_page ?? 10); + + // Get training programs for filter dropdown + $trainingPrograms = TrainingProgram::where('created_by', createdBy()) + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/training/assessments/index', [ + 'trainingAssessments' => $trainingAssessments, + 'trainingPrograms' => $trainingPrograms, + 'filters' => $request->all(['search', 'training_program_id', 'type', 'sort_field', 'sort_direction', 'per_page']), + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'training_program_id' => 'required|exists:training_programs,id', + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'type' => 'required|string|in:quiz,practical,presentation', + 'passing_score' => 'required|numeric|min:0|max:100', + 'criteria' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if training program belongs to current company + $trainingProgram = TrainingProgram::find($request->training_program_id); + if (!$trainingProgram || $trainingProgram->created_by != createdBy()) { + return redirect()->back()->with('error', 'Invalid training program selected'); + } + + TrainingAssessment::create([ + 'training_program_id' => $request->training_program_id, + 'name' => $request->name, + 'description' => $request->description, + 'type' => $request->type, + 'passing_score' => $request->passing_score, + 'criteria' => $request->criteria, + 'created_by' => createdBy(), + ]); + + return redirect()->back()->with('success', 'Training assessment created successfully'); + } + + /** + * Display the specified resource. + */ + public function show(TrainingAssessment $trainingAssessment) + { + // Check if training assessment belongs to current company + if ($trainingAssessment->trainingProgram->created_by != createdBy()) { + return redirect()->back()->with('error', 'You do not have permission to view this training assessment'); + } + + // Load relationships + $trainingAssessment->load(['trainingProgram', 'employeeResults.employeeTraining.employee']); + + // Calculate statistics + $totalResults = $trainingAssessment->employeeResults->count(); + $passedResults = $trainingAssessment->employeeResults->where('is_passed', true)->count(); + $failedResults = $totalResults - $passedResults; + $passRate = $totalResults > 0 ? ($passedResults / $totalResults) * 100 : 0; + $averageScore = $totalResults > 0 ? $trainingAssessment->employeeResults->avg('score') : 0; + + return Inertia::render('hr/training/assessments/show', [ + 'trainingAssessment' => $trainingAssessment, + 'statistics' => [ + 'totalResults' => $totalResults, + 'passedResults' => $passedResults, + 'failedResults' => $failedResults, + 'passRate' => $passRate, + 'averageScore' => $averageScore, + ], + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, TrainingAssessment $trainingAssessment) + { + // Check if training assessment belongs to current company + if ($trainingAssessment->trainingProgram->created_by != createdBy()) { + return redirect()->back()->with('error', 'You do not have permission to update this training assessment'); + } + + $validator = Validator::make($request->all(), [ + 'training_program_id' => 'required|exists:training_programs,id', + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'type' => 'required|string|in:quiz,practical,presentation', + 'passing_score' => 'required|numeric|min:0|max:100', + 'criteria' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if training program belongs to current company + $trainingProgram = TrainingProgram::find($request->training_program_id); + if (!$trainingProgram || $trainingProgram->created_by != createdBy()) { + return redirect()->back()->with('error', 'Invalid training program selected'); + } + + $trainingAssessment->update([ + 'training_program_id' => $request->training_program_id, + 'name' => $request->name, + 'description' => $request->description, + 'type' => $request->type, + 'passing_score' => $request->passing_score, + 'criteria' => $request->criteria, + ]); + + return redirect()->back()->with('success', 'Training assessment updated successfully'); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(TrainingAssessment $trainingAssessment) + { + // Check if training assessment belongs to current company + if ($trainingAssessment->trainingProgram->created_by != createdBy()) { + return redirect()->back()->with('error', 'You do not have permission to delete this training assessment'); + } + + // Check if assessment has results + if ($trainingAssessment->employeeResults()->count() > 0) { + return redirect()->back()->with('error', 'Cannot delete assessment that has employee results'); + } + + // Delete the training assessment + $trainingAssessment->delete(); + + return redirect()->back()->with('success', 'Training assessment deleted successfully'); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/TrainingProgramController.php b/app/Http/Controllers/TrainingProgramController.php new file mode 100644 index 000000000..c23fca08a --- /dev/null +++ b/app/Http/Controllers/TrainingProgramController.php @@ -0,0 +1,287 @@ +can('manage-training-programs')) { + $query = TrainingProgram::with(['trainingType'])->where(function ($q) { + if (Auth::user()->can('manage-any-training-programs')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-training-programs')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle training type filter + if ($request->has('training_type_id') && !empty($request->training_type_id)) { + $query->where('training_type_id', $request->training_type_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status)) { + $query->where('status', $request->status); + } + + // Handle mandatory filter + if ($request->has('is_mandatory') && $request->is_mandatory === 'true') { + $query->where('is_mandatory', true); + } + + // Handle self-enrollment filter + if ($request->has('is_self_enrollment') && $request->is_self_enrollment === 'true') { + $query->where('is_self_enrollment', true); + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'status', 'duration', 'cost', 'capacity', 'is_mandatory', 'is_self_enrollment', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field) && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + + // Add counts + $query->withCount(['sessions', 'employeeTrainings']); + + $trainingPrograms = $query->paginate($request->per_page ?? 10); + + // Get training types for filter dropdown with branch and departments + $trainingTypes = TrainingType::with(['branch', 'departments']) + ->whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name', 'branch_id') + ->get(); + + return Inertia::render('hr/training/programs/index', [ + 'trainingPrograms' => $trainingPrograms, + 'trainingTypes' => $trainingTypes, + 'filters' => $request->all(['search', 'training_type_id', 'status', 'is_mandatory', 'is_self_enrollment', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'training_type_id' => 'required|exists:training_types,id', + 'description' => 'nullable|string', + 'duration' => 'nullable|integer|min:1', + 'cost' => 'nullable|numeric|min:0', + 'capacity' => 'nullable|integer|min:1', + 'status' => 'required|string|in:draft,active,completed,cancelled', + 'materials' => 'nullable|string', + 'prerequisites' => 'nullable|string', + 'is_mandatory' => 'nullable|boolean', + 'is_self_enrollment' => 'nullable|boolean', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if training type belongs to current company + $trainingType = TrainingType::find($request->training_type_id); + if (!$trainingType || $trainingType->created_by != createdBy()) { + return redirect()->back()->with('error', 'Invalid training type selected'); + } + + $programData = [ + 'name' => $request->name, + 'training_type_id' => $request->training_type_id, + 'description' => $request->description, + 'duration' => $request->duration, + 'cost' => $request->cost, + 'capacity' => $request->capacity, + 'status' => $request->status, + 'prerequisites' => $request->prerequisites, + 'is_mandatory' => $request->is_mandatory ?? false, + 'is_self_enrollment' => $request->is_self_enrollment ?? false, + 'created_by' => creatorId(), + ]; + + // Handle materials from media library + if ($request->has('materials')) { + $programData['materials'] = $request->materials; + } + + TrainingProgram::create($programData); + + return redirect()->back()->with('success', __('Training program created successfully')); + } + + /** + * Display the specified resource. + */ + public function show(TrainingProgram $trainingProgram) + { + // Check if training program belongs to current company + if (!in_array($trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view this training program')); + } + + // Load relationships + $trainingProgram->load(['trainingType', 'sessions', 'employeeTrainings.employee', 'assessments']); + + // Get session statistics + $completedSessions = $trainingProgram->sessions()->where('status', 'completed')->count(); + $totalSessions = $trainingProgram->sessions()->count(); + $sessionCompletionRate = $totalSessions > 0 ? ($completedSessions / $totalSessions) * 100 : 0; + + // Get employee statistics + $completedTrainings = $trainingProgram->employeeTrainings()->where('status', 'completed')->count(); + $totalTrainings = $trainingProgram->employeeTrainings()->count(); + $employeeCompletionRate = $totalTrainings > 0 ? ($completedTrainings / $totalTrainings) * 100 : 0; + + return Inertia::render('hr/training/programs/show', [ + 'trainingProgram' => $trainingProgram, + 'statistics' => [ + 'completedSessions' => $completedSessions, + 'totalSessions' => $totalSessions, + 'sessionCompletionRate' => $sessionCompletionRate, + 'completedTrainings' => $completedTrainings, + 'totalTrainings' => $totalTrainings, + 'employeeCompletionRate' => $employeeCompletionRate, + ], + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, TrainingProgram $trainingProgram) + { + // Check if training program belongs to current company + if (!in_array($trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this training program')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'training_type_id' => 'required|exists:training_types,id', + 'description' => 'nullable|string', + 'duration' => 'nullable|integer|min:1', + 'cost' => 'nullable|numeric|min:0', + 'capacity' => 'nullable|integer|min:1', + 'status' => 'required|string|in:draft,active,completed,cancelled', + 'materials' => 'nullable|string', + 'prerequisites' => 'nullable|string', + 'is_mandatory' => 'nullable|boolean', + 'is_self_enrollment' => 'nullable|boolean', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if training type belongs to current company + $trainingType = TrainingType::find($request->training_type_id); + if (!$trainingType || $trainingType->created_by != createdBy()) { + return redirect()->back()->with('error', 'Invalid training type selected'); + } + + $programData = [ + 'name' => $request->name, + 'training_type_id' => $request->training_type_id, + 'description' => $request->description, + 'duration' => $request->duration, + 'cost' => $request->cost, + 'capacity' => $request->capacity, + 'status' => $request->status, + 'prerequisites' => $request->prerequisites, + 'is_mandatory' => $request->is_mandatory ?? false, + 'is_self_enrollment' => $request->is_self_enrollment ?? false, + ]; + + // Handle materials from media library + if ($request->has('materials')) { + $programData['materials'] = $request->materials; + } + + $trainingProgram->update($programData); + + return redirect()->back()->with('success', __('Training program updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(TrainingProgram $trainingProgram) + { + // Check if training program belongs to current company + if (!in_array($trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this training program')); + } + + // Check if training program has sessions or employee trainings + if ($trainingProgram->sessions()->count() > 0 || $trainingProgram->employeeTrainings()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete training program that has sessions or employee assignments')); + } + + // Delete assessments + $trainingProgram->assessments()->delete(); + + // Delete the training program + $trainingProgram->delete(); + + return redirect()->back()->with('success', __('Training program deleted successfully')); + } + + /** + * Download training materials. + */ + public function downloadMaterials(TrainingProgram $trainingProgram) + { + // Check if training program belongs to current company + if (!in_array($trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access these materials')); + } + + if (!$trainingProgram->materials) { + return redirect()->back()->with('error', __('Training materials not found')); + } + + // Handle cloud storage URLs (already full URLs) + if (filter_var($trainingProgram->materials, FILTER_VALIDATE_URL)) { + return Storage::download($trainingProgram->materials); + } + + // Handle local storage paths + $relativePath = str_replace('/Product/hrmgo-saas-react/storage/', '', $trainingProgram->materials); + + if (!Storage::exists($relativePath)) { + return redirect()->back()->with('error', __('Training materials not found')); + } + + return Storage::download($relativePath); + } +} diff --git a/app/Http/Controllers/TrainingSessionController.php b/app/Http/Controllers/TrainingSessionController.php new file mode 100644 index 000000000..6fc6a9077 --- /dev/null +++ b/app/Http/Controllers/TrainingSessionController.php @@ -0,0 +1,468 @@ +withPermissionCheck(); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('location', 'like', '%' . $request->search . '%') + ->orWhere('notes', 'like', '%' . $request->search . '%') + ->orWhereHas('trainingProgram', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%'); + }); + }); + } + + // Handle program filter + if ($request->has('training_program_id') && !empty($request->training_program_id)) { + $query->where('training_program_id', $request->training_program_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status)) { + $query->where('status', $request->status); + } + + // Handle location type filter + if ($request->has('location_type') && !empty($request->location_type)) { + $query->where('location_type', $request->location_type); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('start_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('start_date', '<=', $request->date_to); + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'start_date', 'end_date', 'status', 'location', 'location_type', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field)) { + $sortField = $request->sort_field === 'date_time' ? 'start_date' : $request->sort_field; + if (in_array($sortField, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($sortField, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + } else { + $query->orderBy('id', 'desc'); + } + + // Add attendance count and trainers count + $query->withCount(['attendance', 'trainers']); + + $trainingSessions = $query->paginate($request->per_page ?? 10); + + // Get training programs for filter dropdown + $trainingPrograms = TrainingProgram::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + // Get employees for trainer dropdown + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => isset($user->employee) ? $user->employee->employee_id : '-' + ]; + }); + + return Inertia::render('hr/training/sessions/index', [ + 'trainingSessions' => $trainingSessions, + 'trainingPrograms' => $trainingPrograms, + 'employees' => $employees, + 'filters' => $request->all(['search', 'training_program_id', 'status', 'location_type', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } + + /** + * Display the calendar view. + */ + public function calendar(Request $request) + { + $query = TrainingSession::with(['trainingProgram']) + ->withPermissionCheck(); + + // Handle program filter + if ($request->has('training_program_id') && !empty($request->training_program_id)) { + $query->where('training_program_id', $request->training_program_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status)) { + $query->where('status', $request->status); + } + + // Get all sessions for calendar + $trainingSessions = $query->get(); + + // Format sessions for calendar + $calendarEvents = $trainingSessions->map(function ($session) { + $statusColors = [ + 'scheduled' => '#3788d8', + 'in_progress' => '#f59e0b', + 'completed' => '#10b77f', + 'cancelled' => '#ef4444', + ]; + + return [ + 'id' => $session->id, + 'title' => $session->name ?? $session->trainingProgram->name, + 'start' => $session->start_date, + 'end' => $session->end_date, + 'backgroundColor' => $statusColors[$session->status] ?? '#6b7280', + 'borderColor' => $statusColors[$session->status] ?? '#6b7280', + 'url' => route('hr.training-sessions.show', $session->id), + 'extendedProps' => [ + 'program' => $session->trainingProgram->name, + 'location' => $session->location, + 'status' => $session->status, + ], + ]; + }); + + // Get training programs for filter dropdown + $trainingPrograms = TrainingProgram::whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get(); + + return Inertia::render('hr/training/sessions/calendar', [ + 'calendarEvents' => $calendarEvents, + 'trainingPrograms' => $trainingPrograms, + 'filters' => $request->all(['training_program_id', 'status']), + ]); + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'training_program_id' => 'required|exists:training_programs,id', + 'name' => 'nullable|string|max:255', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'location' => 'nullable|string|max:255', + 'location_type' => 'required|string|in:physical,virtual', + 'meeting_link' => 'nullable|string|max:255|required_if:location_type,virtual', + 'status' => 'required|string|in:scheduled,in_progress,completed,cancelled', + 'notes' => 'nullable|string', + 'is_recurring' => 'nullable|boolean', + 'recurrence_pattern' => 'nullable|string|in:daily,weekly,monthly|required_if:is_recurring,true', + 'recurrence_count' => 'nullable|integer|min:1|required_if:is_recurring,true', + 'trainer_ids' => 'nullable|array', + 'trainer_ids.*' => 'exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if training program belongs to current company + $trainingProgram = TrainingProgram::find($request->training_program_id); + if (!$trainingProgram || !in_array($trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid training program selected'); + } + + // Check if trainers belong to current company + if (!empty($request->trainer_ids)) { + $trainerIds = $request->trainer_ids; + $validTrainers = User::whereIn('id', $trainerIds) + ->whereIn('created_by', getCompanyAndUsersId()) + ->pluck('id') + ->toArray(); + + if (count($validTrainers) !== count($trainerIds)) { + return redirect()->back()->with('error', 'Invalid trainer selection'); + } + } + + $sessionData = [ + 'training_program_id' => $request->training_program_id, + 'name' => $request->name, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'location' => $request->location, + 'location_type' => $request->location_type, + 'meeting_link' => $request->meeting_link, + 'status' => $request->status, + 'notes' => $request->notes, + 'is_recurring' => $request->is_recurring ?? false, + 'recurrence_pattern' => $request->recurrence_pattern, + 'recurrence_count' => $request->recurrence_count, + 'created_by' => creatorId(), + ]; + + $session = TrainingSession::create($sessionData); + + // Attach trainers if provided + if (!empty($request->trainer_ids)) { + $session->trainers()->attach($request->trainer_ids); + } + + // Create recurring sessions if needed + if ($request->is_recurring && $request->recurrence_count > 0) { + $this->createRecurringSessions($session, $request->trainer_ids ?? []); + } + + return redirect()->back()->with('success', __('Training session created successfully')); + } + + /** + * Display the specified resource. + */ + public function show(TrainingSession $trainingSession) + { + // Check if training session belongs to current company + if (!in_array($trainingSession->trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to view this training session')); + } + + // Load relationships + $trainingSession->load(['trainingProgram', 'trainers', 'attendance.employee']); + + // Process trainer avatars + $trainingSession->trainers->transform(function ($trainer) { + $rawAvatar = $trainer->getRawOriginal('avatar'); + $trainer->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + return $trainer; + }); + + // Format attendance data for all employees with attendance records + $attendanceData = $trainingSession->attendance->map(function ($attendance) { + return [ + 'employee_id' => $attendance->employee_id, + 'name' => $attendance->employee->name, + 'employee_id_display' => $attendance->employee->employee->employee_id ?? '-', + 'is_present' => $attendance->is_present, + 'notes' => $attendance->notes, + ]; + }); + + return Inertia::render('hr/training/sessions/show', [ + 'trainingSession' => $trainingSession, + 'attendanceData' => $attendanceData, + ]); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, TrainingSession $trainingSession) + { + // Check if training session belongs to current company + if (!in_array($trainingSession->trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this training session'); + } + + $validator = Validator::make($request->all(), [ + 'training_program_id' => 'required|exists:training_programs,id', + 'name' => 'nullable|string|max:255', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'location' => 'nullable|string|max:255', + 'location_type' => 'required|string|in:physical,virtual', + 'meeting_link' => 'nullable|string|max:255|required_if:location_type,virtual', + 'status' => 'required|string|in:scheduled,in_progress,completed,cancelled', + 'notes' => 'nullable|string', + 'trainer_ids' => 'nullable|array', + 'trainer_ids.*' => 'exists:users,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if training program belongs to current company + $trainingProgram = TrainingProgram::find($request->training_program_id); + if (!$trainingProgram || !in_array($trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'Invalid training program selected'); + } + + // Check if trainers belong to current company + if (!empty($request->trainer_ids)) { + $trainerIds = $request->trainer_ids; + $validTrainers = User::whereIn('id', $trainerIds) + ->whereIn('created_by', getCompanyAndUsersId()) + ->pluck('id') + ->toArray(); + + if (count($validTrainers) !== count($trainerIds)) { + return redirect()->back()->with('error', 'Invalid trainer selection'); + } + } + + $sessionData = [ + 'training_program_id' => $request->training_program_id, + 'name' => $request->name, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'location' => $request->location, + 'location_type' => $request->location_type, + 'meeting_link' => $request->meeting_link, + 'status' => $request->status, + 'notes' => $request->notes, + ]; + + $trainingSession->update($sessionData); + + // Sync trainers + if (isset($request->trainer_ids)) { + $trainingSession->trainers()->sync($request->trainer_ids); + } else { + $trainingSession->trainers()->detach(); + } + + return redirect()->back()->with('success', __('Training session updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(TrainingSession $trainingSession) + { + // Check if training session belongs to current company + if (!in_array($trainingSession->trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this training session'); + } + + // Delete attendance records + $trainingSession->attendance()->delete(); + + // Detach trainers + $trainingSession->trainers()->detach(); + + // Delete the training session + $trainingSession->delete(); + + return redirect()->back()->with('success', __('Training session deleted successfully')); + } + + /** + * Update attendance for a training session. + */ + public function updateAttendance(Request $request, TrainingSession $trainingSession) + { + // Check if training session belongs to current company + if (!in_array($trainingSession->trainingProgram->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update attendance for this training session'); + } + + $validator = Validator::make($request->all(), [ + 'attendance' => 'required|array', + 'attendance.*.employee_id' => 'required|exists:users,id', + 'attendance.*.is_present' => 'required|boolean', + 'attendance.*.notes' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employees belong to current company + $employeeIds = collect($request->attendance)->pluck('employee_id')->toArray(); + $validEmployees = User::whereIn('id', $employeeIds) + ->whereIn('created_by', getCompanyAndUsersId()) + ->pluck('id') + ->toArray(); + + if (count($validEmployees) !== count($employeeIds)) { + return redirect()->back()->with('error', 'Invalid employee selection'); + } + + // Delete existing attendance records + $trainingSession->attendance()->delete(); + + // Create new attendance records + foreach ($request->attendance as $attendanceData) { + TrainingSessionAttendance::create([ + 'training_session_id' => $trainingSession->id, + 'employee_id' => $attendanceData['employee_id'], + 'is_present' => $attendanceData['is_present'], + 'notes' => $attendanceData['notes'], + ]); + } + + return redirect()->back()->with('success', __('Attendance updated successfully')); + } + + /** + * Create recurring sessions based on the pattern. + */ + private function createRecurringSessions($originalSession, $trainerIds = []) + { + $startDate = $originalSession->start_date; + $endDate = $originalSession->end_date; + $duration = $startDate->diffInSeconds($endDate); + + for ($i = 1; $i <= $originalSession->recurrence_count; $i++) { + // Calculate new dates based on recurrence pattern + switch ($originalSession->recurrence_pattern) { + case 'daily': + $newStartDate = $startDate->copy()->addDays($i); + break; + case 'weekly': + $newStartDate = $startDate->copy()->addWeeks($i); + break; + case 'monthly': + $newStartDate = $startDate->copy()->addMonths($i); + break; + default: + continue 2; // Skip this iteration if pattern is invalid + } + + $newEndDate = $newStartDate->copy()->addSeconds($duration); + + // Create new session + $newSession = TrainingSession::create([ + 'training_program_id' => $originalSession->training_program_id, + 'name' => $originalSession->name, + 'start_date' => $newStartDate, + 'end_date' => $newEndDate, + 'location' => $originalSession->location, + 'location_type' => $originalSession->location_type, + 'meeting_link' => $originalSession->meeting_link, + 'status' => 'scheduled', + 'notes' => $originalSession->notes, + 'is_recurring' => false, // Child sessions are not recurring + 'created_by' => $originalSession->created_by, + ]); + + // Attach trainers + if (!empty($trainerIds)) { + $newSession->trainers()->attach($trainerIds); + } + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/TrainingTypeController.php b/app/Http/Controllers/TrainingTypeController.php new file mode 100644 index 000000000..74ccd8fa4 --- /dev/null +++ b/app/Http/Controllers/TrainingTypeController.php @@ -0,0 +1,242 @@ +can('manage-training-types')) { + $query = TrainingType::with(['departments.branch', 'branch']) + ->withCount('trainingPrograms') + ->where(function ($q) { + if (Auth::user()->can('manage-any-training-types')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-training-types')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->where(function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + }); + } + + // Handle branch filter + if ($request->has('branch_id') && !empty($request->branch_id)) { + $query->whereHas('departments', function ($q) use ($request) { + $q->where('departments.branch_id', $request->branch_id); + }); + } + + // Handle department filter + if ($request->has('department_id') && !empty($request->department_id)) { + $query->whereHas('departments', function ($q) use ($request) { + $q->where('departments.id', $request->department_id); + }); + } + + // Handle sorting + $allowedSortFields = ['id', 'name', 'description', 'created_at']; + if ($request->has('sort_field') && !empty($request->sort_field) && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + + $trainingTypes = $query->paginate($request->per_page ?? 10); + + // Get branches for filter dropdown + $branches = Branch::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name') + ->get(); + + // Get departments for filter dropdown + $departments = Department::whereIn('created_by', getCompanyAndUsersId()) + ->select('id', 'name', 'branch_id') + ->get(); + + return Inertia::render('hr/training/types/index', [ + 'trainingTypes' => $trainingTypes, + 'branches' => $branches, + 'departments' => $departments, + 'filters' => $request->all(['search', 'branch_id', 'department_id', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'branch_id' => 'required|exists:branches,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if branch belongs to current company + $branch = Branch::find($request->branch_id); + if (!$branch || !in_array($branch->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid branch selected')); + } + + $trainingType = TrainingType::create([ + 'name' => $request->name, + 'description' => $request->description, + 'branch_id' => $request->branch_id, + 'created_by' => creatorId(), + ]); + + return redirect()->back()->with('success', __('Training type created successfully')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, TrainingType $trainingType) + { + // Check if training type belongs to current company + if (!in_array($trainingType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this training type')); + } + + $validator = Validator::make($request->all(), [ + 'name' => 'required|string|max:255', + 'description' => 'nullable|string', + 'branch_id' => 'required|exists:branches,id', + 'department_ids' => 'nullable|array', + 'department_ids.*' => 'exists:departments,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if branch belongs to current company + $branch = Branch::find($request->branch_id); + if (!$branch || !in_array($branch->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('Invalid branch selected')); + } + + // Check if departments belong to current company + if (!empty($request->department_ids)) { + $departmentIds = $request->department_ids; + $validDepartments = Department::whereIn('created_by', getCompanyAndUsersId()) + ->whereIn('id', $departmentIds) + ->pluck('id') + ->toArray(); + + if (count($validDepartments) !== count($departmentIds)) { + return redirect()->back()->with('error', __('Invalid department selection')); + } + } + + $trainingType->update([ + 'name' => $request->name, + 'description' => $request->description, + 'branch_id' => $request->branch_id, + ]); + + // Sync departments + if (isset($request->department_ids)) { + $trainingType->departments()->sync($request->department_ids); + } else { + $trainingType->departments()->detach(); + } + + return redirect()->back()->with('success', __('Training type updated successfully')); + } + + /** + * Assign departments to training type. + */ + public function assignDepartments(Request $request, TrainingType $trainingType) + { + // Check if training type belongs to current company + if (!in_array($trainingType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this training type')); + } + + $validator = Validator::make($request->all(), [ + 'department_ids' => 'nullable|array', + 'department_ids.*' => 'exists:departments,id', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if departments belong to current company and the training type's branch + if (!empty($request->department_ids)) { + $departmentIds = $request->department_ids; + $validDepartments = Department::whereIn('created_by', getCompanyAndUsersId()) + ->where('branch_id', $trainingType->branch_id) + ->whereIn('id', $departmentIds) + ->pluck('id') + ->toArray(); + + if (count($validDepartments) !== count($departmentIds)) { + return redirect()->back()->with('error', __('Invalid department selection for this training type\'s branch')); + } + } + + // Sync departments + if (isset($request->department_ids)) { + $trainingType->departments()->sync($request->department_ids); + } else { + $trainingType->departments()->detach(); + } + + return redirect()->back()->with('success', __('Departments assigned successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(TrainingType $trainingType) + { + // Check if training type belongs to current company + if (!in_array($trainingType->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this training type')); + } + + // Check if training type is being used by any training programs + if ($trainingType->trainingPrograms()->count() > 0) { + return redirect()->back()->with('error', __('Cannot delete training type that is being used by training programs')); + } + + // Detach all departments + $trainingType->departments()->detach(); + + // Delete the training type + $trainingType->delete(); + + return redirect()->back()->with('success', __('Training type deleted successfully')); + } +} diff --git a/app/Http/Controllers/TranslationController.php b/app/Http/Controllers/TranslationController.php new file mode 100644 index 000000000..d6a568bfc --- /dev/null +++ b/app/Http/Controllers/TranslationController.php @@ -0,0 +1,110 @@ +check()) { + // Update authenticated user's language and direction settings + auth()->user()->update(['lang' => $locale]); + + // Setting::updateOrCreate( + // [ + // 'key' => 'layoutDirection', + // 'user_id' => auth()->id() + // ], + // [ + // 'value' => $direction + // ] + // ); + // if (in_array($locale, ['ar', 'he'])) { + // Setting::updateOrCreate( + // [ + // 'key' => 'layoutDirection', + // 'user_id' => auth()->id() + // ], + // [ + // 'value' => $direction + // ] + // ); + // } + } else { + // For unauthenticated users on auth pages, use superadmin's language + $superAdmin = User::where('type', 'superadmin')->first(); + if ($superAdmin && request()->is('login', 'register', 'password/*', 'email/*')) { + $locale = $superAdmin->lang ?? 'en'; + $path = resource_path("lang/{$locale}.json"); + + if (!File::exists($path)) { + $path = resource_path("lang/en.json"); + $locale = 'en'; + } + + // Re-determine direction based on superadmin's locale + $direction = in_array($locale, ['ar', 'he']) ? 'right' : 'left'; + $layoutDirection = in_array($locale, ['ar', 'he']) ? 'rtl' : 'ltr'; + } + } + } + + $translations = json_decode(File::get($path), true); + + // Add layout direction to the response + $response = [ + 'translations' => $translations, + 'layoutDirection' => $layoutDirection, + 'locale' => $locale + ]; + + return response()->json($response); + } + + // Add a method to get the initial locale + public function getInitialLocale() + { + // First check cookie for all users for consistency + $cookieLang = Cookie::get('app_language'); + if ($cookieLang) { + return $cookieLang; + } + + if (auth()->check()) { + // For authenticated users, get from user preferences + return auth()->user()->lang ?? 'en'; + } else if (request()->is('login', 'register', 'password/*', 'email/*')) { + // For auth pages, get from superadmin + $superAdmin = User::where('type', 'superadmin')->first(); + return $superAdmin->lang ?? 'en'; + } + + // Default fallback + return 'en'; + } +} diff --git a/app/Http/Controllers/TripController.php b/app/Http/Controllers/TripController.php new file mode 100644 index 000000000..e2d3a39cc --- /dev/null +++ b/app/Http/Controllers/TripController.php @@ -0,0 +1,574 @@ +can('manage-trips')) { + $query = Trip::with(['employee', 'approver'])->where(function ($q) { + if (Auth::user()->can('manage-any-trips')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-trips')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }) + ->orWhere('purpose', 'like', '%' . $request->search . '%') + ->orWhere('destination', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('start_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('end_date', '<=', $request->date_to); + } + + // Handle sorting + $allowedSortFields = ['id', 'employee_id', 'purpose', 'destination', 'start_date', 'end_date', 'status', 'advance_amount', 'total_expenses']; + if ($request->has('sort_field') && !empty($request->sort_field) && in_array($request->sort_field, $allowedSortFields)) { + $sortDirection = in_array($request->sort_direction, ['asc', 'desc']) ? $request->sort_direction : 'asc'; + $query->orderBy($request->sort_field, $sortDirection); + } else { + $query->orderBy('id', 'desc'); + } + + $trips = $query->paginate($request->per_page ?? 10); + + $trips->getCollection()->transform(function ($trip) { + if ($trip->employee) { + $rawAvatar = $trip->employee->getRawOriginal('avatar'); + $trip->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $trip; + }); + + // Get employees for filter dropdown + $employees = User::with('employee') + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + + return Inertia::render('hr/trips/index', [ + 'trips' => $trips, + 'employees' => $this->getFilteredEmployees(), + 'filters' => $request->all(['search', 'employee_id', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-trips') && !Auth::user()->can('manage-any-trips')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + return $employees; + } + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'purpose' => 'required|string|max:255', + 'destination' => 'required|string|max:255', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'description' => 'nullable|string', + 'expected_outcomes' => 'nullable|string', + 'documents' => 'nullable|string', + 'advance_amount' => 'nullable|numeric|min:0', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $user = User::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (!$user) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + $tripData = [ + 'employee_id' => $request->employee_id, + 'purpose' => $request->purpose, + 'destination' => $request->destination, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'description' => $request->description, + 'expected_outcomes' => $request->expected_outcomes, + 'status' => 'planned', + 'advance_amount' => $request->advance_amount, + 'advance_status' => $request->advance_amount > 0 ? 'requested' : null, + 'created_by' => creatorId(), + ]; + + // Handle document from media library + if ($request->has('documents')) { + $tripData['documents'] = $request->documents; + } + + Trip::create($tripData); + + return redirect()->back()->with('success', __('Trip created successfully')); + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Trip $trip) + { + // Check if trip belongs to current company + if (!in_array($trip->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this trip'); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'purpose' => 'required|string|max:255', + 'destination' => 'required|string|max:255', + 'start_date' => 'required|date', + 'end_date' => 'required|date|after_or_equal:start_date', + 'description' => 'nullable|string', + 'expected_outcomes' => 'nullable|string', + 'status' => 'nullable|string|in:planned,ongoing,completed,cancelled', + 'documents' => 'nullable|string', + 'advance_amount' => 'nullable|numeric|min:0', + 'advance_status' => 'nullable|string|in:requested,approved,paid,reconciled', + 'reimbursement_status' => 'nullable|string|in:pending,approved,paid', + 'trip_report' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $user = User::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (!$user) { + return redirect()->back()->with('error', 'Invalid employee selected'); + } + + $tripData = [ + 'employee_id' => $request->employee_id, + 'purpose' => $request->purpose, + 'destination' => $request->destination, + 'start_date' => $request->start_date, + 'end_date' => $request->end_date, + 'description' => $request->description, + 'expected_outcomes' => $request->expected_outcomes, + 'advance_amount' => $request->advance_amount, + 'advance_status' => $request->advance_status, + 'reimbursement_status' => $request->reimbursement_status, + 'trip_report' => $request->trip_report, + ]; + + // Update status if provided and different from current + if ($request->has('status') && $request->status !== $trip->status) { + $tripData['status'] = $request->status; + + // If status is being set to ongoing, completed, or cancelled, set approved_by and approved_at + if (in_array($request->status, ['ongoing', 'completed', 'cancelled']) && !$trip->approved_by) { + $tripData['approved_by'] = auth()->id(); + $tripData['approved_at'] = now(); + } + } + + // Handle document from media library + if ($request->has('documents')) { + $tripData['documents'] = $request->documents; + } + + $trip->update($tripData); + + return redirect()->back()->with('success', __('Trip updated successfully')); + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Trip $trip) + { + // Check if trip belongs to current company + if (!in_array($trip->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this trip'); + } + + // Delete associated expenses + foreach ($trip->expenses as $expense) { + $expense->delete(); + } + + $trip->delete(); + + return redirect()->back()->with('success', __('Trip deleted successfully')); + } + + /** + * Change the status of the trip. + */ + public function changeStatus(Request $request, Trip $trip) + { + // Check if trip belongs to current company + if (!in_array($trip->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this trip'); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:planned,ongoing,completed,cancelled', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $updateData = [ + 'status' => $request->status, + ]; + + // If status is being set to ongoing, completed, or cancelled, set approved_by and approved_at + if (in_array($request->status, ['ongoing', 'completed', 'cancelled']) && !$trip->approved_by) { + $updateData['approved_by'] = auth()->id(); + $updateData['approved_at'] = now(); + } + + $trip->update($updateData); + + return redirect()->back()->with('success', __('Trip status updated successfully')); + } + + /** + * Update the advance status of the trip. + */ + public function updateAdvanceStatus(Request $request, Trip $trip) + { + // Check if trip belongs to current company + if (!in_array($trip->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this trip'); + } + + $validator = Validator::make($request->all(), [ + 'advance_status' => 'required|string|in:requested,approved,paid,reconciled', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $trip->update([ + 'advance_status' => $request->advance_status, + ]); + + return redirect()->back()->with('success', __('Trip advance status updated successfully')); + } + + /** + * Update the reimbursement status of the trip. + */ + public function updateReimbursementStatus(Request $request, Trip $trip) + { + // Check if trip belongs to current company + if (!in_array($trip->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this trip'); + } + + $validator = Validator::make($request->all(), [ + 'reimbursement_status' => 'required|string|in:pending,approved,paid', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $trip->update([ + 'reimbursement_status' => $request->reimbursement_status, + ]); + + return redirect()->back()->with('success', __('Trip reimbursement status updated successfully')); + } + + /** + * Download document file. + */ + public function downloadDocument(Trip $trip) + { + // Check if trip belongs to current company + if (!in_array($trip->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to access this document'); + } + + if (!$trip->documents) { + return redirect()->back()->with('error', 'Document file not found'); + } + + $filePath = getStorageFilePath($trip->documents); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', 'Document file not found'); + } + + return response()->download($filePath); + } + + /** + * Show the trip expenses. + */ + public function showExpenses(Trip $trip) + { + // Check if trip belongs to current company + if (!in_array($trip->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to view these expenses'); + } + + $expenses = $trip->expenses()->orderBy('expense_date', 'desc')->get(); + + return Inertia::render('hr/trips/expenses', [ + 'trip' => $trip->load('employee'), + 'expenses' => $expenses, + ]); + } + + /** + * Store a new expense for the trip. + */ + public function storeExpense(Request $request, Trip $trip) + { + // Check if trip belongs to current company + if (!in_array($trip->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to add expenses to this trip'); + } + + $validator = Validator::make($request->all(), [ + 'expense_type' => 'required|string|max:255', + 'expense_date' => 'required|date', + 'amount' => 'required|numeric|min:0', + 'currency' => 'required|string|max:10', + 'description' => 'nullable|string', + 'receipt' => 'nullable|string', + 'is_reimbursable' => 'nullable|boolean', + ]); + + // Validate date range separately to avoid type conversion issues + $expenseDate = $request->expense_date; + if ($expenseDate < $trip->start_date->format('Y-m-d') || $expenseDate > $trip->end_date->format('Y-m-d')) { + return redirect()->back()->withErrors(['expense_date' => 'The expense date must be between trip start and end dates.'])->withInput(); + } + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $expenseData = [ + 'trip_id' => $trip->id, + 'expense_type' => $request->expense_type, + 'expense_date' => $request->expense_date, + 'amount' => $request->amount, + 'currency' => $request->currency, + 'description' => $request->description, + 'is_reimbursable' => $request->is_reimbursable ?? true, + 'status' => 'pending', + 'created_by' => creatorId(), + ]; + + // Handle receipt from media library + if ($request->has('receipt')) { + $expenseData['receipt'] = $request->receipt; + } + + TripExpense::create($expenseData); + + // Update trip total expenses + $totalExpenses = $trip->expenses()->sum('amount') + $request->amount; + $trip->update([ + 'total_expenses' => $totalExpenses, + 'reimbursement_status' => 'pending' + ]); + + return redirect()->back()->with('success', __('Expense added successfully')); + } + + /** + * Update an expense for the trip. + */ + public function updateExpense(Request $request, Trip $trip, TripExpense $expense) + { + // Check if expense belongs to this trip and company + if ($expense->trip_id != $trip->id || !in_array($expense->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this expense'); + } + + $validator = Validator::make($request->all(), [ + 'expense_type' => 'required|string|max:255', + 'expense_date' => 'required|date', + 'amount' => 'required|numeric|min:0', + 'currency' => 'required|string|max:10', + 'description' => 'nullable|string', + 'receipt' => 'nullable|string', + 'is_reimbursable' => 'nullable|boolean', + 'status' => 'nullable|string|in:pending,approved,rejected', + ]); + + // Validate date range separately to avoid type conversion issues + $expenseDate = $request->expense_date; + if ($expenseDate < $trip->start_date->format('Y-m-d') || $expenseDate > $trip->end_date->format('Y-m-d')) { + return redirect()->back()->withErrors(['expense_date' => 'The expense date must be between trip start and end dates.'])->withInput(); + } + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $expenseData = [ + 'expense_type' => $request->expense_type, + 'expense_date' => $request->expense_date, + 'amount' => $request->amount, + 'currency' => $request->currency, + 'description' => $request->description, + 'is_reimbursable' => $request->is_reimbursable ?? true, + 'status' => $request->status ?? $expense->status, + ]; + + // Handle receipt from media library + if ($request->has('receipt')) { + $expenseData['receipt'] = $request->receipt; + } + + $oldAmount = $expense->amount; + $expense->update($expenseData); + + // Update trip total expenses + $totalExpenses = $trip->expenses()->sum('amount'); + $trip->update([ + 'total_expenses' => $totalExpenses + ]); + + return redirect()->back()->with('success', __('Expense updated successfully')); + } + + /** + * Delete an expense for the trip. + */ + public function destroyExpense(Trip $trip, TripExpense $expense) + { + // Check if expense belongs to this trip and company + if ($expense->trip_id != $trip->id || !in_array($expense->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to delete this expense'); + } + + // Delete receipt if exists + if ($expense->receipt) { + Storage::disk('public')->delete($expense->receipt); + } + + $expense->delete(); + + // Update trip total expenses + $totalExpenses = $trip->expenses()->sum('amount'); + $trip->update([ + 'total_expenses' => $totalExpenses + ]); + + return redirect()->back()->with('success', __('Expense deleted successfully')); + } + + /** + * Download receipt file. + */ + public function downloadReceipt(Trip $trip, TripExpense $expense) + { + // Check if expense belongs to this trip and company + if ($expense->trip_id != $trip->id || !in_array($expense->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to access this receipt'); + } + + if (!$expense->receipt) { + return redirect()->back()->with('error', 'Receipt file not found'); + } + + $filePath = getStorageFilePath($expense->receipt); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', 'Receipt file not found'); + } + + return response()->download($filePath); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 000000000..2b5832121 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,289 @@ +can('manage-users')) { + $authUserRole = $authUser->roles->first()?->name; + // Allow superadmin, admin, product-manager, contact-manager, viewer + if (!$authUser->hasPermissionTo('view-users')) { + abort(403, 'Unauthorized Access Prevented'); + } + + // $userQuery = User::withPermissionCheck()->with(['roles', 'creator'])->latest(); + $userQuery = User::with(['roles', 'creator'])->where(function ($q) { + if (Auth::user()->can('manage-any-users')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-users')) { + $q->where('created_by', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + })->latest(); + + # Admin + if ($authUserRole === 'super admin') { + $userQuery->whereDoesntHave('roles', function ($q) { + $q->where('name', 'super admin'); + }); + } + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $search = $request->search; + $userQuery->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('email', 'like', "%{$search}%"); + }); + } + + // Handle role filter + if ($request->has('role') && $request->role !== 'all') { + $userQuery->whereHas('roles', function ($q) use ($request) { + $q->where('roles.id', $request->role); + }); + } + + // Handle sorting + if ($request->has('sort_field') && $request->has('sort_direction')) { + $userQuery->orderBy($request->sort_field, $request->sort_direction); + } + + // Handle pagination + $perPage = $request->has('per_page') ? (int) $request->per_page : 10; + $users = $userQuery->where('type', '!=', 'employee')->paginate($perPage)->withQueryString(); + + // Transform data to resolve avatar URLs + $users->getCollection()->transform(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'email' => $user->email, + 'avatar' => check_file($user->avatar) ? get_file($user->avatar) : get_file('avatars/avatar.png'), + 'type' => $user->type, + 'status' => $user->status, + 'created_at' => $user->created_at, + 'roles' => $user->roles->map(fn($role) => [ + 'id' => $role->id, + 'name' => $role->name, + 'label' => $role->label ?? $role->name, + ]), + ]; + }); + + # Roles listing - Get all roles without filtering + if ($authUserRole == 'company') { + // $roles = Role::where('created_by', $authUser->id)->get(); + $roles = Role::whereIn('created_by', getCompanyAndUsersId()) + ->where('name', '!=', 'employee') + ->get(); + } else { + $roles = Role::where('name', '!=', 'employee')->whereIn('created_by', getCompanyAndUsersId())->get(); + } + + // Get plan limits for company users and staff users (only in SaaS mode) + $planLimits = null; + if (isSaas()) { + if ($authUser->type === 'company' && $authUser->plan) { + $currentUserCount = User::whereIn('created_by', getCompanyAndUsersId())->where('type', '!=', 'employee')->count(); + $planLimits = [ + 'current_users' => $currentUserCount, + 'max_users' => $authUser->plan->max_users, + 'can_create' => $currentUserCount < $authUser->plan->max_users + ]; + } + // Check for staff users (created by company users) + elseif ($authUser->type !== 'superadmin' && $authUser->created_by) { + $companyUser = User::find($authUser->created_by); + if ($companyUser && $companyUser->type === 'company' && $companyUser->plan) { + $currentUserCount = User::whereIn('created_by', getCompanyAndUsersId())->where('type', '!=', 'employee')->count(); + $planLimits = [ + 'current_users' => $currentUserCount, + 'max_users' => $companyUser->plan->max_users, + 'can_create' => $currentUserCount < $companyUser->plan->max_users + ]; + } + } + } + + + return Inertia::render('users/index', [ + 'users' => $users, + 'roles' => $roles, + 'planLimits' => $planLimits, + 'filters' => [ + 'search' => $request->search ?? '', + 'role' => $request->role ?? 'all', + 'per_page' => $perPage, + 'sort_field' => $request->sort_field ?? 'created_at', + 'sort_direction' => $request->sort_direction ?? 'desc', + ], + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Store a newly created resource in storage. + */ + public function store(UserRequest $request) + { + // Set user language same as creator (company) + $authUser = Auth::user(); + if (Auth::user()->can('create-users')) { + $companySettings = settings(); + $userLang = isset($companySettings['defaultLanguage']) ? $companySettings['defaultLanguage'] : $authUser->lang; + // Check plan limits for company users (only in SaaS mode) + if (isSaas() && $authUser->type === 'company' && $authUser->plan) { + $currentUserCount = User::where('created_by', $authUser->id)->count(); + $maxUsers = $authUser->plan->max_users; + + if ($currentUserCount >= $maxUsers) { + return redirect()->back()->with('error', __('User limit exceeded. Your plan allows maximum :max users. Please upgrade your plan.', ['max' => $maxUsers])); + } + } + // Check plan limits for staff users (created by company users) + elseif (isSaas() && $authUser->type !== 'superadmin' && $authUser->created_by) { + $companyUser = User::find($authUser->created_by); + if ($companyUser && $companyUser->type === 'company' && $companyUser->plan) { + $currentUserCount = User::where('created_by', $companyUser->id)->count(); + $maxUsers = $companyUser->plan->max_users; + + if ($currentUserCount >= $maxUsers) { + return redirect()->back()->with('error', __('User limit exceeded. Your company plan allows maximum :max users. Please contact your administrator.', ['max' => $maxUsers])); + } + } + } + + if (!in_array(auth()->user()->type, ['superadmin', 'company'])) { + $created_by = auth()->user()->created_by; + } else { + $created_by = auth()->id(); + } + + $user = User::create([ + 'name' => $request->name, + 'email' => $request->email, + 'password' => Hash::make($request->password), + 'created_by' => creatorId(), + 'lang' => $userLang, + ]); + + if ($user && $request->roles) { + // Convert role names to IDs for syncing + $role = Role::where('id', $request->roles) + ->where('created_by', $created_by)->first(); + + $user->roles()->sync([$role->id]); + $user->type = $role->name; + $user->save(); + + // Trigger email notification + event(new \App\Events\UserCreated($user, $request->password)); + + // Check for email errors + if (session()->has('email_error')) { + return redirect()->route('users.index')->with('warning', __('User created successfully, but welcome email failed: ') . session('email_error')); + } + + return redirect()->route('users.index')->with('success', __('User created with roles')); + } + return redirect()->back()->with('error', __('Unable to create User. Please try again!')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(UserRequest $request, User $user) + { + if (Auth::user()->can('edit-users')) { + if ($user) { + $user->name = $request->name; + $user->email = $request->email; + + // find and syncing role + if ($request->roles) { + if (!in_array(auth()->user()->type, ['superadmin', 'company'])) { + $created_by = auth()->user()->created_by; + } else { + $created_by = auth()->id(); + } + $role = Role::where('id', $request->roles) + ->where('created_by', $created_by)->first(); + + $user->roles()->sync([$role->id]); + $user->type = $role->name; + } + + $user->save(); + return redirect()->route('users.index')->with('success', __('User updated with roles')); + } + return redirect()->back()->with('error', __('Unable to update User. Please try again!')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(User $user) + { + if (Auth::user()->can('delete-users')) { + if ($user) { + $user->delete(); + return redirect()->route('users.index')->with('success', __('User deleted with roles')); + } + return redirect()->back()->with('error', __('Unable to delete User. Please try again!')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Reset user password + */ + public function resetPassword(Request $request, User $user) + { + $request->validate([ + 'password' => 'required|min:8|confirmed', + ]); + + $user->password = Hash::make($request->password); + $user->save(); + + return redirect()->route('users.index')->with('success', __('Password reset successfully')); + } + + /** + * Toggle user status + */ + public function toggleStatus(User $user) + { + $user->status = $user->status === 'active' ? 'inactive' : 'active'; + $user->save(); + + return redirect()->route('users.index')->with('success', __('User status updated successfully')); + } + + // switchBusiness method removed +} diff --git a/app/Http/Controllers/WarningController.php b/app/Http/Controllers/WarningController.php new file mode 100644 index 000000000..f8b00a311 --- /dev/null +++ b/app/Http/Controllers/WarningController.php @@ -0,0 +1,437 @@ +can('manage-warnings')) { + $query = Warning::with(['employee', 'issuer', 'approver'])->where(function ($q) { + if (Auth::user()->can('manage-any-warnings')) { + $q->whereIn('created_by', getCompanyAndUsersId()); + } elseif (Auth::user()->can('manage-own-warnings')) { + $q->where('created_by', Auth::id())->orWhere('employee_id', Auth::id()); + } else { + $q->whereRaw('1 = 0'); + } + }); + + // Handle search + if ($request->has('search') && !empty($request->search)) { + $query->whereHas('employee', function ($q) use ($request) { + $q->where('name', 'like', '%' . $request->search . '%') + ->orWhere('employee_id', 'like', '%' . $request->search . '%'); + }) + ->orWhere('subject', 'like', '%' . $request->search . '%') + ->orWhere('description', 'like', '%' . $request->search . '%'); + } + + // Handle employee filter + if ($request->has('employee_id') && !empty($request->employee_id)) { + $query->where('employee_id', $request->employee_id); + } + + // Handle warning type filter + if ($request->has('warning_type') && !empty($request->warning_type)) { + $query->where('warning_type', $request->warning_type); + } + + // Handle severity filter + if ($request->has('severity') && !empty($request->severity)) { + $query->where('severity', $request->severity); + } + + // Handle status filter + if ($request->has('status') && !empty($request->status) && $request->status !== 'all') { + $query->where('status', $request->status); + } + + // Handle date range filter + if ($request->has('date_from') && !empty($request->date_from)) { + $query->whereDate('warning_date', '>=', $request->date_from); + } + if ($request->has('date_to') && !empty($request->date_to)) { + $query->whereDate('warning_date', '<=', $request->date_to); + } + + // Handle sorting + $sortField = $request->get('sort_field', 'created_at'); + $sortDirection = $request->get('sort_direction', 'desc'); + + // Validate sort field + $allowedSortFields = ['warning_date', 'created_at', 'id']; + if (!in_array($sortField, $allowedSortFields)) { + $sortField = 'created_at'; + } + + $query->orderBy($sortField, $sortDirection); + + $warnings = $query->paginate($request->per_page ?? 10); + + $warnings->getCollection()->transform(function ($warning) { + if ($warning->employee) { + $rawAvatar = $warning->employee->getRawOriginal('avatar'); + $warning->employee->avatar = check_file($rawAvatar) + ? get_file($rawAvatar) + : get_file('avatars/avatar.png'); + } + return $warning; + }); + + // Get managers for issuer dropdown + $managers = User::whereIn('created_by', getCompanyAndUsersId()) + ->whereHas('roles', function ($q) { + $q->where('name', 'like', '%Manager%') + ->orWhere('name', 'like', '%HR%'); + }) + ->select('id', 'name') + ->get(); + + + // Get warning types for filter dropdown + $warningTypes = Warning::whereIn('created_by', getCompanyAndUsersId()) + ->select('warning_type') + ->distinct() + ->pluck('warning_type') + ->toArray(); + + return Inertia::render('hr/warnings/index', [ + 'warnings' => $warnings, + 'employees' => $this->getFilteredEmployees(), + 'managers' => $managers, + 'warningTypes' => $warningTypes, + 'filters' => $request->all(['search', 'employee_id', 'warning_type', 'severity', 'status', 'date_from', 'date_to', 'sort_field', 'sort_direction', 'per_page']), + ]); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + private function getFilteredEmployees() + { + // Get employees for filter dropdown (compatible with getFilteredEmployees logic) + $employeeQuery = Employee::whereIn('created_by', getCompanyAndUsersId()); + + if (Auth::user()->can('manage-own-warnings') && !Auth::user()->can('manage-any-warnings')) { + $employeeQuery->where(function ($q) { + $q->where('created_by', Auth::id())->orWhere('user_id', Auth::id()); + }); + } + + $employees = User::emp() + ->with('employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->where('status', 'active') + ->whereIn('id', $employeeQuery->pluck('user_id')) + ->select('id', 'name') + ->get() + ->map(function ($user) { + return [ + 'id' => $user->id, + 'name' => $user->name, + 'employee_id' => $user->employee->employee_id ?? '' + ]; + }); + return $employees; + } + + /** + * Store a newly created resource in storage. + */ + public function store(Request $request) + { + if (Auth::user()->can('create-warnings')) { + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'warning_by' => 'required|exists:users,id', + 'warning_type' => 'required|string|max:255', + 'subject' => 'required|string|max:255', + 'severity' => 'required|string|in:verbal,written,final', + 'warning_date' => 'required|date', + 'description' => 'nullable|string', + 'documents' => 'nullable|string', + 'expiry_date' => 'nullable|date|after:warning_date', + 'has_improvement_plan' => 'nullable|boolean', + 'improvement_plan_goals' => 'nullable|string|required_if:has_improvement_plan,true', + 'improvement_plan_start_date' => 'nullable|date|required_if:has_improvement_plan,true', + 'improvement_plan_end_date' => 'nullable|date|after:improvement_plan_start_date|required_if:has_improvement_plan,true', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $user = UserModel::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (!$user) { + return redirect()->back()->with('error', __('Invalid employee selected')); + } + + // Check if warning issuer belongs to current company + $issuer = User::find($request->warning_by); + if (!$issuer || (!in_array($issuer->created_by, getCompanyAndUsersId()) && !in_array($issuer->id, getCompanyAndUsersId()))) { + return redirect()->back()->with('error', __('Invalid warning issuer selected')); + } + + $warningData = [ + 'employee_id' => $request->employee_id, + 'warning_by' => $request->warning_by, + 'warning_type' => $request->warning_type, + 'subject' => $request->subject, + 'severity' => $request->severity, + 'warning_date' => $request->warning_date, + 'description' => $request->description, + 'status' => 'draft', + 'expiry_date' => $request->expiry_date, + 'has_improvement_plan' => $request->has_improvement_plan ?? false, + 'improvement_plan_goals' => $request->improvement_plan_goals, + 'improvement_plan_start_date' => $request->improvement_plan_start_date, + 'improvement_plan_end_date' => $request->improvement_plan_end_date, + 'created_by' => creatorId(), + ]; + + // Handle document from media library + if ($request->has('documents')) { + $warningData['documents'] = $request->documents; + } + + Warning::create($warningData); + + return redirect()->back()->with('success', __('Warning created successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the specified resource in storage. + */ + public function update(Request $request, Warning $warning) + { + if (Auth::user()->can('edit-warnings')) { + // Check if warning belongs to current company + if (!in_array($warning->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', 'You do not have permission to update this warning'); + } + + $validator = Validator::make($request->all(), [ + 'employee_id' => 'required|exists:users,id', + 'warning_by' => 'required|exists:users,id', + 'warning_type' => 'required|string|max:255', + 'subject' => 'required|string|max:255', + 'severity' => 'required|string|in:verbal,written,final', + 'warning_date' => 'required|date', + 'description' => 'nullable|string', + 'status' => 'nullable|string|in:draft,issued,acknowledged,expired', + 'documents' => 'nullable|string', + 'acknowledgment_date' => 'nullable|date|after_or_equal:warning_date', + 'employee_response' => 'nullable|string', + 'expiry_date' => 'nullable|date|after:warning_date', + 'has_improvement_plan' => 'nullable|boolean', + 'improvement_plan_goals' => 'nullable|string|required_if:has_improvement_plan,true', + 'improvement_plan_start_date' => 'nullable|date|required_if:has_improvement_plan,true', + 'improvement_plan_end_date' => 'nullable|date|after:improvement_plan_start_date|required_if:has_improvement_plan,true', + 'improvement_plan_progress' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Check if employee belongs to current company + $user = UserModel::where('id', $request->employee_id) + ->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->first(); + if (!$user) { + return redirect()->back()->with('error', 'Invalid employee selected'); + } + + // Check if warning issuer belongs to current company + $issuer = User::find($request->warning_by); + if (!$issuer || (!in_array($issuer->created_by, getCompanyAndUsersId()) && !in_array($issuer->id, getCompanyAndUsersId()))) { + return redirect()->back()->with('error', 'Invalid warning issuer selected'); + } + + $warningData = [ + 'employee_id' => $request->employee_id, + 'warning_by' => $request->warning_by, + 'warning_type' => $request->warning_type, + 'subject' => $request->subject, + 'severity' => $request->severity, + 'warning_date' => $request->warning_date, + 'description' => $request->description, + 'acknowledgment_date' => $request->acknowledgment_date, + 'employee_response' => $request->employee_response, + 'expiry_date' => $request->expiry_date, + 'has_improvement_plan' => $request->has_improvement_plan ?? false, + 'improvement_plan_goals' => $request->improvement_plan_goals, + 'improvement_plan_start_date' => $request->improvement_plan_start_date, + 'improvement_plan_end_date' => $request->improvement_plan_end_date, + 'improvement_plan_progress' => $request->improvement_plan_progress, + ]; + + // Update status if provided and different from current + if ($request->has('status') && $request->status !== $warning->status) { + $warningData['status'] = $request->status; + + // If status is being set to issued, acknowledged, or expired, set approved_by and approved_at + if (in_array($request->status, ['issued', 'acknowledged', 'expired']) && !$warning->approved_by) { + $warningData['approved_by'] = auth()->id(); + $warningData['approved_at'] = now(); + } + } + + // Handle document from media library + if ($request->has('documents')) { + $warningData['documents'] = $request->documents; + } + + $warning->update($warningData); + + return redirect()->back()->with('success', __('Warning updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Remove the specified resource from storage. + */ + public function destroy(Warning $warning) + { + if (Auth::user()->can('delete-warnings')) { + // Check if warning belongs to current company + if (!in_array($warning->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to delete this warning')); + } + $warning->delete(); + + return redirect()->back()->with('success', __('Warning deleted successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Change the status of the warning. + */ + public function changeStatus(Request $request, Warning $warning) + { + if (Auth::user()->can('approve-warnings') || Auth::user()->can('acknowledge-warnings') || Auth::user()->can('edit-warnings')) { + // Check if warning belongs to current company + if (!in_array($warning->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this warning')); + } + + $validator = Validator::make($request->all(), [ + 'status' => 'required|string|in:draft,issued,acknowledged,expired', + 'acknowledgment_date' => 'nullable|date|required_if:status,acknowledged', + 'employee_response' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $updateData = [ + 'status' => $request->status, + 'acknowledgment_date' => $request->acknowledgment_date, + 'employee_response' => $request->employee_response, + ]; + + // If status is being set to issued, acknowledged, or expired, set approved_by and approved_at + if (in_array($request->status, ['issued', 'acknowledged', 'expired']) && !$warning->approved_by) { + $updateData['approved_by'] = auth()->id(); + $updateData['approved_at'] = now(); + } + + $warning->update($updateData); + + return redirect()->back()->with('success', __('Warning status updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Update the improvement plan for a warning. + */ + public function updateImprovementPlan(Request $request, Warning $warning) + { + if (Auth::user()->can('edit-warnings')) { + // Check if warning belongs to current company + if (!in_array($warning->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to update this warning')); + } + + $validator = Validator::make($request->all(), [ + 'has_improvement_plan' => 'required|boolean', + 'improvement_plan_goals' => 'nullable|string|required_if:has_improvement_plan,true', + 'improvement_plan_start_date' => 'nullable|date|required_if:has_improvement_plan,true', + 'improvement_plan_end_date' => 'nullable|date|after:improvement_plan_start_date|required_if:has_improvement_plan,true', + 'improvement_plan_progress' => 'nullable|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + $updateData = [ + 'has_improvement_plan' => $request->has_improvement_plan, + 'improvement_plan_goals' => $request->improvement_plan_goals, + 'improvement_plan_start_date' => $request->improvement_plan_start_date, + 'improvement_plan_end_date' => $request->improvement_plan_end_date, + 'improvement_plan_progress' => $request->improvement_plan_progress, + ]; + + $warning->update($updateData); + + return redirect()->back()->with('success', __('Improvement plan updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Download document file. + */ + public function downloadDocument(Warning $warning) + { + if (Auth::user()->can('view-warnings')) { + // Check if warning belongs to current company + if (!in_array($warning->created_by, getCompanyAndUsersId())) { + return redirect()->back()->with('error', __('You do not have permission to access this document')); + } + + if (!$warning->documents) { + return redirect()->back()->with('error', __('Document file not found')); + } + + $filePath = getStorageFilePath($warning->documents); + + if (!file_exists($filePath)) { + return redirect()->back()->with('error', __('Document file not found')); + } + + return response()->download($filePath); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Controllers/XenditPaymentController.php b/app/Http/Controllers/XenditPaymentController.php new file mode 100644 index 000000000..b35bfd185 --- /dev/null +++ b/app/Http/Controllers/XenditPaymentController.php @@ -0,0 +1,191 @@ +json(['error' => __('Xendit not configured')], 400); + } + + $user = auth()->user(); + $externalId = 'plan_' . $plan->id . '_' . $user->id . '_' . time(); + + $invoiceData = [ + 'external_id' => $externalId, + 'amount' => $pricing['final_price'], + 'description' => 'Plan Subscription: ' . $plan->name, + 'invoice_duration' => 86400, + 'currency' => 'PHP', + 'customer' => [ + 'given_names' => $user->name ?? 'Customer', + 'email' => $user->email + ], + 'success_redirect_url' => route('xendit.success', [ + 'plan_id' => $plan->id, + 'user_id' => $user->id, + 'billing_cycle' => $validated['billing_cycle'], + 'coupon_code' => $validated['coupon_code'] ?? '' + ]), + 'failure_redirect_url' => route('plans.index') + ]; + + $response = \Http::withHeaders([ + 'Authorization' => 'Basic ' . base64_encode($settings['payment_settings']['xendit_api_key'] . ':'), + 'Content-Type' => 'application/json' + ])->post('https://api.xendit.co/v2/invoices', $invoiceData); + + if ($response->successful()) { + $result = $response->json(); + if (isset($result['invoice_url'])) { + return response()->json([ + 'success' => true, + 'payment_url' => $result['invoice_url'], + 'external_id' => $externalId + ]); + } + } + + return response()->json(['error' => $response->body()], 500); + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + try { + $planId = $request->input('plan_id'); + $userId = $request->input('user_id'); + $billingCycle = $request->input('billing_cycle', 'monthly'); + $couponCode = $request->input('coupon_code'); + + if ($planId && $userId) { + $plan = Plan::find($planId); + $user = \App\Models\User::find($userId); + + if ($plan && $user) { + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $billingCycle, + 'payment_method' => 'xendit', + 'coupon_code' => $couponCode, + 'payment_id' => $request->input('external_id', 'xendit_' . time()), + ]); + + if (!auth()->check()) { + auth()->login($user); + } + + return redirect()->route('plans.index')->with('success', __('Payment completed successfully and plan activated')); + } + } + + return redirect()->route('plans.index')->with('error', __('Payment verification failed')); + + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment processing failed')); + } + } + + public function processPayment(Request $request) + { + $validated = validatePaymentRequest($request, [ + 'external_id' => 'required|string', + 'customer_details' => 'required|array', + ]); + + try { + $settings = getPaymentMethodConfig('xendit'); + + $plan = Plan::findOrFail($validated['plan_id']); + $pricing = calculatePlanPricing($plan, $validated['coupon_code'] ?? null); + + createPlanOrder([ + 'user_id' => auth()->id(), + 'plan_id' => $validated['plan_id'], + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'xendit', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['external_id'], + 'status' => 'pending' + ]); + + $invoiceData = [ + 'external_id' => $validated['external_id'], + 'amount' => $pricing['final_price'], + 'description' => 'Plan Subscription - ' . $plan->name, + 'invoice_duration' => 86400, + 'customer' => [ + 'given_names' => $validated['customer_details']['firstName'], + 'surname' => $validated['customer_details']['lastName'], + 'email' => $validated['customer_details']['email'] + ], + 'customer_notification_preference' => [ + 'invoice_created' => ['email'], + 'invoice_reminder' => ['email'], + 'invoice_paid' => ['email'] + ], + 'success_redirect_url' => route('plans.index'), + 'failure_redirect_url' => route('plans.index') + ]; + + $response = \Http::withHeaders([ + 'Authorization' => 'Basic ' . base64_encode($settings['secret_key'] . ':'), + 'Content-Type' => 'application/json' + ])->post('https://api.xendit.co/v2/invoices', $invoiceData); + + if ($response->successful()) { + $result = $response->json(); + if (isset($result['invoice_url'])) { + return redirect($result['invoice_url']); + } + } + + processPaymentSuccess([ + 'user_id' => auth()->id(), + 'plan_id' => $validated['plan_id'], + 'billing_cycle' => $validated['billing_cycle'], + 'payment_method' => 'xendit', + 'coupon_code' => $validated['coupon_code'] ?? null, + 'payment_id' => $validated['external_id'], + ]); + + return redirect()->route('plans.index')->with('success', __('Xendit payment completed')); + } catch (\Exception $e) { + return handlePaymentError($e, 'xendit'); + } + } + + public function callback(Request $request) + { + $externalId = $request->input('external_id'); + $status = $request->input('status'); + + if ($status === 'PAID') { + $planOrder = PlanOrder::where('payment_id', $externalId)->first(); + + if ($planOrder && $planOrder->status === 'pending') { + $planOrder->update(['status' => 'approved']); + $planOrder->activateSubscription(); + } + } + + return response('OK', 200); + } +} \ No newline at end of file diff --git a/app/Http/Controllers/YooKassaPaymentController.php b/app/Http/Controllers/YooKassaPaymentController.php new file mode 100644 index 000000000..34f736d73 --- /dev/null +++ b/app/Http/Controllers/YooKassaPaymentController.php @@ -0,0 +1,155 @@ +json(['error' => 'YooKassa not configured'], 400); + } + + $client = new Client(); + $client->setAuth((int)$settings['payment_settings']['yookassa_shop_id'], $settings['payment_settings']['yookassa_secret_key']); + + $orderID = strtoupper(str_replace('.', '', uniqid('', true))); + $user = auth()->user(); + + $payment = $client->createPayment([ + 'amount' => [ + 'value' => number_format($pricing['final_price'], 2, '.', ''), + 'currency' => 'RUB', + ], + 'confirmation' => [ + 'type' => 'redirect', + 'return_url' => route('yookassa.success', [ + 'plan_id' => $plan->id, + 'order_id' => $orderID, + 'billing_cycle' => $validated['billing_cycle'], + 'coupon_code' => $validated['coupon_code'] ?? null + ]), + ], + 'capture' => true, + 'description' => 'Plan: ' . $plan->name, + 'metadata' => [ + 'plan_id' => $plan->id, + 'user_id' => $user->id, + 'billing_cycle' => $validated['billing_cycle'], + 'coupon_code' => $validated['coupon_code'] ?? null, + 'order_id' => $orderID + ] + ], uniqid('', true)); + + if ($payment['confirmation']['confirmation_url'] != null) { + return response()->json([ + 'success' => true, + 'payment_url' => $payment['confirmation']['confirmation_url'], + 'payment_id' => $payment['id'] + ]); + } else { + return response()->json(['error' => __('Payment creation failed')], 500); + } + + } catch (\Exception $e) { + return response()->json(['error' => __('Payment creation failed')], 500); + } + } + + public function success(Request $request) + { + try { + $planId = $request->input('plan_id'); + $billingCycle = $request->input('billing_cycle'); + $couponCode = $request->input('coupon_code'); + $orderId = $request->input('order_id'); + + if ($planId && $orderId) { + $plan = Plan::find($planId); + + // Find user by session or create temporary assignment + $user = null; + if (auth()->check()) { + $user = auth()->user(); + } else { + // Try to find user from recent plan orders + $recentOrder = \App\Models\PlanOrder::where('payment_id', 'like', '%' . substr($orderId, -8)) + ->where('created_at', '>=', now()->subHours(1)) + ->first(); + if ($recentOrder) { + $user = \App\Models\User::find($recentOrder->user_id); + } + } + + if ($plan && $user) { + // Assign plan to user immediately + $user->plan_id = $plan->id; + $user->plan_expire_date = $billingCycle === 'yearly' ? now()->addYear() : now()->addMonth(); + $user->save(); + + // Create plan order record + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $billingCycle, + 'payment_method' => 'yookassa', + 'coupon_code' => $couponCode, + 'payment_id' => $orderId, + ]); + + return redirect()->route('plans.index')->with('success', 'Payment successful and plan activated'); + } + } + return redirect()->route('plans.index')->with('error', __('Payment verification failed')); + } catch (\Exception $e) { + return redirect()->route('plans.index')->with('error', __('Payment processing failed')); + } + } + + public function callback(Request $request) + { + try { + $paymentId = $request->input('object.id'); + $status = $request->input('object.status'); + $metadata = $request->input('object.metadata'); + + if ($paymentId && $status === 'succeeded' && $metadata) { + $planId = $metadata['plan_id']; + $userId = $metadata['user_id']; + + $plan = Plan::find($planId); + $user = \App\Models\User::find($userId); + + if ($plan && $user) { + // Assign plan to user + $user->plan_id = $plan->id; + $user->plan_expire_date = $metadata['billing_cycle'] === 'yearly' ? now()->addYear() : now()->addMonth(); + $user->save(); + + processPaymentSuccess([ + 'user_id' => $user->id, + 'plan_id' => $plan->id, + 'billing_cycle' => $metadata['billing_cycle'] ?? 'monthly', + 'payment_method' => 'yookassa', + 'coupon_code' => $metadata['coupon_code'] ?? null, + 'payment_id' => $paymentId, + ]); + } + } + return response()->json(['status' => 'success']); + } catch (\Exception $e) { + return response()->json(['error' => __('Callback processing failed')], 500); + } + } +} \ No newline at end of file diff --git a/app/Http/Controllers/ZektoSettingsController.php b/app/Http/Controllers/ZektoSettingsController.php new file mode 100644 index 000000000..4d3a1f84d --- /dev/null +++ b/app/Http/Controllers/ZektoSettingsController.php @@ -0,0 +1,96 @@ +can('manage-biomatric-attedance-settings')) { + $validator = Validator::make($request->all(), [ + 'zkteco_api_url' => 'required|url', + 'zkteco_username' => 'required|string|max:255', + 'zkteco_password' => 'required|string|max:255', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + // Update settings + updateSetting('zkteco_api_url', $request->zkteco_api_url); + updateSetting('zkteco_username', $request->zkteco_username); + updateSetting('zkteco_password', $request->zkteco_password); + updateSetting('isZktecoSync', 0); + + return redirect()->back()->with('success', __('ZKTeco settings updated successfully')); + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } + + /** + * Generate auth token from ZKTeco API + */ + public function generateToken(Request $request) + { + if (Auth::user()->can('manage-biomatric-attedance-settings')) { + $validator = Validator::make($request->all(), [ + 'zkteco_api_url' => 'required|url', + 'zkteco_username' => 'required|string', + 'zkteco_password' => 'required|string', + ]); + + if ($validator->fails()) { + return redirect()->back()->withErrors($validator)->withInput(); + } + + try { + $url = "$request->zkteco_api_url" . '/api-token-auth/'; + $headers = array( + "Content-Type: application/json" + ); + $data = array( + "username" => $request->zkteco_username, + "password" => $request->zkteco_password + ); + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $response = curl_exec($ch); + curl_close($ch); + $auth_token = json_decode($response, true); + if (isset($auth_token['token'])) { + // Store the generated token using existing settings structure + updateSetting('isZktecoSync', 1); + updateSetting('zkteco_auth_token', $auth_token['token']); + + return redirect()->back()->with([ + 'success' => __('Auth token generated successfully'), + 'token' => $auth_token['token'] + ]); + } else { + throw new \Exception(isset($auth_token['non_field_errors']) ? $auth_token['non_field_errors'][0] : "Something went wrong please try again"); + } + } catch (\Exception $e) { + + return redirect()->back()->with('error', $e->getMessage()); + } + } else { + return redirect()->back()->with('error', __('Permission Denied.')); + } + } +} diff --git a/app/Http/Middleware/CareerSharedDataMiddleware.php b/app/Http/Middleware/CareerSharedDataMiddleware.php new file mode 100644 index 000000000..923b63017 --- /dev/null +++ b/app/Http/Middleware/CareerSharedDataMiddleware.php @@ -0,0 +1,93 @@ +route('userSlug'); + $userId = $this->getUserIdFromSlug($userSlug); + + if (! $userId) { + abort(403, 'Company not found'); + } + + $companySettings = $this->getCompanySettings($userId); + $companyCurrency = $companySettings['defaultCurrency'] ?? 'USD'; + if ($companyCurrency) { + $getCurrencySymbol = Currency::where('code', $companyCurrency)->first(); + $currencySymbol = null; + if ($getCurrencySymbol) { + $currencySymbol = $getCurrencySymbol->symbol; + } else { + $currencySymbol = '$'; + } + } + $companySettings = array_merge($companySettings, [ + 'currencySymbol' => $currencySymbol, + ]); + + $companyUser = User::find($userId); + if ($companyUser) { + $companySettings = array_merge($companySettings, [ + 'company_name' => $companyUser->name, + 'company_email' => $companyUser->email, + ]); + } + + // Add to request for controller access + $request->merge([ + 'companyId' => $userId, + 'userSlug' => $userSlug, + 'companySettings' => $companySettings, + ]); + + Inertia::share([ + 'companySettings' => $companySettings, + 'userSlug' => $userSlug, + 'companyId' => $userId, + ]); + + return $next($request); + } + + private function getUserIdFromSlug($userSlug) + { + if ($userSlug) { + $user = User::where('slug', $userSlug)->first(); + if ($user) { + $userId = getCompanyId($user->id); + + return $userId; + } + + return null; + } + + return null; + } + + private function getCompanySettings($userId) + { + if (! $userId) { + return []; + } + + $settings = settings($userId); + $user = User::find($userId); + + if ($user) { + $settings['company_name'] = $user->name; + $settings['company_email'] = $user->email; + } + + return $settings; + } +} diff --git a/app/Http/Middleware/CheckInstallation.php b/app/Http/Middleware/CheckInstallation.php new file mode 100644 index 000000000..8660aaeb3 --- /dev/null +++ b/app/Http/Middleware/CheckInstallation.php @@ -0,0 +1,75 @@ +is('install/*') || + $request->is('update/*') || + $request->is('css/*') || + $request->is('js/*') || + $request->is('images/*') || + $request->is('installer/*') + ) { + return $next($request); + } + + // Check only on dashboard, login, register routes + if (!$request->is('/*') && !$request->is('dashboard*') && !$request->is('login') && !$request->is('register')) { + return $next($request); + } + + // If not installed, redirect to /install + if (!$this->isInstalled()) { + return redirect('/install'); + } + + // If logged in as superadmin and migrations needed, redirect to /update + if (isSaas()) { + if (auth()->check() && auth()->user()->hasRole('superadmin') && $this->needsMigration()) { + return redirect('/update'); + } + } else { + if (auth()->check() && auth()->user()->hasRole('company') && $this->needsMigration()) { + return redirect('/update'); + } + + } + + + return $next($request); + } + + /** + * Check if application is installed + */ + private function isInstalled(): bool + { + return file_exists(storage_path('installed')); + } + + /** + * Check if migrations are needed + */ + private function needsMigration(): bool + { + try { + Artisan::call('migrate:status'); + $output = Artisan::output(); + return strpos($output, 'Pending') !== false; + } catch (\Exception $e) { + return false; + } + } +} diff --git a/app/Http/Middleware/CheckLandingPageEnabled.php b/app/Http/Middleware/CheckLandingPageEnabled.php new file mode 100644 index 000000000..e3725b4fb --- /dev/null +++ b/app/Http/Middleware/CheckLandingPageEnabled.php @@ -0,0 +1,25 @@ +route()->getName() === 'home') { + return redirect()->route('login'); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/CheckPermission.php b/app/Http/Middleware/CheckPermission.php new file mode 100644 index 000000000..62d2cd78f --- /dev/null +++ b/app/Http/Middleware/CheckPermission.php @@ -0,0 +1,39 @@ +check()) { + return redirect()->route('login'); + } + + $user = auth()->user(); + + // Super admin has all permissions + if ($user->type === 'superadmin' || $user->type === 'super admin') { + return $next($request); + } + + // Check if user has the required permission + if (!$user->hasPermissionTo($permission)) { + if ($request->expectsJson()) { + return response()->json(['message' => 'Forbidden'], 403); + } + + // Redirect to first available page + return redirect()->route('dashboard.redirect'); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/CheckPlanAccess.php b/app/Http/Middleware/CheckPlanAccess.php new file mode 100644 index 000000000..f82f6b1b4 --- /dev/null +++ b/app/Http/Middleware/CheckPlanAccess.php @@ -0,0 +1,60 @@ +user(); + + if (!$user) { + return $next($request); + } + // Super admin has full access + if ($user->isSuperAdmin()) { + return $next($request); + } + + // Only company users need plan checks + if ($user->type !== 'company') { + $company = User::find($user->created_by); + if ($company && $company->type === 'company' && $company->isPlanExpired()) { + auth()->logout(); + return redirect()->route('login')->with('error', __('Access denied. Only company users can access this area.')); + } + } + + // Check if user needs plan subscription + if ($user->needsPlanSubscription()) { + $message = __('Please subscribe to a plan to continue.'); + + if ($user->isTrialExpired()) { + $message = __('Your trial period has expired. Please subscribe to a plan to continue.'); + // Reset trial status + $user->update([ + 'plan_id' => null, + 'is_trial' => null, + 'trial_expire_date' => null, + 'trial_day' => 0 + ]); + } elseif ($user->isPlanExpired()) { + $message = __('Your plan has expired. Please renew your subscription.'); + // Reset expired plan + $user->update([ + 'plan_id' => null, + 'plan_expire_date' => null + ]); + } + + return redirect()->route('plans.index')->with('error', $message); + } + + return $next($request); + } +} diff --git a/app/Http/Middleware/CheckSaas.php b/app/Http/Middleware/CheckSaas.php new file mode 100644 index 000000000..715ce4016 --- /dev/null +++ b/app/Http/Middleware/CheckSaas.php @@ -0,0 +1,20 @@ +isMethod('GET')) { + return $next($request); + } + + // Allow POST requests for creating new data + if ($request->isMethod('POST') && !$this->isUpdateOrDeleteRoute($request)) { + return $next($request); + } + + // Block PUT, PATCH, DELETE requests (editing/deleting existing data) + if (in_array($request->method(), ['PUT', 'PATCH', 'DELETE'])) { + return $this->demoModeResponse($request); + } + + // Block specific update/delete POST routes + if ($this->isUpdateOrDeleteRoute($request)) { + return $this->demoModeResponse($request); + } + + return $next($request); + } + + /** + * Check if the route is for updating or deleting existing data + */ + private function isUpdateOrDeleteRoute(Request $request): bool + { + $route = $request->route(); + if (!$route) return false; + + $routeName = $route->getName(); + $uri = $request->getPathInfo(); + + // Routes that modify existing data + $restrictedPatterns = [ + '/toggle-status', + '/approve', + '/reject', + '/reset-password', + '/upgrade-plan', + '/reply', + '/settings', + '/update', + '/destroy', + '/payment-settings', + 'api/media/batch', + 'switch-business', + '/hr/attendance/clock-in', + '/hr/attendance/clock-out', + '/languages', + '/language', + 'language/save', + ]; + + foreach ($restrictedPatterns as $pattern) { + if (str_contains($uri, $pattern)) { + return true; + } + } + + // Route names that modify existing data + $restrictedRoutePatterns = [ + '.profile.update', + '.update', + '.destroy', + '.toggle-status', + '.approve', + '.reject', + '.reset-password', + '.upgrade-plan', + '.reply', + 'payment.settings', + 'api.media.batch', + 'api.media.destroy', + 'switch-business', + 'hr.attendance.clock-in', + 'hr.attendance.clock-out', + 'hr.payslips.bulk-generate', + 'language', + 'language.save', + 'contacts.send-reply', + 'landing-page.custom-pages.store', + ]; + + if ($routeName) { + foreach ($restrictedRoutePatterns as $pattern) { + if (str_contains($routeName, $pattern)) { + return true; + } + } + } + + return false; + } + + /** + * Return demo mode response + */ + private function demoModeResponse(Request $request): Response + { + $message = 'This action is disabled in demo mode. You can only create new data, not modify existing demo data.'; + + if ($request->expectsJson() || $request->is('api/*')) { + return response()->json([ + 'message' => $message, + 'demo_mode' => true + ], 403); + } + + return redirect()->back()->with('error', $message); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/EnsureEmailIsVerified.php b/app/Http/Middleware/EnsureEmailIsVerified.php new file mode 100644 index 000000000..186e35b68 --- /dev/null +++ b/app/Http/Middleware/EnsureEmailIsVerified.php @@ -0,0 +1,24 @@ +user() && + $request->user() instanceof MustVerifyEmail && + !$request->user()->hasVerifiedEmail()) { + return redirect()->route('verification.notice'); + } + + return $next($request); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/HandleAppearance.php b/app/Http/Middleware/HandleAppearance.php new file mode 100644 index 000000000..f1a02bbc9 --- /dev/null +++ b/app/Http/Middleware/HandleAppearance.php @@ -0,0 +1,23 @@ +cookie('appearance') ?? 'system'); + + return $next($request); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php new file mode 100644 index 000000000..5576db85c --- /dev/null +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -0,0 +1,164 @@ + + */ + public function share(Request $request): array + { + [$message, $author] = str(Inspiring::quotes()->random())->explode('-'); + + // Skip database queries during installation + if ($request->is('install/*') || $request->is('update/*') || !file_exists(storage_path('installed'))) { + // Get available languages even during installation + $languagesFile = resource_path('lang/language.json'); + $availableLanguages = []; + if (file_exists($languagesFile)) { + $availableLanguages = json_decode(file_get_contents($languagesFile), true) ?? []; + } + $globalSettings = [ + 'currencySymbol' => '$', + 'currencyNname' => 'US Dollar', + 'base_url' => config('app.url'), + 'image_url' => config('app.url'), + 'is_demo' => config('app.is_demo', false), + 'is_saas' => isSaas(), + 'availableLanguages' => $availableLanguages, + ]; + + $companySlug = ''; + $checkUser = Auth::user(); + if ($checkUser && $checkUser->hasRole('company')) { + $companySlug = Auth::user()->slug ?? ''; + } else { + $authUser = Auth::user(); + if ($authUser) { + $getCompanyId = getCompanyId($authUser->id); + $getUser = Auth::user()->where('id', $getCompanyId)->first(); + if ($getUser) { + $companySlug = $getUser->slug; + } + } + + } + } else { + // Get system settings + $settings = settings(); + // Get currency symbol + $currencyCode = $settings['defaultCurrency'] ?? 'USD'; + $currency = Currency::where('code', $currencyCode)->first(); + $currencySettings = []; + if ($currency) { + $currencySettings = [ + 'currencySymbol' => $currency->symbol, + 'currencyNname' => $currency->name, + ]; + } else { + $currencySettings = [ + 'currencySymbol' => '$', + 'currencyNname' => 'US Dollar', + ]; + } + + // Get available languages + $languagesFile = resource_path('lang/language.json'); + $availableLanguages = []; + if (file_exists($languagesFile)) { + $availableLanguages = json_decode(file_get_contents($languagesFile), true) ?? []; + } + + // Merge currency settings with other settings + $globalSettings = array_merge($settings, $currencySettings); + $globalSettings['base_url'] = config('app.url'); + $globalSettings['image_url'] = config('app.url'); + $globalSettings['is_demo'] = config('app.is_demo'); + $globalSettings['is_saas'] = isSaas(); + $globalSettings['availableLanguages'] = $availableLanguages; + + $companySlug = ''; + $checkUser = Auth::user(); + if ($checkUser && $checkUser->hasRole('company')) { + $companySlug = Auth::user()->slug ?? ''; + } else { + $authUser = Auth::user(); + if ($authUser) { + $getCompanyId = getCompanyId($authUser->id); + $getUser = Auth::user()->where('id', $getCompanyId)->first(); + if ($getUser) { + $companySlug = $getUser->slug; + } + } + + } + } + + return [ + ...parent::share($request), + 'name' => config('app.name'), + 'base_url' => config('app.url'), + 'image_url' => config('app.url'), + 'quote' => ['message' => trim($message), 'author' => trim($author)], + 'csrf_token' => csrf_token(), + 'auth' => [ + 'user' => $request->user() ? array_merge( + $request->user()->toArray(), + [ + 'avatar' => check_file($request->user()->avatar) ? get_file($request->user()->avatar) : get_file('avatars/avatar.png'), + ] + ) : null, + 'roles' => fn() => $request->user()?->roles->pluck('name'), + 'permissions' => fn() => $request->user()?->getAllPermissions()->pluck('name'), + ], + // 'userLanguage' => $request->user()?->lang ?? 'en', + 'userLanguage' => config('app.is_demo') + ? $request->cookie('app_language') + : ($request->user()?->lang ?? $globalSettings['defaultLanguage'] ?? 'en'), + 'isImpersonating' => session('impersonated_by') ? true : false, + 'ziggy' => fn(): array => [ + ...(new Ziggy)->toArray(), + 'location' => $request->url(), + ], + 'flash' => [ + 'success' => $request->session()->get('success'), + 'error' => $request->session()->get('error'), + ], + 'globalSettings' => $globalSettings, + 'is_demo' => config('app.is_demo'), + 'companySlug' => $companySlug, + ]; + } +} diff --git a/app/Http/Middleware/SettingMiddleware.php b/app/Http/Middleware/SettingMiddleware.php new file mode 100644 index 000000000..0654c88e9 --- /dev/null +++ b/app/Http/Middleware/SettingMiddleware.php @@ -0,0 +1,22 @@ +ensureStorageLink(); + + // Skip during installation + if (!$request->is('install/*') && !$request->is('update/*') && file_exists(storage_path('installed'))) { + // Share settings with all Inertia responses + Inertia::share([ + 'globalSettings' => function () { + return settings(); // Use our helper function + } + ]); + } + + return $next($request); + } + + /** + * Ensure storage symlink exists + */ + private function ensureStorageLink() + { + if (!File::exists(public_path('storage'))) { + try { + Artisan::call('storage:link'); + } catch (\Exception $e) { + // Silently fail if unable to create link + } + } + } +} \ No newline at end of file diff --git a/app/Http/Middleware/SuperAdminMiddleware.php b/app/Http/Middleware/SuperAdminMiddleware.php new file mode 100644 index 000000000..f47bf7f40 --- /dev/null +++ b/app/Http/Middleware/SuperAdminMiddleware.php @@ -0,0 +1,30 @@ +user(); + + if (!$user) { + return redirect()->back()->with('error', 'Unauthorized access'); + } + + // Allow Super Admin in all modes + if ($user->isSuperAdmin()) { + return $next($request); + } + + // Allow Company users only in non-SaaS mode + if ($user->type === 'company' && !isSaas()) { + return $next($request); + } + + return redirect()->back()->with('error', 'Unauthorized access'); + } +} \ No newline at end of file diff --git a/app/Http/Middleware/VerifyCsrfToken.php b/app/Http/Middleware/VerifyCsrfToken.php new file mode 100644 index 000000000..0428f8d5e --- /dev/null +++ b/app/Http/Middleware/VerifyCsrfToken.php @@ -0,0 +1,25 @@ + + */ + protected $except = [ + 'payments/aamarpay/success', + 'payments/aamarpay/callback', + 'payments/tap/success', + 'payments/tap/callback', + 'payments/benefit/success', + 'payments/benefit/callback', + 'payments/easebuzz/success', + 'payments/easebuzz/callback', + 'payments/paytabs/callback' + ]; +} \ No newline at end of file diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php new file mode 100644 index 000000000..ebb5bae9b --- /dev/null +++ b/app/Http/Requests/Auth/LoginRequest.php @@ -0,0 +1,104 @@ +|string> + */ + public function rules(): array + { + $rules = [ + 'email' => ['required', 'string', 'email'], + 'password' => ['required', 'string'], + ]; + + if (getSetting('recaptchaEnabled') && getSetting('recaptchaVersion') == 'v2') { + $rules['recaptcha_token'] = ['required']; + } + + return $rules; + } + + public function messages(): array + { + return [ + 'recaptcha_token.required' => 'Please complete the reCAPTCHA verification.', + ]; + } + + /** + * Attempt to authenticate the request's credentials. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function authenticate(): void + { + $this->ensureIsNotRateLimited(); + if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { + RateLimiter::hit($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.failed'), + ]); + } + // Check if user account is inactive + $user = Auth::user(); + if ($user->status === 'inactive') { + Auth::logout(); + throw ValidationException::withMessages([ + 'email' => __('Your account is inactive. Please contact administrator.'), + ]); + } + RateLimiter::clear($this->throttleKey()); + } + + /** + * Ensure the login request is not rate limited. + * + * @throws \Illuminate\Validation\ValidationException + */ + public function ensureIsNotRateLimited(): void + { + if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { + return; + } + + event(new Lockout($this)); + + $seconds = RateLimiter::availableIn($this->throttleKey()); + + throw ValidationException::withMessages([ + 'email' => __('auth.throttle', [ + 'seconds' => $seconds, + 'minutes' => ceil($seconds / 60), + ]), + ]); + } + + /** + * Get the rate limiting throttle key for the request. + */ + public function throttleKey(): string + { + return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip()); + } +} diff --git a/app/Http/Requests/BranchRequest.php b/app/Http/Requests/BranchRequest.php new file mode 100644 index 000000000..3a8062d0d --- /dev/null +++ b/app/Http/Requests/BranchRequest.php @@ -0,0 +1,74 @@ +isMethod('POST')) { + return $user->hasPermissionTo('create-branch'); + } + + if ($this->isMethod('PUT') || $this->isMethod('PATCH')) { + return $user->hasPermissionTo('edit-branch'); + } + + return false; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + $user = Auth::user(); + $createdBy = $user->type === 'company' ? $user->id : $user->created_by; + + $rules = [ + 'name' => 'required|string|max:255', + 'location' => 'required|string', + 'phone' => 'nullable|string|max:20', + 'email' => 'nullable|email|max:255', + 'branch_head' => 'nullable|string|max:255', + 'status' => 'required|in:active,inactive', + ]; + + // For create request + if ($this->isMethod('POST')) { + $rules['code'] = [ + 'required', + 'string', + 'max:50', + Rule::unique('branches')->where(function ($query) use ($createdBy) { + return $query->where('created_by', $createdBy); + }) + ]; + } + + // For update request + if ($this->isMethod('PUT') || $this->isMethod('PATCH')) { + $rules['code'] = [ + 'required', + 'string', + 'max:50', + Rule::unique('branches')->where(function ($query) use ($createdBy) { + return $query->where('created_by', $createdBy); + })->ignore($this->branch->id) + ]; + } + + return $rules; + } +} \ No newline at end of file diff --git a/app/Http/Requests/CategoryRequest.php b/app/Http/Requests/CategoryRequest.php new file mode 100644 index 000000000..a75d56491 --- /dev/null +++ b/app/Http/Requests/CategoryRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'description' => 'required|string', + 'image' => 'nullable|image|mimes:jpeg,png,jpg,gif|max:2048', + ]; + } +} diff --git a/app/Http/Requests/CouponRequest.php b/app/Http/Requests/CouponRequest.php new file mode 100644 index 000000000..0057ac045 --- /dev/null +++ b/app/Http/Requests/CouponRequest.php @@ -0,0 +1,74 @@ +|string> + */ + public function rules(): array + { + $couponId = $this->route('coupon') ? $this->route('coupon')->id : null; + + return [ + 'name' => 'required|string|max:255', + 'type' => 'required|in:percentage,flat', + 'minimum_spend' => 'nullable|numeric|min:0', + 'maximum_spend' => 'nullable|numeric|min:0|gte:minimum_spend', + 'discount_amount' => [ + 'required', + 'numeric', + 'min:0', + function ($attribute, $value, $fail) { + if ($this->type === 'percentage' && $value > 99) { + $fail('The discount amount cannot exceed 99% for percentage discounts.'); + } + } + ], + 'use_limit_per_coupon' => 'nullable|integer|min:1', + 'use_limit_per_user' => 'nullable|integer|min:1', + 'expiry_date' => 'nullable|date|after:today', + 'code' => [ + 'required_if:code_type,manual', + 'string', + 'max:50', + Rule::unique('coupons', 'code')->ignore($couponId) + ], + 'code_type' => 'required|in:manual,auto', + 'status' => 'boolean' + ]; + } + + /** + * Get custom messages for validator errors. + */ + public function messages(): array + { + return [ + 'name.required' => 'The coupon name is required.', + 'type.required' => 'The discount type is required.', + 'type.in' => 'The discount type must be either percentage or flat amount.', + 'discount_amount.required' => 'The discount amount is required.', + 'discount_amount.min' => 'The discount amount must be greater than 0.', + 'maximum_spend.gte' => 'The maximum spend must be greater than or equal to minimum spend.', + 'expiry_date.after' => 'The expiry date must be a future date.', + 'code.required_if' => 'The coupon code is required when manual entry is selected.', + 'code.unique' => 'This coupon code is already taken.', + 'code_type.required' => 'Please select a code generation method.', + ]; + } +} diff --git a/app/Http/Requests/PermissionRequest.php b/app/Http/Requests/PermissionRequest.php new file mode 100644 index 000000000..82d6e1b64 --- /dev/null +++ b/app/Http/Requests/PermissionRequest.php @@ -0,0 +1,29 @@ +|string> + */ + public function rules(): array + { + return [ + 'module' => 'required|string', + 'label' => 'required|string', + 'description' => 'nullable|string', + ]; + } +} diff --git a/app/Http/Requests/ProductFormRequest.php b/app/Http/Requests/ProductFormRequest.php new file mode 100644 index 000000000..0a8294826 --- /dev/null +++ b/app/Http/Requests/ProductFormRequest.php @@ -0,0 +1,54 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => 'required|string|max:255', + 'description' => 'required|string|max:1000', + 'price' => 'required|numeric|min:0', + 'category_id' => 'nullable|exists:categories,id', + 'featured_image' => 'nullable|mimes:jpeg,png,jpg,gif,webp,avif|max:2048', + ]; + } + + /** + * Function: messages + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Please enter the product name.', + 'name.string' => 'The product name must be a string.', + 'name.max' => 'The product name may not be greater than 255 characters.', + 'description.required' => 'Please enter product description.', + 'description.string' => 'The product description must be a string.', + 'description.max' => 'The product description may not be greater than 1000 characters.', + 'price.required' => 'Please enter the product price.', + 'price.numeric' => 'The product price must be a number.', + 'price.min' => 'The product price must be at least 0.', + 'category_id.exists' => 'The selected category does not exist.', + 'featured_image.image' => 'The featured image must be an image file.', + 'featured_image.mimes' => 'The featured image must be a file of type: jpeg, png, jpg, gif, webp.', + 'featured_image.max' => 'The featured image may not be greater than 2048 KB.', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Requests/RoleRequest.php b/app/Http/Requests/RoleRequest.php new file mode 100644 index 000000000..86c3d4907 --- /dev/null +++ b/app/Http/Requests/RoleRequest.php @@ -0,0 +1,93 @@ + [ + 'required', + 'string', + function ($attribute, $value, $fail) { + $this->validateSystemRole($value, $fail); + } + ], + 'description' => 'nullable|string', + 'permissions' => 'required|array' + ]; + } + + + private function validatePermissionAccess($permissionName, $fail) + { + $user = Auth::user(); + $userType = $user->type ?? 'company'; + + // Superadmin can assign any permission + if ($userType === 'superadmin' || $userType === 'super admin') { + return; + } + + // Get allowed modules for current user role + $allowedModules = config('role-permissions.' . $userType, config('role-permissions.company')); + + // Check if permission belongs to allowed module + $permission = Permission::where('name', $permissionName)->first(); + + if (!$permission) { + $fail('Permission does not exist.'); + return; + } + + // Skip validation if permission module is not in allowed modules (commented out) + if (!in_array($permission->module, $allowedModules)) { + return; // Allow but will be filtered out by controller + } + + // For company users, validate settings permissions + if ($userType === 'company' && $permission->module === 'settings') { + $allowedSettingsPermissions = [ + 'manage-email-settings', + 'manage-system-settings', + 'manage-brand-settings' + ]; + + if (!in_array($permissionName, $allowedSettingsPermissions)) { + $fail('You are not authorized to assign this permission.'); + } + } + } + + + private function validateSystemRole($label, $fail) + { + $user = Auth::user(); + $userType = $user->type ?? 'company'; + + // Superadmin can create/edit any role + if ($userType === 'superadmin' || $userType === 'super admin') { + return; + } + + $systemRoles = ['superadmin', 'super admin', 'company']; + $slug = \Illuminate\Support\Str::slug($label); + + if ( + in_array(strtolower($label), array_map('strtolower', $systemRoles)) || + in_array($slug, $systemRoles) + ) { + $fail('This role name is reserved for system use. Please choose a different name.'); + } + } +} diff --git a/app/Http/Requests/Settings/ProfileUpdateRequest.php b/app/Http/Requests/Settings/ProfileUpdateRequest.php new file mode 100644 index 000000000..42d7690b1 --- /dev/null +++ b/app/Http/Requests/Settings/ProfileUpdateRequest.php @@ -0,0 +1,35 @@ +|string> + */ + public function rules(): array + { + return [ + 'name' => ['required', 'string', 'max:255'], + + 'email' => [ + 'required', + 'string', + 'lowercase', + 'email', + 'max:255', + Rule::unique(User::class)->ignore($this->user()->id), + ], + + 'avatar' => ['nullable', 'image', 'mimes:jpeg,png,jpg,gif'], + '_method' => ['sometimes', 'string', 'in:PATCH'], + ]; + } +} diff --git a/app/Http/Requests/UserRequest.php b/app/Http/Requests/UserRequest.php new file mode 100644 index 000000000..f36e09658 --- /dev/null +++ b/app/Http/Requests/UserRequest.php @@ -0,0 +1,35 @@ +|string> + */ + public function rules(): array + { + $userId = $this->route('user') ? $this->route('user')->id : null; + + return [ + 'name' => 'required|string', + 'email' => 'required|email|unique:users,email' . ($userId ? ',' . $userId : ''), + 'password' => $this->isMethod('POST') ? 'required|string|min:6' : 'nullable|string|min:6', + 'password_confirmation' => $this->isMethod('POST') ? 'required|same:password' : 'nullable|same:password', + 'roles' => 'required' + ]; + } +} diff --git a/app/Libraries/Coingate/Coingate.php b/app/Libraries/Coingate/Coingate.php new file mode 100755 index 000000000..976c4fdb8 --- /dev/null +++ b/app/Libraries/Coingate/Coingate.php @@ -0,0 +1,84 @@ + 1, + CURLOPT_URL => $url + ); + + if ($method == 'POST') { + $headers[] = 'Content-Type: application/x-www-form-urlencoded'; + $curl_options[CURLOPT_POST] = 1; + curl_setopt($curl, CURLOPT_POSTFIELDS, http_build_query($post_params)); + } + + curl_setopt_array($curl, $curl_options); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_USERAGENT, $user_agent); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, $curlopt_ssl_verifypeer); + + // Debug logging + \Log::info('CoinGate Request Debug', [ + 'url' => $url, + 'headers' => $headers, + 'auth_token' => substr(self::$auth_token, 0, 10) . '...', + 'environment' => self::$environment + ]); + + $raw_response = curl_exec($curl); + $decoded_response = json_decode($raw_response, true); + $response = $decoded_response ? $decoded_response : $raw_response; + $http_status = curl_getinfo($curl, CURLINFO_HTTP_CODE); + + curl_close($curl); + + if ($method == 'GET') { + return $response; + } else { + return ['response' => $response, 'status_code' => $http_status]; + } + } +} \ No newline at end of file diff --git a/app/Libraries/Easebuzz/easebuzz_payment_gateway.php b/app/Libraries/Easebuzz/easebuzz_payment_gateway.php new file mode 100755 index 000000000..802a3261a --- /dev/null +++ b/app/Libraries/Easebuzz/easebuzz_payment_gateway.php @@ -0,0 +1,407 @@ +MERCHANT_KEY = $key; + $this->SALT = $salt; + $this->ENV = $env; + } + + + /* + * initiatePaymentAPI function to integrate easebuzz for payment. + * + * http method used - POST + * + * param string $txnid - holds the transaction id (which is auto generate using hash) + * param array $params - holds the $_POST data which is pass from the html form. + * + * ##Return values + * + * - return array ApiResponse['status']== 1 means successful. + * + * - return array ApiResponse['status']== 0 means error. + * + * @param array $params - holds the $_POST data which is pass from the html form. + * + * @return array ApiResponse['status']== 1 successful. + * @return array ApiResponse['status']== 0 error. + * + * ##Helper methods for initiate payment(payment.php) + * + * - initiate_payment(arg1, arg2, arg3, arg4) :- call all method initiate payment and dispaly payment page. + * + * - _payment(arg1, arg2, arg3, arg4) :- use for initiate payment. + * + * - _paymentResponse(arg1) :- use for show api response (like error, payment page etc.). + * + * - _checkArgumentValidation(arg1, arg2, arg3, arg4) :- check no. of argument validation. + * + * - _removeSpaceAndPreparePostArray(arg1) :- remove space, anonymous tag from the $_POST and prepare array. + * + * - _typeValidation(arg1, arg2, arg3) :- check type validation (like amount shoud be float etc). + * + * - _emptyValidation(arg1, arg2) :- check empty validation for Mandatory Parameters. + * + * - _email_validation(arg1) :- check email format validation. + * + * - _getURL(arg1) :- get URL based on set enviroment. + * + * - _pay(arg1, arg2, arg3) :- initiate payment. + * + * ## below method call from _pay() method. + * + * -- _getHashKey(arg1, arg2) :- generate hash key based on hash sequence. + * + * -- _curlCall(arg1, arg2) :- initiate pay link. + * + * ## below method call from _curlCall() method. + * + * Note :- Before call below method, install cURL. if cURL is already installed the go ahead. + * + * --- curl_init() :- Initializes a new session and return a cURL. + * + * --- curl_setopt_array(arg1, arg2) :- Set multiple options for a cURL transfer. + * + * --- curl_exec(arg1) :- Perform a cURL session. + * + * --- curl_errno(arg1) :- check there is any error or not in curl execution. + * + */ + public function initiatePaymentAPI($params, $redirect=True){ + // include file + include_once('payment.php'); + + // generate transaction ID and push into $params array + // $txnid = substr(hash('sha256', mt_rand() . microtime()), 0, 20); + // $params['txnid'] = $txnid; + return initiate_payment($params, $redirect, $this->MERCHANT_KEY, $this->SALT, $this->ENV); + } + + /* + * transactionAPI function to query for single transaction + * + * http method used - POST + * + * param array $params - holds the $_POST data which is pass from the html form. + * + * ##Return values + * + * - return array ApiResponse['status']== 1 means successful. + * + * - return array ApiResponse['status']== 0 means error. + * + * @param array $params - holds the $_POST data which is pass from the html form. + * + * @return array ApiResponse['status']== 1 successful. + * @return array ApiResponse['status']== 0 error. + * + * ##Helper methods for initiate transaction(transaction.php) + * + * - get_transaction_details(arg1, arg2, arg3, arg4) :- call all method initiate transaction. + * + * - _transaction(arg1, arg2, arg3, arg4) :- use for initiate transaction. + * + * - _transactionResponse(arg1, arg2) :- use for verify api response is acceptable or not. + * + * - _checkArgumentValidation(arg1, arg2, arg3, arg4) :- check no. of argument validation. + * + * - _removeSpaceAndPreparePostArray(arg1) :- remove space, anonymous tag from the $_POST and prepare array. + * + * - _typeValidation(arg1, arg2, arg3) :- check type validation (like amount shoud be float etc). + * + * - _emptyValidation(arg1, arg2) :- check empty validation for Mandatory Parameters. + * + * - _email_validation(arg1) :- check email format validation. + * + * - _getURL(arg1) :- get URL based on set enviroment. + * + * - _getTransaction(arg1, arg2, arg3) :- initiate transaction. + * + * ## below method call from _getTransaction() method. + * + * -- _getHashKey(arg1, arg2) :- generate hash key based on hash sequence. + * + * -- _curlCall(arg1, arg2) :- initiate pay link. + * + * ## below method call from _curlCall() method. + * + * Note :- Before call below method, install cURL. if cURL is already installed the go ahead. + * + * --- curl_init() :- Initializes a new session and return a cURL. + * + * --- curl_setopt_array(arg1, arg2) :- Set multiple options for a cURL transfer. + * + * --- curl_exec(arg1) :- Perform a cURL session. + * + * --- curl_errno(arg1) :- check there is any error or not in curl execution. + * + * ## below method call from _transactionResponse() method. + * + * -- _getReverseHashKey(arg1, arg2) :- generate reverse hash key for response verification. + * + */ + public function transactionAPI($params){ + // include file + include_once('transaction.php'); + $result = get_transaction_details($params, $this->MERCHANT_KEY, $this->SALT, $this->ENV); + return json_encode($result); + } + + + /* + * transactionDateAPI function to transaction based on date. + * + * http method used - POST + * + * param array $params - holds the $_POST data which is pass from the html form. + * + * ##Return values + * + * - return array ApiResponse['status']== 1 means successful. + * + * - return array ApiResponse['status']== 0 means error. + * + * @param array $params - holds the $_POST data which is pass from the html form. + * + * @return array ApiResponse['status']== 1 successful. + * @return array ApiResponse['status']== 0 error. + * + * ##Helper methods for initiate date transaction(transaction_date.php) + * + * - get_transactions_by_date(arg1, arg2, arg3, arg4) :- call all method initiate date transaction. + * + * - _date_transaction(arg1, arg2, arg3, arg4) :- use for initiate date transaction. + * + * - _checkArgumentValidation(arg1, arg2, arg3, arg4) :- check no. of argument validation. + * + * - _removeSpaceAndPreparePostArray(arg1) :- remove space, anonymous tag from the $_POST and prepare array. + * + * - _typeValidation(arg1, arg2, arg3) :- check type validation (like amount shoud be float etc). + * + * - _emptyValidation(arg1, arg2) :- check empty validation for Mandatory Parameters. + * + * - _email_validation(arg1) :- check email format validation. + * + * - _getURL(arg1) :- get URL based on set enviroment. + * + * - _getDateTransaction(arg1, arg2, arg3) :- initiate date transaction. + * + * ## below method call from _getDateTransaction() method. + * + * -- _getHashKey(arg1, arg2) :- generate hash key based on hash sequence. + * + * -- _curlCall(arg1, arg2) :- initiate pay link. + * + * ## below method call from _curlCall() method. + * + * Note :- Before call below method, install cURL. if cURL is already installed the go ahead. + * + * --- curl_init() :- Initializes a new session and return a cURL. + * + * --- curl_setopt_array(arg1, arg2) :- Set multiple options for a cURL transfer. + * + * --- curl_exec(arg1) :- Perform a cURL session. + * + * --- curl_errno(arg1) :- check there is any error or not in curl execution. + * + */ + public function transactionDateAPI($params){ + // include file + include_once('transaction_date.php'); + $result = get_transactions_by_date($params, $this->MERCHANT_KEY, $this->SALT, $this->ENV); + return json_encode($result); + } + + + /* + * refundAPI function to refund for the transaction + * + * http method used - POST + * + * param array $params - holds the $_POST data which is pass from the html form. + * + * ##Return values + * + * - return array ApiResponse['status']== 1 means successful. + * + * - return array ApiResponse['status']== 0 means error. + * + * @param array $params - holds the $_POST data which is pass from the html form. + * + * @return array ApiResponse['status']== 1 successful. + * @return array ApiResponse['status']== 0 error. + * + * ##Helper methods for initiate refund (refund.php) + * + * - initiate_refund(arg1, arg2, arg3, arg4) :- call all method initiate refund. + * + * - _refund(arg1, arg2, arg3, arg4) :- use for initiate refund. + * + * - _checkArgumentValidation(arg1, arg2, arg3, arg4) :- check no. of argument validation. + * + * - _removeSpaceAndPreparePostArray(arg1) :- remove space, anonymous tag from the $_POST and prepare array. + * + * - _typeValidation(arg1, arg2, arg3) :- check type validation (like amount shoud be float etc). + * + * - _emptyValidation(arg1, arg2) :- check empty validation for Mandatory Parameters. + * + * - _email_validation(arg1) :- check email format validation. + * + * - _getURL(arg1) :- get URL based on set enviroment. + * + * - _refundPayment(arg1, arg2, arg3) :- initiate refund. + * + * ## below method call from _refundPayment() method. + * + * -- _getHashKey(arg1, arg2) :- generate hash key based on hash sequence. + * + * -- _curlCall(arg1, arg2) :- initiate pay link. + * + * ## below method call from _curlCall() method. + * + * Note :- Before call below method, install cURL. if cURL is already installed the go ahead. + * + * --- curl_init() :- Initializes a new session and return a cURL. + * + * --- curl_setopt_array(arg1, arg2) :- Set multiple options for a cURL transfer. + * + * --- curl_exec(arg1) :- Perform a cURL session. + * + * --- curl_errno(arg1) :- check there is any error or not in curl execution. + * + */ + public function refundAPI($params){ + // include file + include_once('refund.php'); + $result = initiate_refund($params, $this->MERCHANT_KEY, $this->SALT, $this->ENV); + return json_encode($result); + } + + + /* + * payoutAPI function to payout for particular date. + * + * http method used - POST + * + * param array $params - holds the $_POST data which is pass from the html form. + * + * ##Return values + * + * - return array ApiResponse['status']== 1 means successful. + * + * - return array ApiResponse['status']== 0 means error. + * + * @param array $params - holds the $_POST data which is pass from the html form. + * + * @return array ApiResponse['status']== 1 successful. + * @return array ApiResponse['status']== 0 error. + * + * ##Helper methods for initiate payout (payout.php) + * + * - get_payout_details_by_date(arg1, arg2, arg3, arg4) :- call all method initiate payout. + * + * - _payout(arg1, arg2, arg3, arg4) :- use for initiate payout. + * + * - _checkArgumentValidation(arg1, arg2, arg3, arg4) :- check no. of argument validation. + * + * - _removeSpaceAndPreparePostArray(arg1) :- remove space, anonymous tag from the $_POST and prepare array. + * + * - _typeValidation(arg1, arg2, arg3) :- check type validation (like amount shoud be float etc). + * + * - _emptyValidation(arg1, arg2) :- check empty validation for Mandatory Parameters. + * + * - _email_validation(arg1) :- check email format validation. + * + * - _getURL(arg1) :- get URL based on set enviroment. + * + * - _payoutPayment(arg1, arg2, arg3) :- initiate payout payment. + * + * ## below method call from _payoutPayment() method. + * + * -- _getHashKey(arg1, arg2) :- generate hash key based on hash sequence. + * + * -- _curlCall(arg1, arg2) :- initiate pay link. + * + * ## below method call from _curlCall() method. + * + * Note :- Before call below method, install cURL. if cURL is already installed the go ahead. + * + * --- curl_init() :- Initializes a new session and return a cURL. + * + * --- curl_setopt_array(arg1, arg2) :- Set multiple options for a cURL transfer. + * + * --- curl_exec(arg1) :- Perform a cURL session. + * + * --- curl_errno(arg1) :- check there is any error or not in curl execution. + * + */ + public function payoutAPI($params){ + // include file + include_once('payout.php'); + $result = get_payout_details_by_date($params, $this->MERCHANT_KEY, $this->SALT, $this->ENV); + return json_encode($result); + } + + + /* + * easebuzzResponse mehod to verify easebuzz API response is acceptable or not. + * + * http method used - POST + * + * - params array $params - holds the API response array. + * + * ##Return values + * + * - return array $result- holds the API response array after verification of response. + * + * @params array $params - holds the API response array. + * + * @return array $result- holds the API response array after verification of response. + * + * ##Helper methods for display API response(payment.php) + * + * - response(arg1, arg2) :- verify API response and retrun response array. + * + * - _removeSpaceAndPrepareAPIResponseArray(arg1) :- remove space, anonymous tag from the API response + * array and prepare API response array. + * + * - _emptyValidation(arg1, arg2) :- check empty validation in API response array. + * + * - _getResponse(arg1, arg2) :- check response is correct or not. + * + * ## below method call from _getResponse() method. + * + * -- _getReverseHashKey(arg1, arg2) :- generate reverse hash key for validation. + * + */ + public function easebuzzResponse($params){ + // include file + include_once('payment.php'); + + $result = easebuzz_response($params, $this->SALT); + + return json_encode($result); + } + + } +?> + + diff --git a/app/Libraries/Easebuzz/payment.php b/app/Libraries/Easebuzz/payment.php new file mode 100755 index 000000000..385ee7ba4 --- /dev/null +++ b/app/Libraries/Easebuzz/payment.php @@ -0,0 +1,894 @@ +status==1){ + + $iframe_result = array( + "status"=>$result->status, + 'key' => $merchant_key, + 'access_key' => $result->data, + ); + + return json_encode($iframe_result); + } + else{ + return json_encode($result); + } + } + + } + + +/* + * _payment method use for initiate payment. + * + * param string $key - holds the merchant key. + * param string $txnid - holds the transaction id. + * param string $firstname - holds the first name. + * param string $email - holds the email. + * param string $amount - holds the amount. + * param string $phone - holds the phone. + * param string $hash - holds the hash key. + * param string $productInfo - holds the product information. + * param string $successURL - holds the success URL. + * param string $failureURL - holds the failure URL. + * param string $udf1 - holds the udf1. + * param string $udf2 - holds the udf2. + * param string $udf3 - holds the udf3. + * param string $udf4 - holds the udf4. + * param string $udf5 - holds the udf5. + * param string $address1 - holds the first address. + * param string $address2 - holds the second address. + * param string $city - holds the city. + * param string $state - holds the state. + * param string $country - holds the country. + * param string $zipcode - holds the zipcode. + * + * #### Define variable + * + * $postedArray array - holds merchant key and $_POST form data. + * $URL string - holds url based on the $env(enviroment : 'test' or 'prod') + * + * ##Return values + * + * - return array $pay_result - holds the response with status and data. + * + * - return integer status = 1 successful. + * + * - return integer status = 0 error. + * + * @param string $key - holds the merchant key. + * @param string $txnid - holds the transaction id. + * @param string $firstname - holds the first name. + * @param string $email - holds the email. + * @param string $amount - holds the amount. + * @param string $phone - holds the phone. + * @param string $hash - holds the hash key. + * @param string $productInfo - holds the product information. + * @param string $successURL - holds the success URL. + * @param string $failureURL - holds the failure URL. + * @param string $udf1 - holds the udf1. + * @param string $udf2 - holds the udf2. + * @param string $udf3 - holds the udf3. + * @param string $udf4 - holds the udf4. + * @param string $udf5 - holds the udf5. + * @param string $address1 - holds the first address. + * @param string $address2 - holds the second address. + * @param string $city - holds the city. + * @param string $state - holds the state. + * @param string $country - holds the country. + * @param string $zipcode - holds the zipcode. + * + * @return array $pay_result - holds the response with status and data. + * @return integer status = 1 successful. + * @return integer status = 0 error. + * + */ + function _payment($params, $redirect, $merchant_key, $salt, $env){ + + $postedArray = ''; + $URL = ''; + + // argument validation + $argument_validation = _checkArgumentValidation($params, $merchant_key, $salt, $env); + if (is_array($argument_validation) && $argument_validation['status'] === 0) { + return $argument_validation; + } + + // push merchant key into $params array. + $params['key'] = $merchant_key; + + // remove white space, htmlentities(converts characters to HTML entities), prepared $postedArray. + $postedArray = _removeSpaceAndPreparePostArray($params); + + // empty validation + $empty_validation = _emptyValidation($postedArray, $salt); + if (is_array($empty_validation) && $empty_validation['status'] === 0) { + return $empty_validation; + } + + // check amount should be float or not + if (preg_match("/^([\d]+)\.([\d]?[\d])$/", $postedArray['amount'])) { + $postedArray['amount'] = (float) $postedArray['amount']; + } + + // type validation + $type_validation = _typeValidation($postedArray, $salt, $env); + if ($type_validation !== true) { + return $type_validation; + } + + // again amount convert into string + $diff_amount_string = abs(strlen($params['amount']) - strlen("" . $postedArray['amount'] . "")); + $diff_amount_string = ($diff_amount_string === 2) ? 1 : 2; + $postedArray['amount'] = sprintf("%." . $diff_amount_string . "f", $postedArray['amount']); + + // email validation + $email_validation = _email_validation($postedArray['email']); + if ($email_validation !== true) + return $email_validation; + + // get URL based on enviroment like ($env = 'test' or $env = 'prod') + $URL = _getURL($env); + + // process to start pay + $pay_result = _pay($postedArray, $redirect, $salt, $URL); + + return $pay_result; + } + + +/* + * _checkArgumentValidation method Check number of Arguments Validation. Means how many arguments submitted + * from form and verify with + * API documentation. + * + * param array $params - holds the all $_POST data. + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return interger 1 number of arguments match. + * + * - return array status = 0 number of arguments mismatch. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return interger 1 number of arguments match. + * @return array status = 0 number of arguments mismatch. + * + */ + function _checkArgumentValidation($params, $merchant_key, $salt, $env){ + $args = func_get_args(); + $argsc = count($args); + if ($argsc !== 4) { + return array( + 'status' => 0, + 'data' => 'Invalid number of arguments.' + ); + } + return 1; + } + + +/* + * _removeSpaceAndPreparePostArray method Remove white space, converts characters to HTML entities + * and prepared the posted array. + * + * param array $params - holds $_POST array, merchant key and transaction key. + * + * ##Return values + * + * - return array $temp_array - holds the all posted value after removing space. + * + * @param array $params - holds $_POST array, merchant key and transaction key. + * + * @return array $temp_array - holds the all posted value after removing space.c + * + */ + function _removeSpaceAndPreparePostArray($params){ + /* + $temp_array = array( + 'key' => trim(htmlentities($params['key'], ENT_QUOTES)), + 'txnid' => trim(htmlentities($params['txnid'], ENT_QUOTES)), + 'amount' => trim(htmlentities($params['amount'], ENT_QUOTES)), + 'firstname' => trim(htmlentities($params['firstname'], ENT_QUOTES)), + 'email' => trim(htmlentities($params['email'], ENT_QUOTES)), + 'phone' => trim(htmlentities($params['phone'], ENT_QUOTES)), + 'udf1' => trim(htmlentities($params['udf1'], ENT_QUOTES)), + 'udf2' => trim(htmlentities($params['udf2'], ENT_QUOTES)), + 'udf3' => trim(htmlentities($params['udf3'], ENT_QUOTES)), + 'udf4' => trim(htmlentities($params['udf4'], ENT_QUOTES)), + 'udf5' => trim(htmlentities($params['udf5'], ENT_QUOTES)), + 'productinfo' => trim(htmlentities($params['productinfo'], ENT_QUOTES)), + 'surl' => trim(htmlentities($params['surl'], ENT_QUOTES)), + 'furl' => trim(htmlentities($params['furl'], ENT_QUOTES)), + 'address1' => trim(htmlentities($params['address1'], ENT_QUOTES)), + 'address2' => trim(htmlentities($params['address2'], ENT_QUOTES)), + 'city' => trim(htmlentities($params['city'], ENT_QUOTES)), + 'state' => trim(htmlentities($params['state'], ENT_QUOTES)), + 'country' => trim(htmlentities($params['country'], ENT_QUOTES)), + 'zipcode' => trim(htmlentities($params['zipcode'], ENT_QUOTES)) + ); + + if (array_key_exists("sub_merchant_id", $params) and !empty($params['sub_merchant_id']) ) + $temp_array['sub_merchant_id'] = trim( htmlentities($params['sub_merchant_id'], ENT_QUOTES) ); + + if (array_key_exists("unique_id", $params) and !empty($params['unique_id']) ) + $temp_array['unique_id'] = trim( htmlentities($params['unique_id'], ENT_QUOTES) ); + + if (array_key_exists("split_payments", $params) and !empty($params['split_payments']) ) + $temp_array['split_payments'] = trim($params['split_payments']); + + if (array_key_exists("show_payment_mode", $params) and !empty($params['show_payment_mode']) ) + $temp_array['show_payment_mode'] = trim($params['show_payment_mode']); + */ + $temp_array = array(); + foreach ($params as $key => $value) { + if (array_key_exists($key, $params) and !empty($key) ){ + if($key != "split_payments"){ + $temp_array[$key] = trim(htmlentities($value, ENT_QUOTES)); + }else{ + $temp_array[$key] = trim($value); + } + } + } + return $temp_array; + } + + +/* + * _emptyValidation method check empty validation for Mandatory Parameters. + * + * param array $params - holds the all $_POST data + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all $params Mandatory parameters is not empty. + * + * - return array with status and data - $params parameters or $salt are empty. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all $params Mandatory parameters is not empty. + * @return array with status and data - $params parameters or $salt are empty. + * + */ + function _emptyValidation($params, $salt){ + $empty_value = false; + if (empty($params['key'])) + $empty_value = 'Merchant Key'; + + if (empty($params['txnid'])) + $empty_value = 'Transaction ID'; + + if (empty($params['amount'])) + $empty_value = 'Amount'; + + if (empty($params['firstname'])) + $empty_value = 'First Name'; + + if (empty($params['email'])) + $empty_value = 'Email'; + + if (empty($params['phone'])) + $empty_value = 'Phone'; + + if (!empty($params['phone'])){ + if (strlen((string)$params['phone'])!=10){ + $empty_value = 'Phone number must be 10 digit and '; + } + } + + + if (empty($params['productinfo'])) + $empty_value = 'Product Infomation'; + + if (empty($params['surl'])) + $empty_value = 'Success URL'; + + if (empty($params['furl'])) + $empty_value = 'Failure URL'; + + if (empty($salt)) + $empty_value = 'Merchant Salt Key'; + + if ($empty_value !== false) { + return array( + 'status' => 0, + 'data' => 'Mandatory Parameter ' . $empty_value . ' can not empty' + ); + } + return true; + } + + +/* + * _typeValidation method check type validation for field. + * + * param array $params - holds the all $_POST data. + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all params parameters type are correct. + * + * - return array with status and data - params parameters type mismatch. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all params parameters type are correct. + * @return array with status and data - params parameters type mismatch. + * + */ + function _typeValidation($params, $salt, $env){ + $type_value = false; + if (!is_string($params['key'])) + $type_value = "Merchant Key should be string"; + + if (!is_float($params['amount'])) + $type_value = "The amount should float up to two or one decimal."; + + if (!is_string($params['productinfo'])) + $type_value = "Product Information should be string"; + + if (!is_string($params['firstname'])) + $type_value = "First Name should be string"; + + if (!is_string($params['phone'])) + $type_value = "Phone Number should be number"; + + if (!is_string($params['email'])) + $type_value = "Email should be string"; + + if (!is_string($params['surl'])) + $type_value = "Success URL should be string"; + + if (!is_string($params['furl'])) + $type_value = "Failure URL should be string"; + + if ($type_value !== false) { + return array( + 'status' => 0, + 'data' => $type_value + ); + } + return true; + } + + +/* + * _email_validation method check email format validation + * + * param string $email - holds the email address. + * + * ##Return values + * + * - return boolean true - email format is correct. + * + * - return array with status and data - email format is incorrect. + * + * @param string $email - holds the email address. + * + * @return boolean true - email format is correct. + * @return array with status and data - email format is incorrect. + * + */ + function _email_validation($email){ + $email_regx = "/^([\w\.-]+)@([\w-]+)\.([\w]{2,8})(\.[\w]{2,8})?$/"; + if (!preg_match($email_regx, $email)) { + return array( + 'status' => 0, + 'data' => 'Email invalid, Please enter valid email.' + ); + } + return true; + } + + +/* + * _getURL method set based on enviroment ($env = 'test' or $env = 'prod') and + * generate url link. + * + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return string $url_link - holds the full url link. + * + * @param string $env - holds the enviroment. + * + * @return string $url_link - holds the full URL. + * + */ + function _getURL($env){ + $url_link = ''; + switch ($env) { + case 'test': + $url_link = "https://testpay.easebuzz.in/"; + break; + case 'prod': + $url_link = 'https://pay.easebuzz.in/'; + break; + case 'local': + $url_link = 'http://localhost:8005/'; + break; + case 'dev': + $url_link = 'https://devpay.easebuzz.in/'; + break; + default: + $url_link = "https://testpay.easebuzz.in/"; + } + return $url_link; + } + + +/* + * _pay method initiate payment will be start from here. + * + * params array $params_array - holds all form data with merchant key, transaction id etc. + * params string $salt_key - holds the merchant salt key. + * params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * param string $key - holds the merchant key. + * param string $txnid - holds the transaction id. + * param string $firstname - holds the first name. + * param string $email - holds the email. + * param float $amount - holds the amount. + * param string $phone - holds the phone. + * param string $hash - holds the hash key. + * param string $productInfo - holds the product information. + * param string $successURL - holds the success URL. + * param string $failureURL - holds the failure URL. + * param string $udf1 - holds the udf1. + * param string $udf2 - holds the udf2. + * param string $udf3 - holds the udf3. + * param string $udf4 - holds the udf4. + * param string $udf5 - holds the udf5. + * param string $address1 - holds the first address. + * param string $address2 - holds the second address. + * param string $city - holds the city. + * param string $state - holds the state. + * param string $country - holds the country. + * param string $zipcode - holds the zipcode. + * + * ##Return values + * + * - return array with status and data - holds the details + * + * - return integer status = 0 means error. + * + * - return integer status = 1 means success and go the url link. + * + * @params array $params_array - holds all form data with merchant key, transaction id etc. + * @params string $salt_key - holds the merchant salt key. + * @params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * @param string $key - holds the merchant key. + * @param string $txnid - holds the transaction id. + * @param string $firstname - holds the first name. + * @param string $email - holds the email. + * @param float $amount - holds the amount. + * @param string $phone - holds the phone. + * @param string $hash - holds the hash key. + * @param string $productInfo - holds the product information. + * @param string $successURL - holds the success URL. + * @param string $failureURL - holds the failure URL. + * @param string $udf1 - holds the udf1. + * @param string $udf2 - holds the udf2. + * @param string $udf3 - holds the udf3. + * @param string $udf4 - holds the udf4. + * @param string $udf5 - holds the udf5. + * @param string $address1 - holds the first address. + * @param string $address2 - holds the second address. + * @param string $city - holds the city. + * @param string $state - holds the state. + * @param string $country - holds the country. + * @param string $zipcode - holds the zipcode. + * + * @return array with status and data - holds the details + * @return integer status = 0 means error. + * @return integer status = 1 means success and go the url link. + * + */ + function _pay($params_array, $redirect, $salt_key, $url){ + + $hash_key = ''; + // generate hash key and push into params array. + $hash_key = _getHashKey($params_array, $salt_key); + $params_array['hash'] = $hash_key; + + // call curl_call() for initiate pay link + $curl_result = _curlCall($url . 'payment/initiateLink', http_build_query($params_array)); + + // print_r($curl_result); + // die; + + $accesskey = ($curl_result->status === 1) ? $curl_result->data : null; + + if (empty($accesskey)) { + return $curl_result; + } else { + if ($redirect == true) { + $curl_result->data = $url . 'pay/' . $accesskey; + } else { + $curl_result->data = $accesskey; + // return $accesskey; + } + return $curl_result; + } + } + + +/* + * _getHashKey method generate Hash key based on the API call (initiatePayment API). + * + * hash format (hash sequence) : + * $hash = key|txnid|amount|productinfo|firstname|email|udf1|udf2|udf3|udf4|udf5|udf6|udf7|udf8|udf9|udf10|salt + * + * params string $hash_sequence - holds the format of hash key (sequence). + * params array $params - holds the passed array. + * params string $salt - holds merchant salt key. + * + * ##Return values + * + * - return string $hash - holds the generated hash key. + * + * @params string $hash_sequence - holds the format of hash key (sequence). + * @params array $params - holds the passed array. + * @params string $salt - holds merchant salt key. + * + * @return string $hash - holds the generated hash key. + * + */ + function _getHashKey($posted, $salt_key){ + $hash_sequence = "key|txnid|amount|productinfo|firstname|email|udf1|udf2|udf3|udf4|udf5|udf6|udf7|udf8|udf9|udf10"; + + // make an array or split into array base on pipe sign. + $hash_sequence_array = explode('|', $hash_sequence); + $hash = null; + + // prepare a string based on hash sequence from the $params array. + foreach ($hash_sequence_array as $value) { + $hash .= isset($posted[$value]) ? $posted[$value] : ''; + $hash .= '|'; + } + + $hash .= $salt_key; + #echo $hash; + #echo " "; + #echo strtolower(hash('sha512', $hash)); + // generate hash key using hash function(predefine) and return + return strtolower(hash('sha512', $hash)); + } + + +/* + * _curlCall method call CURL for initialized payment link. + * + * params string $url - holds the payment URL which will be redirect to. + * params array $params_array - holds the passed array. + * + * ##Return values + * + * - return array with curl_status and data - holds the details. + * + * - return integer curl_status = 0 means error. + * + * - return integer curl_status = 1 means success. + * + * @params string $url - holds the payment URL which will be redirect to. + * @params array $params_array - holds the passed array. + * + * @return array with curl_status and data - holds the details. + * @return integer curl_status = 0 means error. + * @return integer curl_status = 1 means success and go the url link. + * + * ##Method call + * - curl_init() - Initializes a new session and return a cURL. + * - curl_setopt_array() - Set multiple options for a cURL transfer. + * - curl_exec() - Perform a cURL session. + * - curl_errno() - Return the last error number. + * - curl_error() - Return a string containing the last error for the current session. + * + * ##Used value + * - curl_status => 0 : means failure. + * - curl_status => 1 : means Success. + * + */ + function _curlCall($url, $params_array){ + // Initializes a new session and return a cURL. + $cURL = curl_init(); + + ini_set('display_errors', 1); + ini_set('display_startup_errors', 1); + error_reporting(E_ALL); + + // Set multiple options for a cURL transfer. + curl_setopt_array( + $cURL, + array( + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => $params_array, + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36', + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_SSL_VERIFYPEER => 0 + ) + ); + + // Perform a cURL session + $result = curl_exec($cURL); + + + // check there is any error or not in curl execution. + if (curl_errno($cURL)) { + $cURL_error = curl_error($cURL); + if (empty($cURL_error)) + $cURL_error = 'Server Error'; + + return array( + 'curl_status' => 0, + 'error' => $cURL_error + ); + } + + $result = trim($result); + $result_response = json_decode($result); + + return $result_response; + } + + +/* + * _paymentResponse method show response after API call. + * + * params array $params_array - holds the passed array. + * + * ##Return values + * + * - return string URL $result->status = 1 - means go to easebuzz page. + * + * - return string URL $result->status = 0 - means error. + * + * @params array $params_array - holds the passed array. + * + * @return string URL $result->status = 1 - means go to easebuzz page. + * @return string URL $result->status = 0 - means error + * + */ + function _paymentResponse($result){ + + if ($result->status === 1) { + //first way + header('Location:' . $result->data); + + // second wayre + // echo " + // + // "; + + exit(); + } else { + //echo '

'.$result['data'].'

'; + return json_encode($result); + } + } + + +/* + * response method verify API response is acceptable or not and returns the response object. + * + * params array $response_params - holds the response array. + * params string $salt - holds the merchant salt key. + * + * ##Return values + * + * - return array with status and data - holds the details. + * + * - return integer status = 0 means error. + * + * - return integer status = 1 means success. + * + * @params array $response_params - holds the response array. + * @params string $salt - holds the merchant salt key. + * + * @return array with status and data - holds the details. + * @return integer status = 0 means error. + * @return integer status = 1 means success. + * + */ + function easebuzz_response($response_params, $salt_key){ + + // check return response params is array or not + if (!is_array($response_params) || count($response_params) === 0) { + return array( + 'status' => 0, + 'data' => 'Response params is empty.' + ); + } + + // remove white space, htmlentities, prepared $easebuzzPaymentResponse. + $easebuzzPaymentResponse = _removeSpaceAndPrepareAPIResponseArray($response_params); + + // empty validation + $empty_validation = _emptyValidation($easebuzzPaymentResponse, $salt_key); + if (is_array($empty_validation) && $empty_validation['status'] === 0) { + return $empty_validation; + } + + // empty validation for response params status + if (empty($easebuzzPaymentResponse['status'])) { + return array( + 'status' => 0, + 'data' => 'Response status is empty.' + ); + } + + // check response the correct or not + $response_result = _getResponse($easebuzzPaymentResponse, $salt_key); + + return $response_result; + } + + +/* + * _removeSpaceAndPrepareAPIResponseArray method Remove white space, converts characters to HTML entities + * and prepared the posted array. + * + * param array $response_array - holds the API response array. + * + * ##Return values + * + * - return array $temp_array - holds the all posted value after removing space. + * + * @param array $response_array - holds the API response array. + * + * @return array $temp_array - holds the all posted value after removing space. + * + */ + function _removeSpaceAndPrepareAPIResponseArray($response_array){ + $temp_array = array(); + foreach ($response_array as $key => $value) { + $temp_array[$key] = trim(htmlentities($value, ENT_QUOTES)); + } + return $temp_array; + } + + +/* + * _getResponse check response is correct or not. + * + * param array $response_array - holds the API response array. + * param array $s_key - holds the merchant salt key + * + * ##Return values + * + * - return array with status and data - holds the details. + * + * - return integer status = 0 means error. + * + * - return integer status = 1 means success. + * + * @param array $response_array - holds the API response array. + * @param array $s_key - holds the merchant salt key + * + * @return array with status and data - holds the details. + * @return integer status = 0 means error. + * @return integer status = 1 means success. + * + */ + function _getResponse($response_array, $s_key){ + + // reverse hash key for validation means response is correct or not. + $reverse_hash_key = _getReverseHashKey($response_array, $s_key); + + if ($reverse_hash_key === $response_array['hash']) { + switch ($response_array['status']) { + case 'success': + return array( + 'status' => 1, + 'url' => $response_array['surl'], + 'data' => $response_array + ); + break; + case 'failure': + return array( + 'status' => 1, + 'url' => $response_array['furl'], + 'data' => $response_array + ); + break; + default: + return array( + 'status' => 1, + 'data' => $response_array + ); + } + } else { + return array( + 'status' => 0, + 'data' => 'Hash key Mismatch' + ); + } + } + + +/* + * _getReverseHashKey to generate Reverse hash key for validation + * + * reverse hash format (hash sequence) : + * $reverse_hash = salt|status|udf10|udf9|udf8|udf7|udf6|udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key + * + * status in $reverse_hash means => it will the response status which is getting from the response. + * + * params string $reverse_hash_sequence - holds the format of reverse hash key (sequence). + * params array $response_array - holds the response array. + * params string $s_key - holds the merchant salt key. + * + * ##Return values + * + * - return string $reverse_hash - holds the generated reverse hash key. + * + * @params string $reverse_hash_sequence - holds the format of reverse hash key (sequence). + * @params array $response_array - holds the response array. + * @params string $s_key - holds the merchant salt key. + * + * @return string $reverse_hash - holds the generated reverse hash key. + * + */ + function _getReverseHashKey($response_array, $s_key){ + $reverse_hash_sequence = "udf10|udf9|udf8|udf7|udf6|udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key"; + + // make an array or split into array base on pipe sign. + $reverse_hash = ""; + $reverse_hash_sequence_array = explode('|', $reverse_hash_sequence); + $reverse_hash .= $s_key . '|' . $response_array['status']; + + // prepare a string based on reverse hash sequence from the $response_array array. + foreach ($reverse_hash_sequence_array as $value) { + $reverse_hash .= '|'; + $reverse_hash .= isset($response_array[$value]) ? $response_array[$value] : ''; + } + + // generate reverse hash key using hash function(predefine) and return + return strtolower(hash('sha512', $reverse_hash)); + } diff --git a/app/Libraries/Easebuzz/payout.php b/app/Libraries/Easebuzz/payout.php new file mode 100755 index 000000000..aaab9e245 --- /dev/null +++ b/app/Libraries/Easebuzz/payout.php @@ -0,0 +1,476 @@ + 0, + 'data' => 'Invalid number of arguments.' + ); + } + return 1; + } + + + /* + * _removeSpaceAndPreparePostArray method Remove white space, converts characters to HTML entities + * and prepared the posted array. + * + * param array $params - holds $_POST array, merchant key and transaction key. + * + * ##Return values + * + * - return array $temp_array - holds the all posted value after removing space. + * + * @param array $params - holds $_POST array, merchant key and transaction key. + * + * @return array $temp_array - holds the all posted value after removing space. + * + */ + function _removeSpaceAndPreparePostArray($params){ + // $temp_array = array( + // 'merchant_key' => trim( htmlentities($params['merchant_key'], ENT_QUOTES) ), + // 'merchant_email' => trim( htmlentities($params['merchant_email'], ENT_QUOTES) ), + // 'payout_date' => trim( htmlentities($params['payout_date'], ENT_QUOTES) ) + // ); + // return $temp_array; + $temp_array = array(); + foreach ($params as $key => $value) { + if (array_key_exists($key, $params) and !empty($key) ){ + $temp_array[$key] = trim(htmlentities($value, ENT_QUOTES)); + } + } + return $temp_array; + } + + + /* + * _emptyValidation method check empty validation for Mandatory Parameters. + * + * param array $params - holds the all $_POST data + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all $params Mandatory parameters is not empty. + * + * - return array with status and data - $params parameters or $salt are empty. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all $params Mandatory parameters is not empty. + * @return array with status and data - $params parameters or $salt are empty. + * + */ + function _emptyValidation($params, $salt){ + $empty_value = false; + if(empty($params['merchant_key'])) + $empty_value = 'Merchant Key'; + + if(empty($params['merchant_email'])) + $empty_value = 'Merchant email'; + + if(empty($params['payout_date'])) + $empty_value = 'Payout date'; + + if(empty($salt)) + $empty_value = 'Merchant Salt Key'; + + if($empty_value !== false){ + return array( + 'status' => 0, + 'data' => 'Mandatory Parameter '.$empty_value.' is empty' + ); + } + return true; + } + + + /* + * _typeValidation method check type validation for field. + * + * param array $params - holds the all $_POST data. + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all params parameters type are correct. + * + * - return array with status and data - params parameters type mismatch. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all params parameters type are correct. + * @return array with status and data - params parameters type mismatch. + * + */ + function _typeValidation($params, $salt, $env){ + $type_value = false; + if(!is_string($params['merchant_key'])) + $type_value = "Merchant Key should be string"; + + if(!is_string($params['merchant_email'])) + $type_value = "Merchant email should be string"; + + if(!is_string($params['payout_date'])) + $type_value = "Payout should be date"; + + if($type_value !== false){ + return array( + 'status' => 0, + 'data' => $type_value + ); + } + return true; + } + + + /* + * _emailValidation method check email format validation + * + * param string $email - holds the email address. + * + * ##Return values + * + * - return boolean true - email format is correct. + * + * - return array with status and data - email format is incorrect. + * + * @param string $email - holds the email address. + * + * @return boolean true - email format is correct. + * @return array with status and data - email format is incorrect. + * + */ + function _emailValidation($email){ + $email_regx = "/^([\w\.-]+)@([\w-]+)\.([\w]{2,8})(\.[\w]{2,8})?$/"; + if(!preg_match($email_regx, $email)){ + return array( + 'status' => 0, + 'data' => 'Merchant Email invalid, Please enter valid email.' + ); + } + return true; + } + + + /* + * _getURL method set based on enviroment ($env = 'test' or $env = 'prod') and + * generate url link. + * + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return string $url_link - holds the full url link. + * + * @param string $env - holds the enviroment. + * + * @return string $url_link - holds the full URL. + * + */ + function _getURL($env){ + $url_link = ''; + switch($env){ + case 'test' : + $url_link = "https://testdashboard.easebuzz.in/"; + break; + case 'prod' : + $url_link = 'https://dashboard.easebuzz.in/'; + break; + default : + $url_link = "https://testdashboard.easebuzz.in/"; + } + return $url_link; + } + + + /* + * _payoutPayment method initiate payout payment. + * + * params array $params_array - holds all form data with merchant key, transaction id etc. + * params string $salt_key - holds the merchant salt key. + * params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * param string $key - holds the merchant key. + * param string $merchant_email - holds the mrchant email. + * param string $payout_date - holds the payout date. + * + * ##Return values + * + * - return array with status and data - holds the details + * + * - return integer status = 0 means error. + * + * - return integer status = 1 means success and return result. + * + * @params array $params_array - holds all form data with merchant key, merchant email, date etc. + * @params string $salt_key - holds the merchant salt key. + * @params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * @param string $key - holds the merchant key. + * @param string $merchant_email - holds the merchant email. + * @param string $payout_date - holds the payout date. + * + * @return array with status and data - holds the details + * @return integer status = 0 means error. + * @return integer status = 1 means success and go the url link. + * + */ + function _payoutPayment($params_array, $salt_key, $url){ + $hash_key = ''; + + // generate hash key and push into params array. + $hash_key = _getHashKey($params_array, $salt_key); + $params_array['hash'] = $hash_key; + + // call curl_call() for initiate payout link + $curl_result = _curlCall( $url.'payout/v1/retrieve', http_build_query($params_array) ); + + return $curl_result; + } + + + /* + * _getHashKey method generate Hash key based on the API call (payout API). + * + * hash format (hash sequence) : + * $hash = key|merchant_email|payout_date|salt + * + * params string $hash_sequence - holds the format of hash key (sequence). + * params array $params - holds the passed array. + * params string $salt - holds merchant salt key. + * + * ##Return values + * + * - return string $hash - holds the generated hash key. + * + * @params string $hash_sequence - holds the format of hash key (sequence). + * @params array $params - holds the passed array. + * @params string $salt - holds merchant salt key. + * + * @return string $hash - holds the generated hash key. + * + */ + function _getHashKey($posted, $salt_key){ + $hash_sequence = "merchant_key|merchant_email|payout_date"; + + // make an array or split into array base on pipe sign. + $hash_sequence_array = explode( '|', $hash_sequence ); + $hash = null; + + // prepare a string based on hash sequence from the $params array. + foreach($hash_sequence_array as $value ) { + $hash .= isset($posted[$value]) ? $posted[$value] : ''; + $hash .= '|'; + } + + $hash .= $salt_key; + // generate hash key using hash function(predefine) and return + return strtolower( hash('sha512', $hash) ); + } + + + /* + * _curlCall method call CURL for payout payment. + * + * params string $url - holds the payment URL which will be redirect to. + * params array $params_array - holds the passed array. + * + * ##Return values + * + * - return array with curl_status and data - holds the details. + * + * - return integer curl_status = 0 means error. + * + * - return integer curl_status = 1 means success. + * + * @params string $url - holds the payment URL which will be redirect to. + * @params array $params_array - holds the passed array. + * + * @return array with curl_status and data - holds the details. + * @return integer curl_status = 0 means error. + * @return integer curl_status = 1 means success and go the url link. + * + * ##Method call + * - curl_init() - Initializes a new session and return a cURL. + * - curl_setopt_array() - Set multiple options for a cURL transfer. + * - curl_exec() - Perform a cURL session. + * - curl_errno() - Return the last error number. + * - curl_error() - Return a string containing the last error for the current session. + * + * ##Used value + * - curl_status => 0 : means failure. + * - curl_status => 1 : means Success. + * + */ + function _curlCall($url, $params_array){ + // Initializes a new session and return a cURL. + $cURL = curl_init(); + + // Set multiple options for a cURL transfer. + curl_setopt_array( + $cURL, + array ( + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => $params_array, + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36', + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_SSL_VERIFYPEER => 0 + ) + ); + + // Perform a cURL session + $result = curl_exec($cURL); + + // check there is any error or not in curl execution. + if( curl_errno($cURL) ){ + $cURL_error = curl_error($cURL); + if( empty($cURL_error) ) + $cURL_error = 'Server Error'; + + return array( + 'curl_status' => 0, + 'error' => $cURL_error + ); + } + + $result = json_decode($result); + + return $result; + } + +?> + + diff --git a/app/Libraries/Easebuzz/refund.php b/app/Libraries/Easebuzz/refund.php new file mode 100755 index 000000000..fcc88385b --- /dev/null +++ b/app/Libraries/Easebuzz/refund.php @@ -0,0 +1,534 @@ + 0, + 'data' => 'Invalid number of arguments.' + ); + } + return 1; + } + + + /* + * _removeSpaceAndPreparePostArray method Remove white space, converts characters to HTML entities + * and prepared the posted array. + * + * param array $params - holds $_POST array, merchant key and transaction key. + * + * ##Return values + * + * - return array $temp_array - holds the all posted value after removing space. + * + * @param array $params - holds $_POST array, merchant key and transaction key. + * + * @return array $temp_array - holds the all posted value after removing space. + * + */ + function _removeSpaceAndPreparePostArray($params){ + /*$temp_array = array( + 'key' => trim( htmlentities($params['key'], ENT_QUOTES) ), + 'txnid' => trim( htmlentities($params['txnid'], ENT_QUOTES) ), + 'refund_amount' => trim( htmlentities($params['refund_amount'], ENT_QUOTES) ), + 'phone' => trim( htmlentities($params['phone'], ENT_QUOTES) ), + 'amount' => trim( htmlentities($params['amount'], ENT_QUOTES) ), + 'email' => trim( htmlentities($params['email'], ENT_QUOTES) ) + ); + */ + $temp_array = array(); + foreach ($params as $key => $value) { + $temp_array[$key] = trim(htmlentities($value, ENT_QUOTES)); + } + return $temp_array; + } + + + /* + * _emptyValidation method check empty validation for Mandatory Parameters. + * + * param array $params - holds the all $_POST data + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all $params Mandatory parameters is not empty. + * + * - return array with status and data - $params parameters or $salt are empty. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all $params Mandatory parameters is not empty. + * @return array with status and data - $params parameters or $salt are empty. + * + */ + function _emptyValidation($params, $salt){ + $empty_value = false; + if(empty($params['key'])) + $empty_value = 'Merchant Key'; + + if(empty($params['txnid'])) + $empty_value = 'Transaction ID'; + + if(empty($params['refund_amount'])) + $empty_value = 'Refund Amount'; + + if(empty($params['phone'])) + $empty_value = 'Phone'; + + if(empty($params['email'])) + $empty_value = 'Email ID'; + + if(empty($params['amount'])) + $empty_value = ' Paid Amount'; + + if(empty($salt)) + $empty_value = 'Merchant Salt Key'; + + if($empty_value !== false){ + return array( + 'status' => 0, + 'data' => 'Mandatory Parameter '.$empty_value.' can not empty' + ); + } + return true; + } + + + /* + * _typeValidation method check type validation for field. + * + * param array $params - holds the all $_POST data. + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all params parameters type are correct. + * + * - return array with status and data - params parameters type mismatch. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all params parameters type are correct. + * @return array with status and data - params parameters type mismatch. + * + */ + function _typeValidation($params, $salt, $env){ + $type_value = false; + if(!is_string($params['key'])) + $type_value = "Merchant Key should be string"; + + if(!is_string($params['txnid'])) + $type_value = "Transaction ID should be string"; + + if(!is_string($params['phone'])) + $type_value = "Phone Number should be number"; + + if(!is_string($params['email'])) + $type_value = "Email ID should be string"; + + if(!is_float($params['amount'])) + $type_value = "The paid amount should float up to two or one decimal."; + + if(!is_float($params['refund_amount'])) + $type_value = "The refund amount should float up to two or one decimal."; + + if($type_value !== false){ + return array( + 'status' => 0, + 'data' => $type_value + ); + } + return true; + } + + + /* + * _emailValidation method check email format validation + * + * param string $email - holds the email address. + * + * ##Return values + * + * - return boolean true - email format is correct. + * + * - return array with status and data - email format is incorrect. + * + * @param string $email - holds the email address. + * + * @return boolean true - email format is correct. + * @return array with status and data - email format is incorrect. + * + */ + function _emailValidation($email){ + $email_regx = "/^([\w\.-]+)@([\w-]+)\.([\w]{2,8})(\.[\w]{2,8})?$/"; + if(!preg_match($email_regx, $email)){ + return array( + 'status' => 0, + 'data' => 'Email invalid, Please enter valid email.' + ); + } + return true; + } + + + /* + * _getURL method set based on enviroment ($env = 'test' or $env = 'prod') and generate url link. + * + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return string $url_link - holds the full url link. + * + * @param string $env - holds the enviroment. + * + * @return string $url_link - holds the full URL. + * + */ + function _getURL($env){ + $url_link = ''; + switch($env){ + case 'test' : + $url_link = "https://testdashboard.easebuzz.in/"; + break; + case 'prod' : + $url_link = 'https://dashboard.easebuzz.in/'; + break; + default : + $url_link = "https://testdashboard.easebuzz.in/"; + } + return $url_link; + } + + + /* + * _refundPayment method initiate refund payment. + * + * params array $params_array - holds all form data with merchant key, transaction id etc. + * params string $salt_key - holds the merchant salt key. + * params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * param string $key - holds the merchant key. + * param string $txnid - holds the transaction id. + * param float $refund_amount - holds the refund amount. + * param string $email - holds the email. + * param string $amount - holds the amount. + * param string $phone - holds the phone. + * param string $hash - holds the hash key. + * + * ##Return values + * + * - return array with status and data - holds the details + * + * - return integer status = 0 means error. + * + * - return integer status = 1 means success and go the url link. + * + * @params array $params_array - holds all form data with merchant key, transaction id etc. + * @params string $salt_key - holds the merchant salt key. + * @params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * @param string $key - holds the merchant key. + * @param string $txnid - holds the transaction id. + * @param float $refund_amount - holds the refund amount. + * @param string $email - holds the email. + * @param string $amount - holds the amount. + * @param string $phone - holds the phone. + * @param string $hash - holds the hash key. + * + * @return array with status and data - holds the details + * @return integer status = 0 means error. + * @return integer status = 1 means success and go the url link. + * + */ + function _refundPayment($params_array, $salt_key, $url){ + $hash_key = ''; + + // generate hash key and push into params array. + $hash_key = _getHashKey($params_array, $salt_key); + + $params_array['hash'] = $hash_key; + + // call curl_call() for initiate pay link + $curl_result = _curlCall( $url.'transaction/v1/refund', http_build_query($params_array) ); + + return $curl_result; + } + + + /* + * _getHashKey method generate Hash key based on the API call (refund API). + * + * hash format (hash sequence) : + * $hash = key|txnid|amount|refund_amount|email|phone|salt + * + * params string $hash_sequence - holds the format of hash key (sequence). + * params array $params - holds the passed array. + * params string $salt - holds merchant salt key. + * + * ##Return values + * + * - return string $hash - holds the generated hash key. + * + * @params string $hash_sequence - holds the format of hash key (sequence). + * @params array $params - holds the passed array. + * @params string $salt - holds merchant salt key. + * + * @return string $hash - holds the generated hash key. + * + */ + function _getHashKey($posted, $salt_key){ + $hash_sequence = "key|txnid|amount|refund_amount|email|phone"; + + // make an array or split into array base on pipe sign. + $hash_sequence_array = explode( '|', $hash_sequence ); + $hash = null; + + // prepare a string based on hash sequence from the $params array. + foreach($hash_sequence_array as $value ) { + $hash .= isset($posted[$value]) ? $posted[$value] : ''; + $hash .= '|'; + } + + $hash .= $salt_key; + + // generate hash key using hash function(predefine) and return + return strtolower( hash('sha512', $hash) ); + } + + + /* + * _curlCall method call CURL for refund payment link. + * + * params string $url - holds the payment URL which will be redirect to. + * params array $params_array - holds the passed array. + * + * ##Return values + * + * - return array with curl_status and data - holds the details. + * + * - return integer curl_status = 0 means error. + * + * - return integer curl_status = 1 means success. + * + * @params string $url - holds the payment URL which will be redirect to. + * @params array $params_array - holds the passed array. + * + * @return array with curl_status and data - holds the details. + * @return integer curl_status = 0 means error. + * @return integer curl_status = 1 means success and go the url link. + * + * ##Method call + * - curl_init() - Initializes a new session and return a cURL. + * - curl_setopt_array() - Set multiple options for a cURL transfer. + * - curl_exec() - Perform a cURL session. + * - curl_errno() - Return the last error number. + * - curl_error() - Return a string containing the last error for the current session. + * + * ##Used value + * - curl_status => 0 : means failure. + * - curl_status => 1 : means Success. + * + */ + function _curlCall($url, $params_array){ + // Initializes a new session and return a cURL. + $cURL = curl_init(); + + // Set multiple options for a cURL transfer. + curl_setopt_array( + $cURL, + array ( + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => $params_array, + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36', + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_SSL_VERIFYPEER => 0 + ) + ); + + // Perform a cURL session + $result = curl_exec($cURL); + + // check there is any error or not in curl execution. + if( curl_errno($cURL) ){ + $cURL_error = curl_error($cURL); + if( empty($cURL_error) ) + $cURL_error = 'Server Error'; + + return array( + 'curl_status' => 0, + 'error' => $cURL_error + ); + } + + $result = trim($result); + + return json_decode($result); + } + +?> + + diff --git a/app/Libraries/Easebuzz/transaction.php b/app/Libraries/Easebuzz/transaction.php new file mode 100755 index 000000000..0edeb3183 --- /dev/null +++ b/app/Libraries/Easebuzz/transaction.php @@ -0,0 +1,613 @@ + 0, + 'data' => 'Amount should be float and support upto 2 decimal' + ); + } + + // $diff_amount_string = abs( strlen($params['amount']) - strlen("".$postedArray['amount'] ."") ); + // echo $diff_amount_string; + // $diff_amount_string = ($diff_amount_string === 2) ? 1 : 2; + // echo $diff_amount_string; + // $postedArray['amount'] = sprintf("%.". $diff_amount_string ."f", $postedArray['amount']); + + // email validation + $email_validation = _email_validation($postedArray['email']); + if($email_validation !== true) + return $email_validation; + + // get URL based on enviroment like ($env = 'test' or $env = 'prod') + $URL = _getURL($env); + + // process to start get transaction details + $transaction_result = _getTransaction($postedArray, $salt, $URL); + + return $transaction_result; + } + + + /* + * _checkArgumentValidation method Check number of Arguments Validation. Means how many arguments submitted + * from form and verify with + * API documentation. + * + * param array $params - holds the all $_POST data. + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return interger 1 number of arguments match. + * + * - return array status = 0 number of arguments mismatch. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return interger 1 number of arguments match. + * @return array status = 0 number of arguments mismatch. + * + */ + function _checkArgumentValidation($params, $merchant_key, $salt, $env){ + $args = func_get_args(); + $argsc = count($args); + if($argsc !== 4){ + return array( + 'status' => 0, + 'data' => 'Invalid number of arguments.' + ); + } + return 1; + } + + + /* + * _removeSpaceAndPreparePostArray method Remove white space, converts characters to HTML entities + * and prepared the posted array. + * + * param array $params - holds $_POST array, merchant key and transaction key. + * + * ##Return values + * + * - return array $temp_array - holds the all posted value after removing space. + * + * @param array $params - holds $_POST array, merchant key and transaction key. + * + * @return array $temp_array - holds the all posted value after removing space. + * + */ + function _removeSpaceAndPreparePostArray($params){ + /*$temp_array = array( + 'key' => trim( htmlentities($params['key'], ENT_QUOTES) ), + 'txnid' => trim( htmlentities($params['txnid'], ENT_QUOTES) ), + 'amount' => trim( htmlentities($params['amount'], ENT_QUOTES) ), + 'email' => trim( htmlentities($params['email'], ENT_QUOTES) ), + 'phone' => trim( htmlentities($params['phone'], ENT_QUOTES) ) + );*/ + $temp_array = array(); + foreach ($params as $key => $value) { + $temp_array[$key] = trim(htmlentities($value, ENT_QUOTES)); + } + return $temp_array; + } + + + /* + * _emptyValidation method check empty validation for Mandatory Parameters. + * + * param array $params - holds the all $_POST data + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all $params Mandatory parameters is not empty. + * + * - return array with status and data - $params parameters or $salt are empty. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all $params Mandatory parameters is not empty. + * @return array with status and data - $params parameters or $salt are empty. + * + */ + function _emptyValidation($params, $salt){ + $empty_value = false; + if(empty($params['key'])) + $empty_value = 'Merchant Key'; + + if(empty($params['txnid'])) + $empty_value = 'Transaction ID'; + + if(empty($params['amount'])) + $empty_value = 'Transaction Amount'; + + if(empty($params['email'])) + $empty_value ='Email'; + + if(empty($params['phone'])) + $empty_value = 'Phone'; + + if(empty($salt)) + $empty_value = 'Merchant Salt Key'; + + if($empty_value !== false){ + return array( + 'status' => 0, + 'data' => 'Mandatory Parameter '.$empty_value.' can not empty' + ); + } + return true; + } + + + /* + * _typeValidation method check type validation for field. + * + * param array $params - holds the all $_POST data. + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all params parameters type are correct. + * + * - return array with status and data - params parameters type mismatch. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all params parameters type are correct. + * @return array with status and data - params parameters type mismatch. + * + */ + function _typeValidation($params, $salt, $env){ + $type_value = false; + if(!is_string($params['key'])) + $type_value = "Merchant Key should be string"; + + if(!is_float($params['amount'])) + $type_value = "The transaction amount should float up to two or one decimal."; + + if(!is_string($params['txnid'])) + $type_value = "Merchant Transaction ID should be string"; + + if(!is_string($params['phone'])) + $type_value = "Customer Phone Number should be number"; + + if(!is_string($params['email'])) + $type_value = "Customer Email ID should be string"; + + if($type_value !== false){ + return array( + 'status' => 0, + 'data' => $type_value + ); + } + return true; + } + + + /* + * _email_validation method check email format validation + * + * param string $email - holds the email address. + * + * ##Return values + * + * - return boolean true - email format is correct. + * + * - return array with status and data - email format is incorrect. + * + * @param string $email - holds the email address. + * + * @return boolean true - email format is correct. + * @return array with status and data - email format is incorrect. + * + */ + function _email_validation($email){ + $email_regx = "/^([\w\.-]+)@([\w-]+)\.([\w]{2,8})(\.[\w]{2,8})?$/"; + if(!preg_match($email_regx, $email)){ + return array( + 'status' => 0, + 'data' => 'Email invalid, Please enter valid email.' + ); + } + return true; + } + + + /* + * _getURL method set based on enviroment ($env = 'test' or $env = 'prod') + * cand generate url link. + * + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return string $url_link - holds the full url link. + * + * @param string $env - holds the enviroment. + * + * @return string $url_link - holds the full URL. + * + */ + function _getURL($env){ + $url_link = ''; + switch($env){ + case 'test' : + $url_link = "https://testdashboard.easebuzz.in/"; + break; + case 'prod' : + $url_link = 'https://dashboard.easebuzz.in/'; + break; + default : + $url_link = "https://testdashboard.easebuzz.in/"; + } + return $url_link; + } + + + /* + * _getTransaction method get all details of a single transaction. + * + * params array $params_array - holds all form data with merchant key, transaction id etc. + * params string $salt_key - holds the merchant salt key. + * params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * param string $key - holds the merchant key. + * param string $txnid - holds the transaction id. + * param string $email - holds the email. + * param float $amount - holds the amount. + * param string $phone - holds the phone. + * param string $hash - holds the hash key. + * + * ##Return values + * + * - return array with status and data - holds the details + * + * - return integer status = 0 means error. + * + * - return integer status = 1 means success and go the url link. + * + * @params array $params_array - holds all form data with merchant key, transaction id etc. + * @params string $salt_key - holds the merchant salt key. + * @params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * @param string $key - holds the merchant key. + * @param string $txnid - holds the transaction id. + * @param string $email - holds the email. + * @param float $amount - holds the amount. + * @param string $phone - holds the phone. + * @param string $hash - holds the hash key. + * + * @return array with status and data - holds the details + * @return integer status = 0 means error. + * @return integer status = 1 means success and go the url link. + * + */ + function _getTransaction($params_array, $salt_key, $url){ + $hash_key = ''; + + // generate hash key and push into params array. + $hash_key = _getHashKey($params_array, $salt_key); + $params_array['hash'] = $hash_key; + + // call curl_call() for initiate pay link + $curl_result = _curlCall( $url.'transaction/v1/retrieve', http_build_query($params_array) ); + + return $curl_result; + } + + + /* + * _getHashKey method generate Hash key based on the API call (initiatePayment API). + * + * hash format (hash sequence) : + * $hash = key|txnid|amount|email|phone|salt + * + * params string $hash_sequence - holds the format of hash key (sequence). + * params array $params - holds the passed array. + * params string $salt - holds merchant salt key. + * + * ##Return values + * + * - return string $hash - holds the generated hash key. + * + * @params string $hash_sequence - holds the format of hash key (sequence). + * @params array $params - holds the passed array. + * @params string $salt - holds merchant salt key. + * + * @return string $hash - holds the generated hash key. + * + */ + function _getHashKey($posted, $salt_key){ + $hash_sequence = "key|txnid|amount|email|phone"; + + // make an array or split into array base on pipe sign. + $hash_sequence_array = explode( '|', $hash_sequence ); + $hash = null; + + // prepare a string based on hash sequence from the $params array. + foreach($hash_sequence_array as $value ) { + $hash .= isset($posted[$value]) ? $posted[$value] : ''; + $hash .= '|'; + } + + $hash .= $salt_key; + #echo $hash; + // generate hash key using hash function(predefine) and return + return strtolower( hash('sha512', $hash) ); + } + + + /* + * _curlCall method call CURL for get data from the API. + * + * params string $url - holds the payment URL which will be redirect to. + * params array $params_array - holds the passed array. + * + * ##Return values + * + * - return array with curl_status and data - holds the details. + * + * - return integer curl_status = 0 means error. + * + * - return integer curl_status = 1 means success. + * + * @params string $url - holds the payment URL which will be redirect to. + * @params array $params_array - holds the passed array. + * + * @return array with curl_status and data - holds the details. + * @return integer curl_status = 0 means error. + * @return integer curl_status = 1 means success and go the url link. + * + * ##Method call + * - curl_init() - Initializes a new session and return a cURL. + * - curl_setopt_array() - Set multiple options for a cURL transfer. + * - curl_exec() - Perform a cURL session. + * - curl_errno() - Return the last error number. + * - curl_error() - Return a string containing the last error for the current session. + * + * ##Used value + * - curl_status => 0 : means failure. + * - curl_status => 1 : means Success. + * + */ + function _curlCall($url, $params_array){ + // Initializes a new session and return a cURL. + $cURL = curl_init(); + + // Set multiple options for a cURL transfer. + curl_setopt_array( + $cURL, + array ( + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => $params_array, + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36', + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_SSL_VERIFYPEER => 0 + ) + ); + + // Perform a cURL session + $result = curl_exec($cURL); + + // check there is any error or not in curl execution. + if( curl_errno($cURL) ){ + $cURL_error = curl_error($cURL); + if( empty($cURL_error) ) + $cURL_error = 'Server Error'; + + return array( + 'status' => 0, + 'data' => $cURL_error + ); + } + + $result = trim($result); + $result_response = json_decode($result); + + return $result_response; + } + + + /* + * _validateTransactionResponse method call response method for verify the response + * + * params array $params_array - holds the passed array. + * + * ##Return values + * + * - return string URL $result->status = 1 - means go to easebuzz page. + * + * - return string URL $result->status = 0 - means error. + * + * @params array $params_array - holds the passed array. + * + * @return string URL $result->status = 1 - means go to easebuzz page. + * @return string URL $result->status = 0 - means error + * + */ + function _validateTransactionResponse($response_array, $salt_key){ + + if ($response_array->status === 1){ + + // reverse hash key for validation means response is correct or not. + $reverse_hash_key = _getReverseHashKey($response_array, $salt_key); + + if($reverse_hash_key === $response_array->data->hash){ + return $response_array; + }else{ + return array( + 'status' => 0, + 'data' => 'Hash key Mismatch' + ); + } + } + return $response_array; + } + + + /* + * _getReverseHashKey to generate Reverse hash key for validation + * + * reverse hash format (hash sequence) : + * $reverse_hash = salt|status|udf10|udf9|udf8|udf7|udf6|udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key + * + * status in $reverse_hash means => it will the response status which is getting from the response. + * + * params string $reverse_hash_sequence - holds the format of reverse hash key (sequence). + * params object $response_obj - holds the response object. + * params string $s_key - holds the merchant salt key. + * + * ##Return values + * + * - return string $reverse_hash - holds the generated reverse hash key. + * + * @params string $reverse_hash_sequence - holds the format of reverse hash key (sequence). + * @params object $response_obj - holds the response object. + * @params string $s_key - holds the merchant salt key. + * + * @return string $reverse_hash - holds the generated reverse hash key. + * + */ + function _getReverseHashKey($response_obj, $s_key){ + $reverse_hash_sequence = "udf10|udf9|udf8|udf7|udf6|udf5|udf4|udf3|udf2|udf1|email|firstname|productinfo|amount|txnid|key"; + + // make an array or split into array base on pipe sign. + $reverse_hash = ""; + $reverse_hash_sequence_array = explode( '|', $reverse_hash_sequence ); + $reverse_hash .= $s_key.'|' . $response_obj['data']->status; + + // prepare a string based on reverse hash sequence from the $response_obj array. + foreach($reverse_hash_sequence_array as $value ) { + $reverse_hash .= '|'; + $reverse_hash .= $response_obj['data']->$value; + } + + // generate reverse hash key using hash function(predefine) and return + return strtolower( hash('sha512', $reverse_hash) ); + } + +?> + diff --git a/app/Libraries/Easebuzz/transaction_date.php b/app/Libraries/Easebuzz/transaction_date.php new file mode 100755 index 000000000..f67fc5be4 --- /dev/null +++ b/app/Libraries/Easebuzz/transaction_date.php @@ -0,0 +1,479 @@ + 0, + 'data' => 'Invalid number of arguments.' + ); + } + return 1; + } + + + /* + * _removeSpaceAndPreparePostArray method Remove white space, converts characters to HTML entities + * and prepared the posted array. + * + * param array $params - holds $_POST array, merchant key and transaction key. + * + * ##Return values + * + * - return array $temp_array - holds the all posted value after removing space. + * + * @param array $params - holds $_POST array, merchant key and transaction key. + * + * @return array $temp_array - holds the all posted value after removing space. + * + */ + function _removeSpaceAndPreparePostArray($params){ + // $temp_array = array( + // 'merchant_key' => trim( htmlentities($params['merchant_key'], ENT_QUOTES) ), + // 'merchant_email' => trim( htmlentities($params['merchant_email'], ENT_QUOTES) ), + // 'transaction_date' => trim( htmlentities($params['transaction_date'], ENT_QUOTES) ) + // ); + $temp_array = array(); + foreach ($params as $key => $value) { + if (array_key_exists($key, $params) and !empty($key) ){ + $temp_array[$key] = trim(htmlentities($value, ENT_QUOTES)); + } + } + return $temp_array; + } + + + /* + * _typeValidation method check type validation for field. + * + * param array $params - holds the all $_POST data. + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all params parameters type are correct. + * + * - return array with status and data - params parameters type mismatch. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all params parameters type are correct. + * @return array with status and data - params parameters type mismatch. + * + */ + function _typeValidation($params, $salt, $env){ + $type_value = false; + if(!is_string($params['merchant_key'])) + $type_value = "Merchant Key should be string"; + + if(!is_string($params['merchant_email'])) + $type_value = "Merchat Email should be string"; + + if(!is_string($params['transaction_date'])) + $type_value = "Transaction date should be date"; + + if($type_value !== false){ + return array( + 'status' => 0, + 'data' => $type_value + ); + } + return true; + } + + + /* + * _emptyValidation method check empty validation for Mandatory Parameters. + * + * param array $params - holds the all $_POST data + * param string $salt - holds the merchant salt key. + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return boolean true - all $params Mandatory parameters is not empty. + * + * - return array with status and data - $params parameters or $salt are empty. + * + * @param array $params - holds the all $_POST data. + * @param string $salt - holds the merchant salt key. + * @param string $env - holds the enviroment. + * + * @return boolean true - all $params Mandatory parameters is not empty. + * @return array with status and data - $params parameters or $salt are empty. + * + */ + function _emptyValidation($params, $salt){ + $empty_value = false; + if(empty($params['merchant_key'])) + $empty_value = 'Merchant Key'; + + if(empty($params['merchant_email'])) + $empty_value ='Merchant Email'; + + if(empty($params['transaction_date'])) + $empty_value = 'Transaction Date'; + + if(empty($salt)) + $empty_value = 'Merchant Salt Key'; + + if($empty_value !== false){ + return array( + 'status' => 0, + 'data' => 'Mandatory Parameter '.$empty_value.' can not empty' + ); + } + return true; + } + + + /* + * _email_validation method check email format validation + * + * param string $email - holds the email address. + * + * ##Return values + * + * - return boolean true - email format is correct. + * + * - return array with status and data - email format is incorrect. + * + * @param string $email - holds the email address. + * + * @return boolean true - email format is correct. + * @return array with status and data - email format is incorrect. + * + */ + function _email_validation($email){ + $email_regx = "/^([\w\.-]+)@([\w-]+)\.([\w]{2,8})(\.[\w]{2,8})?$/"; + if(!preg_match($email_regx, $email)){ + return array( + 'status' => 0, + 'data' => 'Email invalid, Please enter valid email.' + ); + } + return true; + } + + + /* + * _getURL method set based on enviroment ($env = 'test' or $env = 'prod') + * and generate url link. + * + * param string $env - holds the enviroment. + * + * ##Return values + * + * - return string $url_link - holds the full url link. + * + * @param string $env - holds the enviroment. + * + * @return string $url_link - holds the full URL. + * + */ + function _getURL($env){ + $url_link = ''; + switch($env){ + case 'test' : + $url_link = "https://testdashboard.easebuzz.in/"; + break; + case 'prod' : + $url_link = 'https://dashboard.easebuzz.in/'; + break; + default : + $url_link = "https://testdashboard.easebuzz.in/"; + } + return $url_link; + } + + + /* + * _getDateTransaction method get all transaction details based on date. + * + * params array $params_array - holds all form data with merchant key, transaction date etc. + * params string $salt_key - holds the merchant salt key. + * params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * param string $key - holds the merchant key. + * param string $merchant_email - holds the merchant email id. + * param string $transaction_date - holds the transaction date. + * param string $hash - holds the hash key. + * + * ##Return values + * + * - return array with status and data - holds the details + * + * - return integer status = 0 means error. + * + * - return integer status = 1 means success. + * + * @params array $params_array - holds all form data with merchant email, transaction date etc. + * @params string $salt_key - holds the merchant salt key. + * @params string $url - holds the url based in env(enviroment type $env = 'test' or $env = 'prod') + * + * @param string $key - holds the merchant key. + * @param string $merchant_email - holds the merchant email id. + * @param string $transaction_date - holds the transaction date. + * @param string $hash - holds the hash key. + * + * @return array with status and data - holds the details + * @return integer status = 0 means error. + * @return integer status = 1 means success and go the url link. + * + */ + function _getDateTransaction($params_array, $salt_key, $url){ + $hash_key = ''; + + // generate hash key and push into params array. + $hash_key = _getHashKey($params_array, $salt_key); + $params_array['hash'] = $hash_key; + + // call curl_call() for initiate pay link + $curl_result = _curlCall( $url.'transaction/v1/retrieve/date', http_build_query($params_array) ); + + return $curl_result; + } + + + /* + * _getHashKey method generate Hash key based on the API call (transaction date API). + * + * hash format (hash sequence) : + * $hash = merchant_key|merchant_email|transaction_date|salt + * + * params string $hash_sequence - holds the format of hash key (sequence). + * params array $params - holds the passed array. + * params string $salt - holds merchant salt key. + * + * ##Return values + * + * - return string $hash - holds the generated hash key. + * + * @params string $hash_sequence - holds the format of hash key (sequence). + * @params array $params - holds the passed array. + * @params string $salt - holds merchant salt key. + * + * @return string $hash - holds the generated hash key. + * + */ + function _getHashKey($posted, $salt_key){ + $hash_sequence = "merchant_key|merchant_email|transaction_date"; + + // make an array or split into array base on pipe sign. + $hash_sequence_array = explode( '|', $hash_sequence ); + $hash = null; + + // prepare a string based on hash sequence from the $params array. + foreach($hash_sequence_array as $value ) { + $hash .= isset($posted[$value]) ? $posted[$value] : ''; + $hash .= '|'; + } + + $hash .= $salt_key; + // generate hash key using hash function(predefine) and return + return strtolower( hash('sha512', $hash) ); + } + + + /* + * _curlCall method call CURL for get data from the API based on date + * + * params string $url - holds the payment URL which will be redirect to. + * params array $params_array - holds the passed array. + * + * ##Return values + * + * - return array with curl_status and data - holds the details. + * + * - return integer curl_status = 0 means error. + * + * - return integer curl_status = 1 means success. + * + * @params string $url - holds the payment URL which will be redirect to. + * @params array $params_array - holds the passed array. + * + * @return array with curl_status and data - holds the details. + * @return integer curl_status = 0 means error. + * @return integer curl_status = 1 means success and go the url link. + * + * ##Method call + * - curl_init() - Initializes a new session and return a cURL. + * - curl_setopt_array() - Set multiple options for a cURL transfer. + * - curl_exec() - Perform a cURL session. + * - curl_errno() - Return the last error number. + * - curl_error() - Return a string containing the last error for the current session. + * + * ##Used value + * - curl_status => 0 : means failure. + * - curl_status => 1 : means Success. + * + */ + function _curlCall($url, $params_array){ + + // Initializes a new session and return a cURL. + $cURL = curl_init(); + + // Set multiple options for a cURL transfer. + curl_setopt_array( + $cURL, + array ( + CURLOPT_URL => $url, + CURLOPT_POSTFIELDS => $params_array, + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_USERAGENT => 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/36.0.1985.125 Safari/537.36', + CURLOPT_SSL_VERIFYHOST => 0, + CURLOPT_SSL_VERIFYPEER => 0 + ) + ); + + // Perform a cURL session + $result = curl_exec($cURL); + + // check there is any error or not in curl execution. + if( curl_errno($cURL) ){ + $cURL_error = curl_error($cURL); + if( empty($cURL_error) ) + $cURL_error = 'Server Error'; + + return array( + 'status' => 0, + 'data' => $cURL_error + ); + } + + $temp_result = json_decode($result); + + return $temp_result; + } + +?> + diff --git a/app/Libraries/Tap/Payment.php b/app/Libraries/Tap/Payment.php new file mode 100755 index 000000000..d1ee54f21 --- /dev/null +++ b/app/Libraries/Tap/Payment.php @@ -0,0 +1,353 @@ +REQUIRED_CONFIG_VARS as $parm => $req_status) { + if (key_exists($parm,$config)) { + $this->CONFIG_VARS[$parm] = $config[$parm]; + }else{ + if($req_status){ + throw new \InvalidArgumentException("InvalidArgumentException $parm field"); + } + } + } + + } + + + + + + public function card(Request $request,$data){ + $this->cardValidator($data); + $IP = $request->ip(); + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.tap.company/v2/tokens", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => "{\"card\":{\"number\": ".$this->CARD_VARS['number']." ,\"exp_month\":".$this->CARD_VARS['exp_month'].",\"exp_year\":".$this->CARD_VARS['exp_year'].",\"cvc\":".$this->CARD_VARS['cvc'].",\"name\":\"".$this->CARD_VARS['name']."\",\"address\":{\"country\":\" ".$this->CARD_VARS['country']." \",\"line1\":\" ".$this->CARD_VARS['line1']." \",\"city\":\"".$this->CARD_VARS['city']."\",\"street\":\"".$this->CARD_VARS['street']."\",\"avenue\":\"".$this->CARD_VARS['avenue']."\"}},\"client_ip\":\"".$IP."\"}", + CURLOPT_HTTPHEADER => array( + "authorization: Bearer ".$this->CONFIG_VARS['company_tap_secret_key']." ", + "content-type: application/json" + ), + )); + + + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if ($err) { + throw new \InvalidArgumentException("InvalidArgumentException $err"); + } else { + $json_response = json_decode($response); + if (isset($json_response->errors) && is_array($json_response->errors) && count($json_response->errors) > 0) { + throw new \InvalidArgumentException("Error : ".$json_response->errors[0]->code." "); + } + + if (isset($json_response->object) && $json_response->object == "token") { + $this->CHARGE_VARS['source']['id'] = $json_response->id; + $CARD_SET = true; + } + + } + } + + + + + public function charge($data = [],$redirect = true){ + $this->chargeValidator($data); + $curl = curl_init(); + if($this->CARD_SET){ + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.tap.company/v2/charges", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => "{\"amount\":".$this->CHARGE_VARS['amount'].",\"currency\":\"".$this->CHARGE_VARS['currency']."\",\"threeDSecure\":".$this->CHARGE_VARS['threeDSecure'].",\"save_card\":".(string)$this->CHARGE_VARS['save_card'].",\"description\":\"".$this->CHARGE_VARS['description']."\", + \"statement_descriptor\":\"".$this->CHARGE_VARS['statement_descriptor']."\",\"metadata\":{\"udf1\":\"".$this->CHARGE_VARS['metadata']['udf1']."\", + \"udf2\":\"".$this->CHARGE_VARS['metadata']['udf2']."\"},\"reference\":{\"transaction\":\"".$this->CHARGE_VARS['reference']['transaction']."\", + \"order\":\"".$this->CHARGE_VARS['reference']['order']."\"},\"receipt\":{\"email\":".$this->CHARGE_VARS['receipt']['email'].", + \"sms\":".$this->CHARGE_VARS['receipt']['sms']."},\"customer\":{\"first_name\":\"".$this->CHARGE_VARS['customer']['first_name']."\", + \"middle_name\":\"".$this->CHARGE_VARS['customer']['middle_name']."\",\"last_name\":\"".$this->CHARGE_VARS['customer']['last_name']."\", + \"email\":\"".$this->CHARGE_VARS['customer']['email']."\",\"phone\":{\"country_code\":\"".$this->CHARGE_VARS['customer']['phone']['country_code']."\", + \"number\":\"".$this->CHARGE_VARS['customer']['phone']['number']."\"}}, + \"source\":{\"object\":\"token\",\"id\":\"".$this->CHARGE_VARS['source']['id']."\"},\"post\":{\"url\":\"".$this->CHARGE_VARS['post']['url']."\"}, + \"redirect\":{\"url\":\"".$this->CHARGE_VARS['redirect']['url']."\"}}", + CURLOPT_HTTPHEADER => array( + "authorization: Bearer ".$this->CONFIG_VARS['company_tap_secret_key']." ", + "content-type: application/json" + ), + )); + }else{ + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.tap.company/v2/charges", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => "{\"amount\":".$this->CHARGE_VARS['amount'].",\"currency\":\"".$this->CHARGE_VARS['currency']."\",\"threeDSecure\":".$this->CHARGE_VARS['threeDSecure'].",\"save_card\":".(string)$this->CHARGE_VARS['save_card'].",\"description\":\"".$this->CHARGE_VARS['description']."\", + \"statement_descriptor\":\"".$this->CHARGE_VARS['statement_descriptor']."\",\"metadata\":{\"udf1\":\"".$this->CHARGE_VARS['metadata']['udf1']."\", + \"udf2\":\"".$this->CHARGE_VARS['metadata']['udf2']."\"},\"reference\":{\"transaction\":\"".$this->CHARGE_VARS['reference']['transaction']."\", + \"order\":\"".$this->CHARGE_VARS['reference']['order']."\"},\"receipt\":{\"email\":".$this->CHARGE_VARS['receipt']['email'].", + \"sms\":".$this->CHARGE_VARS['receipt']['sms']."},\"customer\":{\"first_name\":\"".$this->CHARGE_VARS['customer']['first_name']."\", + \"middle_name\":\"".$this->CHARGE_VARS['customer']['middle_name']."\",\"last_name\":\"".$this->CHARGE_VARS['customer']['last_name']."\", + \"email\":\"".$this->CHARGE_VARS['customer']['email']."\",\"phone\":{\"country_code\":\"".$this->CHARGE_VARS['customer']['phone']['country_code']."\", + \"number\":\"".$this->CHARGE_VARS['customer']['phone']['number']."\"}},\"merchant\":{\"id\":\"".$this->CHARGE_VARS['merchant']['id']."\"}, + \"source\":{\"id\":\"".$this->CHARGE_VARS['source']['id']."\"},\"post\":{\"url\":\"".$this->CHARGE_VARS['post']['url']."\"}, + \"redirect\":{\"url\":\"".$this->CHARGE_VARS['redirect']['url']."\"}}", + CURLOPT_HTTPHEADER => array( + "authorization: Bearer ".$this->CONFIG_VARS['company_tap_secret_key']." ", + "content-type: application/json" + ), + )); + } + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if ($err) { + throw new \Exception("Exception $err"); + } else { + $json_response = json_decode($response); + + if (isset($json_response->errors) && is_array($json_response->errors) && count($json_response->errors) > 0) { + throw new \Exception("Error : ".$json_response->errors[0]->code.""); + } + if (isset($json_response->object) && $json_response->object == "charge" && isset($json_response->transaction->url)) { + if($redirect){ + return redirect($json_response->transaction->url); + } + return $json_response; + }else{ + + throw new \Exception("Error : ".$json_response." "); + } + } + + } + + public function getCharge($charge_id){ + if ($charge_id != null) { + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.tap.company/v2/charges/$charge_id", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_POSTFIELDS => "{}", + CURLOPT_HTTPHEADER => array( + "authorization: Bearer ".$this->CONFIG_VARS['company_tap_secret_key']." ", + ), + )); + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if ($err) { + throw new \Exception("Exception $err"); + } else { + $json_response = json_decode($response); + if (isset($json_response->errors) && is_array($json_response->errors) && count($json_response->errors) > 0) { + throw new \Exception("Error : ".$json_response->errors[0]->code." "); + } + if (isset($json_response->object) && $json_response->object == "charge" && isset($json_response->id)) { + return $json_response; + }else{ + throw new \Exception("Error : ".$response." "); + } + } + } + return false; + } + + public function chargesList($options = array()){ + $this->chargesListValidator($options); + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.tap.company/v2/charges/list", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => "{\"period\":{\"date\":{\"from\":".$this->CHARGES_FILTER['period']['date']['from'].",\"to\":".$this->CHARGES_FILTER['period']['date']['to']."}},\"status\":\" ".$this->CHARGES_FILTER['status']." \",\"limit\":".$this->CHARGES_FILTER['limit']."}", + CURLOPT_HTTPHEADER => array( + "authorization: Bearer ".$this->CONFIG_VARS['company_tap_secret_key']." ", + "content-type: application/json" + ), + )); + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if ($err) { + throw new \Exception("Exception $err"); + } else { + $json_response = json_decode($response); + if (isset($json_response->errors) && is_array($json_response->errors) && count($json_response->errors) > 0) { + throw new \Exception("Error : ".$json_response->errors[0]->code." "); + } + if (isset($json_response->object_type) && $json_response->object_type == "list") { + return $json_response; + }else{ + throw new \Exception("Error : ".$response." "); + } + } + + return false; + } + + public function refund($data = []){ + $this->refungValidator($data); + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.tap.company/v2/refunds", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => "{\"charge_id\":\"".$this->REFUND_VARS['charge_id']."\",\"amount\":".$this->REFUND_VARS['amount'].",\"currency\":\"".$this->REFUND_VARS['currency']."\",\"description\":\"".$this->REFUND_VARS['description']."\",\"reason\":\"".$this->REFUND_VARS['reason']."\", + \"reference\":{\"merchant\":\"".$this->REFUND_VARS['reference']['merchant']."\"},\"metadata\":{\"udf1\":\"".$this->REFUND_VARS['metadata']['udf1']."\",\"udf2\":\"".$this->REFUND_VARS['metadata']['udf2']."\"},\"post\":{\"url\":\"".$this->REFUND_VARS['post']['url']."\"}}", + CURLOPT_HTTPHEADER => array( + "authorization: Bearer ".$this->CONFIG_VARS['company_tap_secret_key']." ", + "content-type: application/json" + ), + )); + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if ($err) { + throw new \Exception("Exception $err"); + } else { + $json_response = json_decode($response); + if (isset($json_response->errors) && is_array($json_response->errors) && count($json_response->errors) > 0) { + throw new \Exception("Error : ".$response." "); + } + if (isset($json_response->object) && $json_response->object == "refund") { + return $json_response; + }else{ + throw new \Exception("Error : ".$response." "); + } + } + + return false; + + } + + public function getRefund($refund_id){ + if ($refund_id == null) { + throw new \InvalidArgumentException("InvalidArgumentException refund_id required"); + } + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.tap.company/v2/refunds/$refund_id", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "GET", + CURLOPT_POSTFIELDS => "{}", + CURLOPT_HTTPHEADER => array( + "authorization: Bearer ".$this->CONFIG_VARS['company_tap_secret_key']." ", + ), + )); + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if ($err) { + throw new \Exception("Exception $err"); + } else { + $json_response = json_decode($response); + if (isset($json_response->errors) && is_array($json_response->errors) && count($json_response->errors) > 0) { + throw new \Exception("Error : ".$json_response->errors[0]->code." "); + } + if (isset($json_response->object) && $json_response->object == "refund") { + return $json_response; + }else{ + throw new \Exception("Error : ".$response." "); + } + } + + return false; + } + + public function refundList($options = []){ + $this->refundsListValidator($options); + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "https://api.tap.company/v2/refunds/list", + CURLOPT_RETURNTRANSFER => true, + CURLOPT_ENCODING => "", + CURLOPT_MAXREDIRS => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_CUSTOMREQUEST => "POST", + CURLOPT_POSTFIELDS => "{\"period\":{\"date\":{\"from\":".$this->REFUNDS_FILTER['period']['date']['from'].",\"to\":".$this->REFUNDS_FILTER['period']['date']['to']."}},\"starting_after\":\"\",\"limit\":".$this->REFUNDS_FILTER['limit']."}", + CURLOPT_HTTPHEADER => array( + "authorization: Bearer ".$this->CONFIG_VARS['company_tap_secret_key']." ", + "content-type: application/json" + ), + )); + + $response = curl_exec($curl); + $err = curl_error($curl); + + curl_close($curl); + + if ($err) { + throw new \Exception("Exception $err"); + } else { + $json_response = json_decode($response); + if (isset($json_response->errors) && is_array($json_response->errors) && count($json_response->errors) > 0) { + throw new \Exception("Error : ".$json_response->errors[0]->code." "); + } + if (isset($json_response->object) && $json_response->object == "list") { + return $json_response; + }else{ + throw new \Exception("Error : ".$response." "); + } + } + + return false; + } +} diff --git a/app/Libraries/Tap/Reference.php b/app/Libraries/Tap/Reference.php new file mode 100755 index 000000000..fc2091d59 --- /dev/null +++ b/app/Libraries/Tap/Reference.php @@ -0,0 +1,306 @@ +true]; + protected $CONFIG_VARS = ['company_tap_secret_key'=>null]; + protected $CARD_VARS = ['number' => null,'exp_month' => null,'exp_year' => null,'cvc' => null,'name'=>null,'country'=>null,'line1'=>null,'city'=>null,'street'=>null,'avenue'=>null]; + protected $REQUIRED_CUSTOMER_VARS = ['name']; + protected $REQUIRED_CARD_VARS = ['number' => true,'exp_month' => true,'exp_year' => true,'cvc' => true]; + protected $REQUIRED_CHARGE_VARS = [ + 'customer' => [ + 'first_name' => true,'middle_name' => false,'last_name' => false,'email' => false, + 'phone' => [ + 'country_code' => false,'number' => false + ] + ], + 'address' => [ + 'country' => false,'city' => false,'line1' => false,'ip' => false + ], + 'amount' => true,'currency' => true,'save_card' => false,'threeDSecure' => true,'description' => true,'statement_descriptor' => false, + 'metadata' => [ + 'udf1' => false,'udf2' => false + ], + 'reference' => [ + 'transaction' => false,'order' => false + ], + 'receipt' => [ + 'email' => false,'sms' => false + ], + 'merchant' => [ + 'id' => false + ], + 'source'=>[ + 'id' => false + ], + 'post' => [ + 'url' => true + ], + 'redirect'=>[ + 'url' => true + ] + ]; + protected $CHARGE_VARS = [ + 'customer' => [ + 'first_name' => null,'middle_name'=>null,'last_name'=>null,'email'=>null, + 'phone'=> [ + 'country_code' => null, 'number' => null + ] + ], + 'address' => [ + 'country' => null,'city' => null,'line1' => null,'ip' => null + ], + 'amount' => null,'currency' => null,'save_card' => 'false','description' => null,'threeDSecure' => 'true','statement_descriptor' => null, + 'metadata' => [ + 'udf1' => null,'udf2' => null + ], + 'reference' => [ + 'transaction' => null,'order' => null + ], + 'receipt' => [ + 'email'=>'true','sms' => 'true' + ], + 'merchant' => [ + 'id' => null + ], + 'source'=>[ + 'id' => null + ], + 'post' => [ + 'url' => null + ], + 'redirect'=>[ + 'url' + ] + ]; + + protected $REFUND_VARS = [ + 'charge_id' => null, + 'amount' => null, + 'currency' => null, + 'description' => null, + 'reason' => null, + 'reference' => [ + 'merchant' => null + ], + 'metadata' => [ + 'udf1' => null, + 'udf2' => null, + ], + 'post' => [ + 'url' => null + ] + ]; + + protected $REQUIRED_REFUND_VARS = [ + 'charge_id' => true, + 'amount' => true, + 'currency' => true, + 'description' => false, + 'reason' => true, + 'reference' => [ + 'merchant' => false + ], + 'metadata' => [ + 'udf1' => false, + 'udf2' => false, + ], + 'post' => [ + 'url' => true + ] + ]; + + protected $CHARGES_FILTER = [ + 'period' => [ + 'date' => [ + 'from' => 'null', + 'to' => 'null' + ] + ], + 'status' => 'null', + 'limit' => 24 + ]; + + protected $REFUNDS_FILTER = [ + 'period' => [ + 'date' => [ + 'from' => 'null', + 'to' => 'null' + ] + ], + 'limit' => 24 + ]; + + protected $CHARGE_STATUS_LIST = [ + 'INITIATED','ABANDONED','CANCELLED','FAILED','DECLINED','RESTRICTED','CAPTURED','VOID','TIMEDOUT','UNKNOWN' + ]; + + + protected function cardValidator($data){ + foreach ($this->REQUIRED_CARD_VARS as $parm => $req_status) { + if (key_exists($parm,$data)) { + $this->CARD_VARS[$parm] = $data[$parm]; + }else{ + if($req_status){ + // missing required parm + throw new \InvalidArgumentException("InvalidArgumentException $parm field"); + } + } + } + } + + protected function chargeValidator($data){ + foreach ($this->REQUIRED_CHARGE_VARS as $Firstkey => $req_status) { + if (is_array($req_status)) { + $SecondArray = $this->REQUIRED_CHARGE_VARS[$Firstkey]; + foreach ($SecondArray as $Secondkey => $req_status2) { + if (is_array($req_status2)) { + $ThirdArray = $this->REQUIRED_CHARGE_VARS[$Firstkey][$Secondkey]; + foreach ($ThirdArray as $Thirdkey => $req_status3) { + if (isset($data[$Firstkey][$Secondkey]) && key_exists($Thirdkey,$data[$Firstkey][$Secondkey])) { + $this->CHARGE_VARS[$Firstkey][$Secondkey] = $data[$Firstkey][$Secondkey]; + }else{ + if($req_status3){ + // missing required parm + throw new \InvalidArgumentException("InvalidArgumentException $Firstkey.$Secondkey.$Thirdkey required"); + }else{ + if (in_array($Thirdkey,['country_code','number']) && $this->CHARGE_VARS[$Firstkey][$Secondkey][$Thirdkey] == null ) { + if (!isset($this->CHARGE_VARS['customer']['email']) || isset($this->CHARGE_VARS['customer']['email']) && $this->CHARGE_VARS['customer']['email'] == null) { + throw new \InvalidArgumentException("InvalidArgumentException $Firstkey.phone or $Firstkey.email is required"); + } + } + } + } + } + }else{ + if (isset($data[$Firstkey]) && key_exists($Secondkey,$data[$Firstkey])) { + $this->CHARGE_VARS[$Firstkey][$Secondkey] = $data[$Firstkey][$Secondkey]; + }else{ + if($req_status2){ + // missing required parm + throw new \InvalidArgumentException("InvalidArgumentException $Firstkey.$Secondkey required"); + } + } + } + } + }else{ + if (key_exists($Firstkey,$data)) { + $this->CHARGE_VARS[$Firstkey] = $data[$Firstkey]; + }else{ + if($req_status){ + // missing required parm + throw new \InvalidArgumentException("InvalidArgumentException $Firstkey field"); + } + } + } + } + } + + + protected function refungValidator($data){ + foreach ($this->REQUIRED_REFUND_VARS as $Firstkey => $req_status) { + if (is_array($req_status)) { + $SecondArray = $this->REQUIRED_REFUND_VARS[$Firstkey]; + foreach ($SecondArray as $Secondkey => $req_status2) { + if (isset($data[$Firstkey]) && key_exists($Secondkey,$data[$Firstkey])) { + $this->REFUND_VARS[$Firstkey][$Secondkey] = $data[$Firstkey][$Secondkey]; + }else{ + if($req_status2){ + // missing required parm + throw new \InvalidArgumentException("InvalidArgumentException $Firstkey.$Secondkey required"); + } + } + } + }else{ + if (key_exists($Firstkey,$data)) { + $this->REFUND_VARS[$Firstkey] = $data[$Firstkey]; + }else{ + if($req_status){ + // missing required parm + throw new \InvalidArgumentException("InvalidArgumentException $Firstkey field"); + } + } + } + } + } + + + protected function chargesListValidator($options){ + if (isset($options['period'])) { + if (isset($options['period']['date']['from'])) { + $strtotime = strtotime($options['period']['date']['from']); + if ($strtotime != false && $strtotime > 0) { + $this->CHARGES_FILTER['period']['date']['from'] = $strtotime; + }else{ + throw new \Exception("Exception period from date not valid !"); + } + } + + if (isset($options['period']['date']['to'])) { + $strtotime = strtotime($options['period']['date']['to']); + if ($strtotime != false && $strtotime > 0) { + $this->CHARGES_FILTER['period']['date']['to'] = $strtotime; + }else{ + throw new \Exception("Exception period to date not valid !"); + } + } + } + + if (isset($options['status'])) { + if (in_array($options['status'],$this->CHARGE_STATUS_LIST)) { + $this->CHARGES_FILTER['status'] = $options['status']; + }else{ + throw new \Exception("Exception charge status not valid !"); + } + } + + if (isset($options['limit'])) { + if (is_numeric($options['limit']) && $options['limit'] > 0 && $options['limit'] < 51) { + $this->CHARGES_FILTER['limit'] = $options['limit']; + }else{ + throw new \Exception("Exception charges limit not valid !"); + } + } + } + + + + protected function refundsListValidator($options){ + if (isset($options['period'])) { + if (isset($options['period']['date']['from'])) { + $strtotime = strtotime($options['period']['date']['from']); + if ($strtotime != false && $strtotime > 0) { + $this->REFUNDS_FILTER['period']['date']['from'] = $strtotime; + }else{ + throw new \Exception("Exception period from date not valid !"); + } + } + + if (isset($options['period']['date']['to'])) { + $strtotime = strtotime($options['period']['date']['to']); + if ($strtotime != false && $strtotime > 0) { + $this->REFUNDS_FILTER['period']['date']['to'] = $strtotime; + }else{ + throw new \Exception("Exception period to date not valid !"); + } + } + } + + if (isset($options['limit'])) { + if (is_numeric($options['limit']) && $options['limit'] > 0 && $options['limit'] < 51) { + $this->REFUNDS_FILTER['limit'] = $options['limit']; + }else{ + throw new \Exception("Exception refunds limit not valid !"); + } + } + } + + + + + + +} diff --git a/app/Libraries/Tap/Tap.php b/app/Libraries/Tap/Tap.php new file mode 100755 index 000000000..6ce89dabc --- /dev/null +++ b/app/Libraries/Tap/Tap.php @@ -0,0 +1,19 @@ +publishes([ + __DIR__.'/../config/tap_payment.php' => config_path('tap_payment.php'), + ], 'tap_payment-config'); + + } + + /** + * Register the service provider. + * + * @return void + */ + public function register() + { + $packageConfigFile = __DIR__.'/../config/tap_payment.php'; + + $this->mergeConfigFrom( + $packageConfigFile, 'tap_payment' + ); + + //$this->registerBindings(); + } + + + /** + * Registers app bindings and aliases. + */ + protected function registerBindings() + { + $this->app->singleton(Payment::class, function () { + return new Payment(); + }); + + $this->app->alias(Payment::class, 'Payment'); + } +} diff --git a/app/Listeners/SendUserCreatedEmail.php b/app/Listeners/SendUserCreatedEmail.php new file mode 100644 index 000000000..1d04da4e6 --- /dev/null +++ b/app/Listeners/SendUserCreatedEmail.php @@ -0,0 +1,63 @@ +user; + $plainPassword = $event->plainPassword; + + // Prevent duplicate processing + $userKey = $user->id . '_' . $user->updated_at->timestamp; + if (in_array($userKey, self::$processedUsers)) { + return; + } + + self::$processedUsers[] = $userKey; + + // Prepare email variables + $variables = [ + '{app_url}' => config('app.url'), + '{user_name}' => $user->name, + '{user_email}' => $user->email, + '{user_password}' => $plainPassword ?: 'Password set by user', + '{user_type}' => ucfirst($user->type), + '{app_name}' => config('app.name'), + '{created_date}' => $user->created_at->format('Y-m-d H:i:s'), + ]; + + try { + // Send welcome email to the newly created user in their language + $userLanguage = $user->lang ?? 'en'; + $this->emailService->sendTemplateEmailWithLanguage( + templateName: 'User Created', + variables: $variables, + toEmail: $user->email, + toName: $user->name, + language: $userLanguage + ); + + // Trigger webhooks for New User + $this->webhookService->triggerWebhooks('New User', $user->toArray(), $user->created_by ?? $user->id); + + } catch (Exception $e) { + // Store error in session for frontend notification + session()->flash('email_error', 'Failed to send welcome email: ' . $e->getMessage()); + } + } +} \ No newline at end of file diff --git a/app/Mail/TestMail.php b/app/Mail/TestMail.php new file mode 100644 index 000000000..7fc48b2cf --- /dev/null +++ b/app/Mail/TestMail.php @@ -0,0 +1,31 @@ +subject('Test Email from ' . config('app.name')) + ->view('emails.test'); + } +} \ No newline at end of file diff --git a/app/Models/ActionItem.php b/app/Models/ActionItem.php new file mode 100644 index 000000000..dc62b6056 --- /dev/null +++ b/app/Models/ActionItem.php @@ -0,0 +1,57 @@ + 'date', + 'completed_date' => 'date', + ]; + + public function meeting() + { + return $this->belongsTo(Meeting::class); + } + + public function assignee() + { + return $this->belongsTo(User::class, 'assigned_to'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function getIsOverdueAttribute() + { + return $this->status !== 'Completed' && $this->due_date < Carbon::today(); + } + + public function getDaysRemainingAttribute() + { + if ($this->status === 'Completed') return null; + return Carbon::today()->diffInDays($this->due_date, false); + } +} \ No newline at end of file diff --git a/app/Models/Announcement.php b/app/Models/Announcement.php new file mode 100644 index 000000000..4ad1cd663 --- /dev/null +++ b/app/Models/Announcement.php @@ -0,0 +1,111 @@ + 'date', + 'end_date' => 'date', + 'is_featured' => 'boolean', + 'is_high_priority' => 'boolean', + 'is_company_wide' => 'boolean', + ]; + + /** + * Get the user who created this announcement. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the departments this announcement is targeted to. + */ + public function departments() + { + return $this->belongsToMany(Department::class, 'announcement_department'); + } + + /** + * Get the branches this announcement is targeted to. + */ + public function branches() + { + return $this->belongsToMany(Branch::class, 'announcement_branch'); + } + + /** + * Get the employees who have viewed this announcement. + */ + public function viewedBy() + { + return $this->belongsToMany(User::class, 'announcement_views', 'announcement_id', 'employee_id') + ->withPivot('viewed_at') + ->withTimestamps(); + } + + /** + * Check if the announcement is active based on start and end dates. + */ + public function isActive() + { + $today = now()->startOfDay(); + + if ($this->end_date) { + return $this->start_date->lte($today) && $this->end_date->gte($today); + } + + return $this->start_date->lte($today); + } + + /** + * Scope a query to only include active announcements. + */ + public function scopeActive($query) + { + $today = now()->format('Y-m-d'); + + return $query->where('start_date', '<=', $today) + ->where(function ($q) use ($today) { + $q->whereNull('end_date') + ->orWhere('end_date', '>=', $today); + }); + } + + /** + * Scope a query to only include featured announcements. + */ + public function scopeFeatured($query) + { + return $query->where('is_featured', true); + } + + /** + * Scope a query to only include high priority announcements. + */ + public function scopeHighPriority($query) + { + return $query->where('is_high_priority', true); + } +} \ No newline at end of file diff --git a/app/Models/AnnouncementView.php b/app/Models/AnnouncementView.php new file mode 100644 index 000000000..3957e2294 --- /dev/null +++ b/app/Models/AnnouncementView.php @@ -0,0 +1,37 @@ + 'datetime', + ]; + + /** + * Get the announcement that was viewed. + */ + public function announcement() + { + return $this->belongsTo(Announcement::class); + } + + /** + * Get the employee who viewed the announcement. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } +} \ No newline at end of file diff --git a/app/Models/Asset.php b/app/Models/Asset.php new file mode 100644 index 000000000..93cd9d4ff --- /dev/null +++ b/app/Models/Asset.php @@ -0,0 +1,117 @@ + 'date', + 'warranty_expiry_date' => 'date', + 'purchase_cost' => 'decimal:2', + ]; + + /** + * Get the asset type of this asset. + */ + public function assetType() + { + return $this->belongsTo(AssetType::class); + } + + /** + * Get the user who created this asset. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the assignments for this asset. + */ + public function assignments() + { + return $this->hasMany(AssetAssignment::class); + } + + /** + * Get the current assignment for this asset. + */ + public function currentAssignment() + { + return $this->hasOne(AssetAssignment::class)->whereNull('checkin_date')->latest(); + } + + /** + * Get the maintenances for this asset. + */ + public function maintenances() + { + return $this->hasMany(AssetMaintenance::class); + } + + /** + * Get the depreciation for this asset. + */ + public function depreciation() + { + return $this->hasOne(AssetDepreciation::class); + } + + /** + * Scope a query to only include available assets. + */ + public function scopeAvailable($query) + { + return $query->where('status', 'available'); + } + + /** + * Scope a query to only include assigned assets. + */ + public function scopeAssigned($query) + { + return $query->where('status', 'assigned'); + } + + /** + * Scope a query to only include assets under maintenance. + */ + public function scopeUnderMaintenance($query) + { + return $query->where('status', 'under_maintenance'); + } + + /** + * Scope a query to only include disposed assets. + */ + public function scopeDisposed($query) + { + return $query->where('status', 'disposed'); + } +} \ No newline at end of file diff --git a/app/Models/AssetAssignment.php b/app/Models/AssetAssignment.php new file mode 100644 index 000000000..6c417ce9c --- /dev/null +++ b/app/Models/AssetAssignment.php @@ -0,0 +1,92 @@ + 'date', + 'expected_return_date' => 'date', + 'checkin_date' => 'date', + 'acknowledged_at' => 'datetime', + 'is_acknowledged' => 'boolean', + ]; + + /** + * Get the asset that was assigned. + */ + public function asset() + { + return $this->belongsTo(Asset::class); + } + + /** + * Get the employee to whom the asset was assigned. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the user who assigned the asset. + */ + public function assigner() + { + return $this->belongsTo(User::class, 'assigned_by'); + } + + /** + * Get the user who received the asset back. + */ + public function receiver() + { + return $this->belongsTo(User::class, 'received_by'); + } + + /** + * Scope a query to only include active assignments. + */ + public function scopeActive($query) + { + return $query->whereNull('checkin_date'); + } + + /** + * Scope a query to only include completed assignments. + */ + public function scopeCompleted($query) + { + return $query->whereNotNull('checkin_date'); + } + + /** + * Scope a query to only include overdue assignments. + */ + public function scopeOverdue($query) + { + return $query->whereNull('checkin_date') + ->whereNotNull('expected_return_date') + ->where('expected_return_date', '<', now()); + } +} \ No newline at end of file diff --git a/app/Models/AssetDepreciation.php b/app/Models/AssetDepreciation.php new file mode 100644 index 000000000..9f1799a5a --- /dev/null +++ b/app/Models/AssetDepreciation.php @@ -0,0 +1,91 @@ + 'date', + 'salvage_value' => 'decimal:2', + 'current_value' => 'decimal:2', + 'useful_life_years' => 'integer', + ]; + + /** + * Get the asset that is being depreciated. + */ + public function asset() + { + return $this->belongsTo(Asset::class); + } + + /** + * Get the user who created this depreciation record. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Calculate the current value of the asset. + */ + public function calculateCurrentValue() + { + $asset = $this->asset; + $purchaseDate = $asset->purchase_date; + $purchaseCost = $asset->purchase_cost; + $today = now(); + + if (!$purchaseDate || !$purchaseCost) { + return $purchaseCost; + } + + $ageInYears = $purchaseDate->diffInDays($today) / 365; + + if ($this->method === 'straight_line') { + // Straight-line depreciation + $annualDepreciation = ($purchaseCost - $this->salvage_value) / $this->useful_life_years; + $totalDepreciation = min($ageInYears, $this->useful_life_years) * $annualDepreciation; + $currentValue = $purchaseCost - $totalDepreciation; + } elseif ($this->method === 'reducing_balance') { + // Reducing balance depreciation + $depreciationRate = 1 - pow($this->salvage_value / $purchaseCost, 1 / $this->useful_life_years); + $currentValue = $purchaseCost * pow(1 - $depreciationRate, min($ageInYears, $this->useful_life_years)); + } else { + // Default to straight-line if method is not recognized + $annualDepreciation = ($purchaseCost - $this->salvage_value) / $this->useful_life_years; + $totalDepreciation = min($ageInYears, $this->useful_life_years) * $annualDepreciation; + $currentValue = $purchaseCost - $totalDepreciation; + } + + // Ensure current value doesn't go below salvage value + return max($currentValue, $this->salvage_value); + } + + /** + * Update the current value of the asset. + */ + public function updateCurrentValue() + { + $this->current_value = $this->calculateCurrentValue(); + $this->last_calculated_date = now(); + $this->save(); + + return $this->current_value; + } +} \ No newline at end of file diff --git a/app/Models/AssetMaintenance.php b/app/Models/AssetMaintenance.php new file mode 100644 index 000000000..db8b160ad --- /dev/null +++ b/app/Models/AssetMaintenance.php @@ -0,0 +1,78 @@ + 'date', + 'end_date' => 'date', + 'cost' => 'decimal:2', + ]; + + /** + * Get the asset that is being maintained. + */ + public function asset() + { + return $this->belongsTo(Asset::class); + } + + /** + * Get the user who created this maintenance record. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Scope a query to only include scheduled maintenances. + */ + public function scopeScheduled($query) + { + return $query->where('status', 'scheduled'); + } + + /** + * Scope a query to only include in-progress maintenances. + */ + public function scopeInProgress($query) + { + return $query->where('status', 'in_progress'); + } + + /** + * Scope a query to only include completed maintenances. + */ + public function scopeCompleted($query) + { + return $query->where('status', 'completed'); + } + + /** + * Scope a query to only include cancelled maintenances. + */ + public function scopeCancelled($query) + { + return $query->where('status', 'cancelled'); + } +} \ No newline at end of file diff --git a/app/Models/AssetType.php b/app/Models/AssetType.php new file mode 100644 index 000000000..4d326163c --- /dev/null +++ b/app/Models/AssetType.php @@ -0,0 +1,33 @@ +belongsTo(User::class, 'created_by'); + } + + /** + * Get the assets of this type. + */ + public function assets() + { + return $this->hasMany(Asset::class); + } +} \ No newline at end of file diff --git a/app/Models/AttendancePolicy.php b/app/Models/AttendancePolicy.php new file mode 100644 index 000000000..ce404ee94 --- /dev/null +++ b/app/Models/AttendancePolicy.php @@ -0,0 +1,65 @@ + 'decimal:2', + ]; + + /** + * Get the user who created the policy. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Check if arrival time is late. + */ + public function isLateArrival($actualTime, $expectedTime) + { + $actual = \Carbon\Carbon::parse($actualTime); + $expected = \Carbon\Carbon::parse($expectedTime); + $graceMinutes = $this->late_arrival_grace; + + return $actual->gt($expected->addMinutes($graceMinutes)); + } + + /** + * Check if departure time is early. + */ + public function isEarlyDeparture($actualTime, $expectedTime) + { + $actual = \Carbon\Carbon::parse($actualTime); + $expected = \Carbon\Carbon::parse($expectedTime); + $graceMinutes = $this->early_departure_grace; + + return $actual->lt($expected->subMinutes($graceMinutes)); + } + + /** + * Calculate overtime amount. + */ + public function calculateOvertimeAmount($overtimeHours) + { + return $overtimeHours * $this->overtime_rate_per_hour; + } +} \ No newline at end of file diff --git a/app/Models/AttendanceRecord.php b/app/Models/AttendanceRecord.php new file mode 100644 index 000000000..82e19f754 --- /dev/null +++ b/app/Models/AttendanceRecord.php @@ -0,0 +1,238 @@ + 'date', + 'break_hours' => 'decimal:2', + 'overtime_hours' => 'decimal:2', + 'overtime_amount' => 'decimal:2', + 'is_late' => 'boolean', + 'is_early_departure' => 'boolean', + 'is_absent' => 'boolean', + 'is_holiday' => 'boolean', + 'is_weekend' => 'boolean', + ]; + + /** + * Get the employee. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the shift. + */ + public function shift() + { + return $this->belongsTo(Shift::class); + } + + /** + * Get the attendance policy. + */ + public function attendancePolicy() + { + return $this->belongsTo(AttendancePolicy::class); + } + + /** + * Get the user who created the record. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Calculate total working hours. + */ + public function calculateTotalHours() + { + if ($this->clock_in && $this->clock_out) { + $clockIn = Carbon::parse($this->clock_in); + $clockOut = Carbon::parse($this->clock_out); + + // Handle next day clock out (night shifts) + if ($clockOut->lt($clockIn)) { + $clockOut->addDay(); + } + + $totalMinutes = abs($clockOut->diffInMinutes($clockIn)); + + // Use shift's break times for accurate calculation + $breakMinutes = 0; + if ($this->shift && $this->shift->break_start_time && $this->shift->break_end_time) { + $breakStart = Carbon::parse($this->shift->break_start_time); + $breakEnd = Carbon::parse($this->shift->break_end_time); + + // Handle next day break times for night shifts + if ($breakEnd->lt($breakStart)) { + $breakEnd->addDay(); + } + + // Only deduct break if employee worked through the break period + if ($clockIn->lte($breakStart) && $clockOut->gte($breakEnd)) { + // Worked through entire break - deduct full break + $breakMinutes = $this->shift->break_duration; + } elseif ($clockIn->lte($breakStart) && $clockOut->gt($breakStart) && $clockOut->lte($breakEnd)) { + // Left during break - deduct time spent on break + $breakMinutes = abs($clockOut->diffInMinutes($breakStart)); + } elseif ($clockIn->gt($breakStart) && $clockIn->lt($breakEnd) && $clockOut->gte($breakEnd)) { + // Came during break - deduct partial break (missed part of break) + $breakMinutes = abs($breakEnd->diffInMinutes($clockIn)); + } elseif ($clockIn->gt($breakStart) && $clockOut->lt($breakEnd)) { + // Came and left during break - no break deduction + $breakMinutes = 0; + } + } + + $workingMinutes = max(0, $totalMinutes - $breakMinutes); + $calculatedHours = round($workingMinutes / 60, 2); + + $this->attributes['total_hours'] = $calculatedHours; + $this->attributes['break_hours'] = round($breakMinutes / 60, 2); + } else { + $this->attributes['total_hours'] = 0; + $this->attributes['break_hours'] = 0; + } + + return $this->attributes['total_hours'] ?? 0; + } + + /** + * Check if employee is late. + */ + public function checkLateArrival() + { + if ($this->shift && $this->clock_in && $this->attendancePolicy) { + $expectedTime = $this->shift->start_time; + $this->is_late = $this->attendancePolicy->isLateArrival($this->clock_in, $expectedTime); + } + + return $this->is_late; + } + + /** + * Check if employee left early. + */ + public function checkEarlyDeparture() + { + if ($this->shift && $this->clock_out && $this->attendancePolicy) { + $expectedTime = $this->shift->end_time; + $this->is_early_departure = $this->attendancePolicy->isEarlyDeparture($this->clock_out, $expectedTime); + } + + return $this->is_early_departure; + } + + /** + * Process complete attendance - calculate everything automatically. + */ + public function processAttendance() + { + // Step 1: Calculate total working hours first + $this->calculateTotalHours(); + + // Step 2: Calculate overtime using shift working hours dynamically + if ($this->shift && $this->shift->working_hours > 0) { + $standardHours = $this->shift->working_hours; // Use actual shift hours + } else { + $standardHours = 8; // Fallback to 8 hours if no shift or invalid hours + } + + $this->overtime_hours = max(0, round($this->total_hours - $standardHours, 2)); + + // Step 3: Calculate overtime amount using policy + if ($this->overtime_hours > 0 && $this->attendancePolicy) { + $this->overtime_amount = round($this->overtime_hours * $this->attendancePolicy->overtime_rate_per_hour, 2); + } else { + $this->overtime_amount = 0; + } + + // Step 4: Check late arrival and early departure + if ($this->clock_in && $this->clock_out) { + $this->checkLateArrival(); + $this->checkEarlyDeparture(); + } + + // Step 5: Set status based on holiday or total hours (only if not manually set) + if ($this->is_holiday) { + $this->status = 'holiday'; + } elseif ($this->exists || $this->isDirty('clock_in') || $this->isDirty('clock_out')) { + // Only auto-calculate status for new records or when times change + // $presentThreshold = $standardHours; + // $halfDayThreshold = $presentThreshold / 2; + + // if ($this->total_hours >= $halfDayThreshold) { + // $this->status = 'present'; + // } elseif ($this->total_hours > 0 && $this->total_hours < $halfDayThreshold) { + // $this->status = 'half_day'; + // } else { + // $this->status = 'absent'; + // } + + $fullDayThreshold = $standardHours; // e.g. 8 hours + $halfDayThreshold = $standardHours / 2; // e.g. 4 hours + + if ($this->total_hours >= $fullDayThreshold) { + $this->status = 'present'; + } elseif ($this->total_hours >= $halfDayThreshold) { + $this->status = 'half_day'; + } elseif ($this->total_hours > 0) { + $this->status = 'absent'; // or mark as short_leave if needed + } else { + $this->status = 'absent'; + } + } + // If record exists and times haven't changed, keep manual status + + $this->save(); + } + + /** + * Format clock in time for frontend (H:i format). + */ + public function getClockInAttribute($value) + { + return $value ? \Carbon\Carbon::parse($value)->format('H:i') : null; + } + + /** + * Format clock out time for frontend (H:i format). + */ + public function getClockOutAttribute($value) + { + return $value ? \Carbon\Carbon::parse($value)->format('H:i') : null; + } +} diff --git a/app/Models/AttendanceRegularization.php b/app/Models/AttendanceRegularization.php new file mode 100644 index 000000000..7b240d306 --- /dev/null +++ b/app/Models/AttendanceRegularization.php @@ -0,0 +1,81 @@ + 'date', + 'approved_at' => 'datetime', + ]; + + /** + * Get the employee. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the attendance record. + */ + public function attendanceRecord() + { + return $this->belongsTo(AttendanceRecord::class); + } + + /** + * Get the manager who approved/rejected. + */ + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Get the user who created the regularization. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Apply approved regularization to attendance record. + */ + public function applyToAttendanceRecord() + { + if ($this->status === 'approved' && $this->attendanceRecord) { + // Update the attendance record with requested times + $this->attendanceRecord->update([ + 'clock_in' => $this->requested_clock_in, + 'clock_out' => $this->requested_clock_out, + ]); + + // Process complete attendance calculation with shift and policy + $this->attendanceRecord->processAttendance(); + } + } +} \ No newline at end of file diff --git a/app/Models/Award.php b/app/Models/Award.php new file mode 100644 index 000000000..51a6d7270 --- /dev/null +++ b/app/Models/Award.php @@ -0,0 +1,47 @@ +belongsTo(User::class, 'employee_id'); + } + + /** + * Get the award type of this award. + */ + public function awardType() + { + return $this->belongsTo(AwardType::class); + } + + /** + * Get the user who created this award. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/AwardType.php b/app/Models/AwardType.php new file mode 100644 index 000000000..36bdc7499 --- /dev/null +++ b/app/Models/AwardType.php @@ -0,0 +1,34 @@ +hasMany(Award::class); + } + + /** + * Get the user who created this award type. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/BaseAuthenticatable.php b/app/Models/BaseAuthenticatable.php new file mode 100644 index 000000000..8059f51e6 --- /dev/null +++ b/app/Models/BaseAuthenticatable.php @@ -0,0 +1,23 @@ +getTable(); + return $this->applyPermissionScope($query, $tableName); + } +} \ No newline at end of file diff --git a/app/Models/BaseModel.php b/app/Models/BaseModel.php new file mode 100644 index 000000000..6bf9823a6 --- /dev/null +++ b/app/Models/BaseModel.php @@ -0,0 +1,23 @@ +getTable(); + return $this->applyPermissionScope($query, $tableName); + } +} \ No newline at end of file diff --git a/app/Models/BaseSpatiePermission.php b/app/Models/BaseSpatiePermission.php new file mode 100644 index 000000000..5b2ead3d3 --- /dev/null +++ b/app/Models/BaseSpatiePermission.php @@ -0,0 +1,23 @@ +getTable(); + return $this->applyPermissionScope($query, $tableName); + } +} \ No newline at end of file diff --git a/app/Models/BaseSpatieRole.php b/app/Models/BaseSpatieRole.php new file mode 100644 index 000000000..9f9f7f73e --- /dev/null +++ b/app/Models/BaseSpatieRole.php @@ -0,0 +1,23 @@ +getTable(); + return $this->applyPermissionScope($query, $tableName); + } +} \ No newline at end of file diff --git a/app/Models/Branch.php b/app/Models/Branch.php new file mode 100644 index 000000000..70e3efbef --- /dev/null +++ b/app/Models/Branch.php @@ -0,0 +1,38 @@ + 'string', + ]; + + public function company() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function departments() + { + return $this->hasMany(Department::class); + } +} \ No newline at end of file diff --git a/app/Models/Candidate.php b/app/Models/Candidate.php new file mode 100644 index 000000000..e711f57d7 --- /dev/null +++ b/app/Models/Candidate.php @@ -0,0 +1,117 @@ + 'date', + 'date_of_birth' => 'date', + 'current_salary' => 'decimal:2', + 'expected_salary' => 'decimal:2', + 'custom_question' => 'array', + 'is_archive' => 'boolean', + 'is_employee' => 'boolean', + ]; + + public function job() + { + return $this->belongsTo(JobPosting::class); + } + + public function source() + { + return $this->belongsTo(CandidateSource::class); + } + + public function referralEmployee() + { + return $this->belongsTo(User::class, 'referral_employee_id'); + } + + public function department() + { + return $this->belongsTo(Department::class); + } + + public function branch() + { + return $this->belongsTo(Branch::class); + } + + public function location() + { + return $this->belongsTo(JobLocation::class, 'location_id'); + } + + public function jobType() + { + return $this->belongsTo(JobType::class, 'job_type_id'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function interviews() + { + return $this->hasMany(Interview::class); + } + + public function assessments() + { + return $this->hasMany(CandidateAssessment::class); + } + + public function getFullNameAttribute() + { + return $this->first_name . ' ' . $this->last_name; + } +} \ No newline at end of file diff --git a/app/Models/CandidateAssessment.php b/app/Models/CandidateAssessment.php new file mode 100644 index 000000000..325b835d4 --- /dev/null +++ b/app/Models/CandidateAssessment.php @@ -0,0 +1,50 @@ + 'date', + ]; + + public function candidate() + { + return $this->belongsTo(Candidate::class); + } + + public function conductor() + { + return $this->belongsTo(User::class, 'conducted_by'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function getScorePercentageAttribute() + { + if (!$this->max_score || $this->max_score == 0) { + return null; + } + return round(($this->score / $this->max_score) * 100, 2); + } +} \ No newline at end of file diff --git a/app/Models/CandidateOnboarding.php b/app/Models/CandidateOnboarding.php new file mode 100644 index 000000000..56faffd63 --- /dev/null +++ b/app/Models/CandidateOnboarding.php @@ -0,0 +1,52 @@ + 'date', + ]; + + public function candidate() + { + return $this->belongsTo(Candidate::class); + } + + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + public function checklist() + { + return $this->belongsTo(OnboardingChecklist::class); + } + + public function buddyEmployee() + { + return $this->belongsTo(User::class, 'buddy_employee_id'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/CandidateSource.php b/app/Models/CandidateSource.php new file mode 100644 index 000000000..65fad82e9 --- /dev/null +++ b/app/Models/CandidateSource.php @@ -0,0 +1,28 @@ +belongsTo(User::class, 'created_by'); + } + + public function candidates() + { + return $this->hasMany(Candidate::class, 'source_id'); + } +} \ No newline at end of file diff --git a/app/Models/Category.php b/app/Models/Category.php new file mode 100644 index 000000000..2360afaa0 --- /dev/null +++ b/app/Models/Category.php @@ -0,0 +1,26 @@ +hasMany(Product::class); + } +} \ No newline at end of file diff --git a/app/Models/ChecklistItem.php b/app/Models/ChecklistItem.php new file mode 100644 index 000000000..2233ec698 --- /dev/null +++ b/app/Models/ChecklistItem.php @@ -0,0 +1,37 @@ + 'boolean', + ]; + + public function checklist() + { + return $this->belongsTo(OnboardingChecklist::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/Complaint.php b/app/Models/Complaint.php new file mode 100644 index 000000000..e41253a02 --- /dev/null +++ b/app/Models/Complaint.php @@ -0,0 +1,72 @@ + 'date', + 'resolution_deadline' => 'date', + 'resolution_date' => 'date', + 'follow_up_date' => 'date', + 'is_anonymous' => 'boolean', + ]; + + /** + * Get the employee who filed the complaint. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the employee against whom the complaint was filed. + */ + public function againstEmployee() + { + return $this->belongsTo(User::class, 'against_employee_id'); + } + + /** + * Get the user who is assigned to handle this complaint. + */ + public function assignedUser() + { + return $this->belongsTo(User::class, 'assigned_to'); + } + + /** + * Get the user who created this complaint. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/Contact.php b/app/Models/Contact.php new file mode 100644 index 000000000..f2c5d0703 --- /dev/null +++ b/app/Models/Contact.php @@ -0,0 +1,30 @@ + 'datetime', + 'updated_at' => 'datetime', + ]; + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} diff --git a/app/Models/ContractRenewal.php b/app/Models/ContractRenewal.php new file mode 100644 index 000000000..944028644 --- /dev/null +++ b/app/Models/ContractRenewal.php @@ -0,0 +1,72 @@ + 'date', + 'new_start_date' => 'date', + 'new_end_date' => 'date', + 'approved_at' => 'datetime', + 'new_allowances' => 'array', + 'new_benefits' => 'array', + 'new_basic_salary' => 'decimal:2', + ]; + + public function contract() + { + return $this->belongsTo(EmployeeContract::class); + } + + public function requester() + { + return $this->belongsTo(User::class, 'requested_by'); + } + + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function getNewTotalCompensationAttribute() + { + $total = $this->new_basic_salary; + if ($this->new_allowances && is_array($this->new_allowances)) { + foreach ($this->new_allowances as $allowance) { + $total += $allowance['amount'] ?? 0; + } + } + return $total; + } +} \ No newline at end of file diff --git a/app/Models/ContractTemplate.php b/app/Models/ContractTemplate.php new file mode 100644 index 000000000..82e27505a --- /dev/null +++ b/app/Models/ContractTemplate.php @@ -0,0 +1,50 @@ + 'array', + 'clauses' => 'array', + 'is_default' => 'boolean', + ]; + + public function contractType() + { + return $this->belongsTo(ContractType::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function generateContract($variables = []) + { + $content = $this->template_content; + + foreach ($variables as $key => $value) { + $content = str_replace('{{' . $key . '}}', $value, $content); + } + + return $content; + } +} \ No newline at end of file diff --git a/app/Models/ContractType.php b/app/Models/ContractType.php new file mode 100644 index 000000000..857af2ff1 --- /dev/null +++ b/app/Models/ContractType.php @@ -0,0 +1,36 @@ + 'boolean', + ]; + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function contracts() + { + return $this->hasMany(EmployeeContract::class, 'contract_type_id'); + } +} \ No newline at end of file diff --git a/app/Models/Coupon.php b/app/Models/Coupon.php new file mode 100644 index 000000000..4e4c8c379 --- /dev/null +++ b/app/Models/Coupon.php @@ -0,0 +1,37 @@ + 'decimal:2', + 'maximum_spend' => 'decimal:2', + 'discount_amount' => 'decimal:2', + 'expiry_date' => 'date', + 'status' => 'boolean' + ]; + + public function creator(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } +} diff --git a/app/Models/Currency.php b/app/Models/Currency.php new file mode 100644 index 000000000..a998eca51 --- /dev/null +++ b/app/Models/Currency.php @@ -0,0 +1,20 @@ + 'boolean' + ]; +} diff --git a/app/Models/CustomQuestion.php b/app/Models/CustomQuestion.php new file mode 100644 index 000000000..d904f8ede --- /dev/null +++ b/app/Models/CustomQuestion.php @@ -0,0 +1,26 @@ + 'integer', + ]; + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/Department.php b/app/Models/Department.php new file mode 100644 index 000000000..2e90695fc --- /dev/null +++ b/app/Models/Department.php @@ -0,0 +1,48 @@ +belongsTo(Branch::class); + } + + /** + * Get the user who created the department. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the employees assigned to this department. + */ + public function employees() + { + return $this->hasMany(Employee::class); + } + + public function desginations() + { + return $this->hasMany(Designation::class,'department_id','id'); + } +} \ No newline at end of file diff --git a/app/Models/Designation.php b/app/Models/Designation.php new file mode 100644 index 000000000..5bcbc59ef --- /dev/null +++ b/app/Models/Designation.php @@ -0,0 +1,33 @@ + 'string', + ]; + + public function company() + { + return $this->belongsTo(User::class, 'company_id'); + } + + public function department() + { + return $this->belongsTo(Department::class); + } +} \ No newline at end of file diff --git a/app/Models/DocumentAcknowledgment.php b/app/Models/DocumentAcknowledgment.php new file mode 100644 index 000000000..68011036b --- /dev/null +++ b/app/Models/DocumentAcknowledgment.php @@ -0,0 +1,69 @@ + 'datetime', + 'due_date' => 'date', + 'assigned_at' => 'datetime', + ]; + + public function document() + { + return $this->belongsTo(HrDocument::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function assignedBy() + { + return $this->belongsTo(User::class, 'assigned_by'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function getIsOverdueAttribute() + { + return $this->status === 'Pending' && $this->due_date && $this->due_date < Carbon::today(); + } + + public function getDaysOverdueAttribute() + { + if (!$this->is_overdue) return 0; + return Carbon::today()->diffInDays($this->due_date); + } + + public function getDaysRemainingAttribute() + { + if ($this->status !== 'Pending' || !$this->due_date) return null; + return Carbon::today()->diffInDays($this->due_date, false); + } +} \ No newline at end of file diff --git a/app/Models/DocumentCategory.php b/app/Models/DocumentCategory.php new file mode 100644 index 000000000..276e56294 --- /dev/null +++ b/app/Models/DocumentCategory.php @@ -0,0 +1,36 @@ + 'boolean', + ]; + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function documents() + { + return $this->hasMany(HrDocument::class, 'category_id'); + } +} \ No newline at end of file diff --git a/app/Models/DocumentTemplate.php b/app/Models/DocumentTemplate.php new file mode 100644 index 000000000..86baefa56 --- /dev/null +++ b/app/Models/DocumentTemplate.php @@ -0,0 +1,60 @@ + 'array', + 'default_values' => 'array', + 'is_default' => 'boolean', + ]; + + public function category() + { + return $this->belongsTo(DocumentCategory::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function generateDocument($values = []) + { + $content = $this->template_content; + + // Merge default values with provided values + $allValues = array_merge($this->default_values ?? [], $values); + + // Replace placeholders + foreach ($allValues as $key => $value) { + $content = str_replace('{{' . $key . '}}', $value, $content); + } + + return $content; + } + + public function getPlaceholderList() + { + return $this->placeholders ?? []; + } +} \ No newline at end of file diff --git a/app/Models/DocumentType.php b/app/Models/DocumentType.php new file mode 100644 index 000000000..c18ab8c05 --- /dev/null +++ b/app/Models/DocumentType.php @@ -0,0 +1,27 @@ + 'boolean', + ]; + + public function company() + { + return $this->belongsTo(User::class, 'company_id'); + } +} \ No newline at end of file diff --git a/app/Models/EmailTemplate.php b/app/Models/EmailTemplate.php new file mode 100644 index 000000000..b50233dc5 --- /dev/null +++ b/app/Models/EmailTemplate.php @@ -0,0 +1,25 @@ +hasMany(EmailTemplateLang::class, 'parent_id'); + } + + public function userEmailTemplates(): HasMany + { + return $this->hasMany(UserEmailTemplate::class, 'template_id'); + } +} diff --git a/app/Models/EmailTemplateLang.php b/app/Models/EmailTemplateLang.php new file mode 100644 index 000000000..b1e94bc5a --- /dev/null +++ b/app/Models/EmailTemplateLang.php @@ -0,0 +1,21 @@ +belongsTo(EmailTemplate::class, 'parent_id'); + } +} diff --git a/app/Models/Employee.php b/app/Models/Employee.php new file mode 100644 index 000000000..7f407c41e --- /dev/null +++ b/app/Models/Employee.php @@ -0,0 +1,121 @@ +belongsTo(Branch::class); + } + + /** + * Get the department that the employee belongs to. + */ + public function department() + { + return $this->belongsTo(Department::class); + } + + /** + * Get the designation that the employee has. + */ + public function designation() + { + return $this->belongsTo(Designation::class); + } + + /** + * Get the shift that the employee belongs to. + */ + public function shift() + { + return $this->belongsTo(Shift::class); + } + + /** + * Get the attendance policy that the employee has. + */ + public function attendancePolicy() + { + return $this->belongsTo(AttendancePolicy::class); + } + + /** + * Get the user associated with the employee. + */ + public function user() + { + return $this->belongsTo(User::class, 'user_id'); + } + + /** + * Get the user who created the employee. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the employee's documents. + */ + public function documents() + { + return $this->hasMany(EmployeeDocument::class,'employee_id','user_id'); + } + + /** + * Generate unique employee ID + */ + public static function generateEmployeeId() + { + $lastEmployee = self::orderBy('id', 'desc')->first(); + $nextId = $lastEmployee ? $lastEmployee->id + 1 : 1; + + return 'EMP' . str_pad($nextId, 6, '0', STR_PAD_LEFT); + } +} \ No newline at end of file diff --git a/app/Models/EmployeeAssessmentResult.php b/app/Models/EmployeeAssessmentResult.php new file mode 100644 index 000000000..6bdb549ff --- /dev/null +++ b/app/Models/EmployeeAssessmentResult.php @@ -0,0 +1,67 @@ + 'decimal:2', + 'is_passed' => 'boolean', + 'assessment_date' => 'date', + ]; + + /** + * Get the employee training for this assessment result. + */ + public function employeeTraining() + { + return $this->belongsTo(EmployeeTraining::class); + } + + /** + * Get the training assessment for this result. + */ + public function trainingAssessment() + { + return $this->belongsTo(TrainingAssessment::class); + } + + /** + * Get the user who assessed this result. + */ + public function assessor() + { + return $this->belongsTo(User::class, 'assessed_by'); + } + + /** + * Scope a query to only include passed results. + */ + public function scopePassed($query) + { + return $query->where('is_passed', true); + } + + /** + * Scope a query to only include failed results. + */ + public function scopeFailed($query) + { + return $query->where('is_passed', false); + } +} \ No newline at end of file diff --git a/app/Models/EmployeeContract.php b/app/Models/EmployeeContract.php new file mode 100644 index 000000000..6ca7efcc8 --- /dev/null +++ b/app/Models/EmployeeContract.php @@ -0,0 +1,82 @@ + 'date', + 'end_date' => 'date', + 'approved_at' => 'datetime', + 'benefits' => 'array', + 'basic_salary' => 'decimal:2', + ]; + + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + public function contractType() + { + return $this->belongsTo(ContractType::class); + } + + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function amendments() + { + return $this->hasMany(ContractAmendment::class); + } + + public function renewals() + { + return $this->hasMany(ContractRenewal::class); + } + + public function getIsExpiringAttribute() + { + if (!$this->end_date || $this->status !== 'Active') return false; + return $this->end_date <= Carbon::today()->addDays(30); + } + + public function getDaysUntilExpiryAttribute() + { + if (!$this->end_date || $this->status !== 'Active') return null; + return Carbon::today()->diffInDays($this->end_date, false); + } + + public function getTotalCompensationAttribute() + { + return $this->basic_salary; + } +} \ No newline at end of file diff --git a/app/Models/EmployeeDocument.php b/app/Models/EmployeeDocument.php new file mode 100644 index 000000000..2919f7c1d --- /dev/null +++ b/app/Models/EmployeeDocument.php @@ -0,0 +1,45 @@ +belongsTo(User::class, 'employee_id'); + } + + /** + * Get the document type of this document. + */ + public function documentType() + { + return $this->belongsTo(DocumentType::class); + } + + /** + * Get the user who created the document. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/EmployeeGoal.php b/app/Models/EmployeeGoal.php new file mode 100644 index 000000000..91793720f --- /dev/null +++ b/app/Models/EmployeeGoal.php @@ -0,0 +1,54 @@ + 'date:Y-m-d', + 'end_date' => 'date:Y-m-d', + 'progress' => 'integer', + ]; + + /** + * Get the company that owns this goal. + */ + public function company() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the employee that this goal belongs to. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the goal type that this goal belongs to. + */ + public function goalType() + { + return $this->belongsTo(GoalType::class, 'goal_type_id'); + } +} \ No newline at end of file diff --git a/app/Models/EmployeeReview.php b/app/Models/EmployeeReview.php new file mode 100644 index 000000000..a9ce864d1 --- /dev/null +++ b/app/Models/EmployeeReview.php @@ -0,0 +1,78 @@ + 'date', + 'completion_date' => 'date', + 'overall_rating' => 'float', + ]; + + /** + * Get the company that owns this review. + */ + public function company() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the employee being reviewed. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the employee conducting the review. + */ + public function reviewer() + { + return $this->belongsTo(User::class, 'reviewer_id'); + } + + /** + * Get the review cycle for this review. + */ + public function reviewCycle() + { + return $this->belongsTo(ReviewCycle::class, 'review_cycle_id'); + } + + /** + * Get the template used for this review. + */ + public function template() + { + return $this->belongsTo(ReviewTemplate::class, 'template_id'); + } + + /** + * Get the ratings for this review. + */ + public function ratings() + { + return $this->hasMany(EmployeeReviewRating::class); + } +} \ No newline at end of file diff --git a/app/Models/EmployeeReviewRating.php b/app/Models/EmployeeReviewRating.php new file mode 100644 index 000000000..bdbb3265e --- /dev/null +++ b/app/Models/EmployeeReviewRating.php @@ -0,0 +1,38 @@ + 'float', + ]; + + /** + * Get the review that owns this rating. + */ + public function review() + { + return $this->belongsTo(EmployeeReview::class, 'employee_review_id'); + } + + /** + * Get the performance indicator for this rating. + */ + public function indicator() + { + return $this->belongsTo(PerformanceIndicator::class, 'performance_indicator_id'); + } +} \ No newline at end of file diff --git a/app/Models/EmployeeSalary.php b/app/Models/EmployeeSalary.php new file mode 100644 index 000000000..0a8f06427 --- /dev/null +++ b/app/Models/EmployeeSalary.php @@ -0,0 +1,114 @@ + 'decimal:2', + 'components' => 'array', + 'is_active' => 'boolean', + ]; + + /** + * Get the employee. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id','id'); + } + + + + /** + * Get the user who created the salary. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get active salary for employee. + */ + public static function getActiveSalary($employeeId) + { + return static::where('employee_id', $employeeId) + ->where('is_active', true) + ->first(); + } + + /** + * Get basic salary for employee. + */ + public static function getBasicSalary($employeeId) + { + $salary = static::getActiveSalary($employeeId); + return $salary ? $salary->basic_salary : 0; + } + + /** + * Calculate salary components. + * Accepts a User $employee to resolve base_salary from employees.base_salary dynamically. + * Falls back to $this->basic_salary if $employee is not provided. + */ + public function calculateAllComponents($employee = null) + { + // Resolve basic salary: prefer employees.base_salary, fallback to this record's basic_salary + $basicSalary = $employee?->employee?->base_salary + ? (float) $employee->employee->base_salary + : (float) $this->basic_salary; + + // If no valid base salary configured, skip this employee + if (! $basicSalary || $basicSalary <= 0) { + return null; + } + + $selectedComponentIds = $this->components ?? []; + $components = SalaryComponent::whereIn('id', $selectedComponentIds) + ->where('status', 'active') + ->whereIn('created_by', getCompanyAndUsersId()) + ->get(); + + $earnings = ['Basic Salary' => $basicSalary]; + $deductions = []; + $totalEarnings = $basicSalary; + $totalDeductions = 0; + + foreach ($components as $component) { + $amount = $component->calculateAmount($basicSalary); + if ($component->type === 'earning') { + $earnings[$component->name] = $amount; + $totalEarnings += $amount; + } else { + $deductions[$component->name] = $amount; + $totalDeductions += $amount; + } + } + + return [ + 'basic_salary' => $basicSalary, + 'earnings' => $earnings, + 'deductions' => $deductions, + 'total_earnings' => $totalEarnings, + 'total_deductions'=> $totalDeductions, + 'gross_salary' => $totalEarnings, + 'net_salary' => $totalEarnings - $totalDeductions, + ]; + } +} diff --git a/app/Models/EmployeeTraining.php b/app/Models/EmployeeTraining.php new file mode 100644 index 000000000..f4c03f241 --- /dev/null +++ b/app/Models/EmployeeTraining.php @@ -0,0 +1,105 @@ + 'date', + 'completion_date' => 'date', + 'score' => 'decimal:2', + 'is_passed' => 'boolean', + ]; + + /** + * Get the employee for this training assignment. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the training program for this assignment. + */ + public function trainingProgram() + { + return $this->belongsTo(TrainingProgram::class); + } + + /** + * Get the user who assigned this training. + */ + public function assigner() + { + return $this->belongsTo(User::class, 'assigned_by'); + } + + /** + * Get the assessment results for this employee training. + */ + public function assessmentResults() + { + return $this->hasMany(EmployeeAssessmentResult::class); + } + + /** + * Scope a query to only include assigned trainings. + */ + public function scopeAssigned($query) + { + return $query->where('status', 'assigned'); + } + + /** + * Scope a query to only include in-progress trainings. + */ + public function scopeInProgress($query) + { + return $query->where('status', 'in_progress'); + } + + /** + * Scope a query to only include completed trainings. + */ + public function scopeCompleted($query) + { + return $query->where('status', 'completed'); + } + + /** + * Scope a query to only include failed trainings. + */ + public function scopeFailed($query) + { + return $query->where('status', 'failed'); + } + + /** + * Scope a query to only include passed trainings. + */ + public function scopePassed($query) + { + return $query->where('is_passed', true); + } +} \ No newline at end of file diff --git a/app/Models/EmployeeTransfer.php b/app/Models/EmployeeTransfer.php new file mode 100644 index 000000000..d44853562 --- /dev/null +++ b/app/Models/EmployeeTransfer.php @@ -0,0 +1,108 @@ + 'date', + 'effective_date' => 'date', + 'approved_at' => 'datetime', + ]; + + /** + * Get the employee being transferred. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the branch the employee is being transferred from. + */ + public function fromBranch() + { + return $this->belongsTo(Branch::class, 'from_branch_id'); + } + + /** + * Get the branch the employee is being transferred to. + */ + public function toBranch() + { + return $this->belongsTo(Branch::class, 'to_branch_id'); + } + + /** + * Get the department the employee is being transferred from. + */ + public function fromDepartment() + { + return $this->belongsTo(Department::class, 'from_department_id'); + } + + /** + * Get the department the employee is being transferred to. + */ + public function toDepartment() + { + return $this->belongsTo(Department::class, 'to_department_id'); + } + + /** + * Get the designation the employee is being transferred from. + */ + public function fromDesignation() + { + return $this->belongsTo(Designation::class, 'from_designation_id'); + } + + /** + * Get the designation the employee is being transferred to. + */ + public function toDesignation() + { + return $this->belongsTo(Designation::class, 'to_designation_id'); + } + + /** + * Get the user who approved this transfer. + */ + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Get the user who created this transfer. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/ExperienceCertificateTemplate.php b/app/Models/ExperienceCertificateTemplate.php new file mode 100644 index 000000000..8ed4cb6db --- /dev/null +++ b/app/Models/ExperienceCertificateTemplate.php @@ -0,0 +1,74 @@ +belongsTo(User::class, 'created_by'); + } + + public static function getTemplate($language, $createdBy = null) + { + return self::where('language', $language) + ->where('created_by', $createdBy) + ->first(); + } + + public static function createTemplatesForCompany($companyId) + { + $templates = [ + 'ar' => '

شهادة خبرة

التاريخ: {date}

إلى من يهمه الأمر،

نشهد بأن {employee_name} قد عمل لدى {company_name} بمنصب {designation} من تاريخ {joining_date} إلى {leaving_date}.

خلال فترة عمله، أظهر الموظف المذكور أداءً ممتازاً ومهارات مهنية عالية. لقد كان موظفاً مخلصاً ومسؤولاً.

نتمنى له التوفيق في مساعيه المستقبلية.

مع خالص التقدير،
قسم الموارد البشرية
{company_name}

', + 'da' => '

Erfaringsbevis

Dato: {date}

Til hvem det måtte vedkomme,

Dette er for at bekræfte, at {employee_name} var ansat hos {company_name} som {designation} fra {joining_date} til {leaving_date}.

I løbet af ansættelsesperioden viste den nævnte medarbejder fremragende præstation og høje professionelle færdigheder. Han/hun var en dedikeret og ansvarlig medarbejder.

Vi ønsker ham/hende held og lykke med fremtidige bestræbelser.

Med venlig hilsen,
HR-afdelingen
{company_name}

', + 'de' => '

Arbeitsbescheinigung

Datum: {date}

An wen es betrifft,

Hiermit wird bestätigt, dass {employee_name} bei {company_name} als {designation} vom {joining_date} bis {leaving_date} beschäftigt war.

Während der Beschäftigungszeit zeigte der genannte Mitarbeiter hervorragende Leistungen und hohe berufliche Fähigkeiten. Er/Sie war ein engagierter und verantwortungsvoller Mitarbeiter.

Wir wünschen ihm/ihr alles Gute für zukünftige Unternehmungen.

Mit freundlichen Grüßen,
Personalabteilung
{company_name}

', + 'en' => '

Experience Certificate

Date: {date}

To Whom It May Concern,

This is to certify that {employee_name} was employed with {company_name} as {designation} from {joining_date} to {leaving_date}.

During the period of employment, the above-mentioned employee demonstrated excellent performance and high professional skills. He/She was a dedicated and responsible employee.

We wish him/her all the best for future endeavors.

Sincerely,
HR Department
{company_name}

', + 'es' => '

Certificado de Experiencia

Fecha: {date}

A quien corresponda,

Por la presente certificamos que {employee_name} estuvo empleado en {company_name} como {designation} desde {joining_date} hasta {leaving_date}.

Durante el período de empleo, el empleado mencionado demostró un excelente desempeño y altas habilidades profesionales. Fue un empleado dedicado y responsable.

Le deseamos todo lo mejor para sus futuros proyectos.

Atentamente,
Departamento de RRHH
{company_name}

', + 'fr' => '

Certificat d\'Expérience

Date: {date}

À qui de droit,

Ceci certifie que {employee_name} était employé chez {company_name} en tant que {designation} du {joining_date} au {leaving_date}.

Pendant la période d\'emploi, l\'employé susmentionné a démontré d\'excellentes performances et de hautes compétences professionnelles. Il/Elle était un employé dévoué et responsable.

Nous lui souhaitons tout le meilleur pour ses projets futurs.

Cordialement,
Département RH
{company_name}

', + 'he' => '

תעודת ניסיון

תאריך: {date}

למי שזה נוגע,

זאת להעיד כי {employee_name} הועסק ב-{company_name} בתפקיד {designation} מ-{joining_date} עד {leaving_date}.

במהלך תקופת העבודה, העובד הנ"ל הפגין ביצועים מעולים וכישורים מקצועיים גבוהים. הוא/היא היה/הייתה עובד/ת מסור/ה ואחראי/ת.

אנו מאחלים לו/לה הצלחה בכל המאמצים העתידיים.

בכבוד רב,
מחלקת משאבי אנוש
{company_name}

', + 'it' => '

Certificato di Esperienza

Data: {date}

A chi di competenza,

Si certifica che {employee_name} è stato impiegato presso {company_name} come {designation} dal {joining_date} al {leaving_date}.

Durante il periodo di impiego, il suddetto dipendente ha dimostrato prestazioni eccellenti e alte competenze professionali. È stato un dipendente dedicato e responsabile.

Gli auguriamo tutto il meglio per i futuri progetti.

Cordiali saluti,
Dipartimento HR
{company_name}

', + 'ja' => '

経験証明書

日付: {date}

関係者各位

{employee_name}が{joining_date}から{leaving_date}まで{company_name}で{designation}として雇用されていたことを証明いたします。

雇用期間中、上記従業員は優秀な成績と高い専門技能を示しました。献身的で責任感のある従業員でした。

今後の活動における成功をお祈りいたします。

敬具
人事部
{company_name}

', + 'nl' => '

Ervaring Certificaat

Datum: {date}

Aan wie het betreft,

Hierbij wordt bevestigd dat {employee_name} werkzaam was bij {company_name} als {designation} van {joining_date} tot {leaving_date}.

Tijdens de dienstperiode toonde bovengenoemde werknemer uitstekende prestaties en hoge professionele vaardigheden. Hij/Zij was een toegewijde en verantwoordelijke werknemer.

Wij wensen hem/haar het beste toe voor toekomstige ondernemingen.

Met vriendelijke groet,
HR Afdeling
{company_name}

', + 'pl' => '

Świadectwo Doświadczenia

Data: {date}

Do kogo to dotyczy,

Niniejszym poświadczamy, że {employee_name} był zatrudniony w {company_name} na stanowisku {designation} od {joining_date} do {leaving_date}.

W okresie zatrudnienia wyżej wymieniony pracownik wykazał się doskonałymi wynikami i wysokimi umiejętnościami zawodowymi. Był oddanym i odpowiedzialnym pracownikiem.

Życzymy mu/jej powodzenia w przyszłych przedsięwzięciach.

Z poważaniem,
Dział HR
{company_name}

', + 'pt' => '

Certificado de Experiência

Data: {date}

A quem possa interessar,

Certificamos que {employee_name} esteve empregado na {company_name} como {designation} de {joining_date} a {leaving_date}.

Durante o período de emprego, o funcionário mencionado demonstrou excelente desempenho e altas habilidades profissionais. Foi um funcionário dedicado e responsável.

Desejamos-lhe tudo de bom para empreendimentos futuros.

Atenciosamente,
Departamento de RH
{company_name}

', + 'pt-BR' => '

Certificado de Experiência

Data: {date}

A quem possa interessar,

Certificamos que {employee_name} esteve empregado na {company_name} como {designation} de {joining_date} a {leaving_date}.

Durante o período de emprego, o funcionário mencionado demonstrou excelente desempenho e altas habilidades profissionais. Foi um funcionário dedicado e responsável.

Desejamos-lhe tudo de bom para empreendimentos futuros.

Atenciosamente,
Departamento de RH
{company_name}

', + 'ru' => '

Справка о трудовом стаже

Дата: {date}

Кого это касается,

Настоящим подтверждаем, что {employee_name} работал в {company_name} в должности {designation} с {joining_date} по {leaving_date}.

В период трудоустройства вышеупомянутый сотрудник продемонстрировал отличные результаты и высокие профессиональные навыки. Он/Она был/была преданным и ответственным сотрудником.

Желаем ему/ей всего наилучшего в будущих начинаниях.

С уважением,
Отдел кадров
{company_name}

', + 'tr' => '

Deneyim Belgesi

Tarih: {date}

İlgili Makama,

{employee_name} adlı kişinin {joining_date} tarihinden {leaving_date} tarihine kadar {company_name} şirketinde {designation} pozisyonunda çalıştığını onaylarız.

İstihdam süresi boyunca yukarıda belirtilen çalışan mükemmel performans ve yüksek mesleki beceriler sergilemiştir. Kendisi özverili ve sorumlu bir çalışandı.

Gelecekteki çalışmalarında kendisine başarılar dileriz.

Saygılarımızla,
İnsan Kaynakları Departmanı
{company_name}

', + 'zh' => '

工作经验证明

日期:{date}

致相关人员:

兹证明{employee_name}于{joining_date}至{leaving_date}期间在{company_name}担任{designation}职位。

在任职期间,上述员工表现出色,具备高水平的专业技能。他/她是一位敬业负责的员工。

祝愿他/她在未来的工作中一切顺利。

此致
人力资源部
{company_name}

' + ]; + + $variables = json_encode(['date', 'company_name', 'employee_name', 'designation', 'joining_date', 'leaving_date']); + + try { + foreach ($templates as $language => $content) { + self::updateOrCreate( + [ + 'language' => $language, + 'created_by' => $companyId + ], + [ + 'content' => $content, + 'variables' => $variables + ] + ); + } + return true; + } catch (\Exception $e) { + Log::error('Failed to create Experience Certificate templates for company ID: ' . $companyId . '. Error: ' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/app/Models/GoalType.php b/app/Models/GoalType.php new file mode 100644 index 000000000..9c80ae273 --- /dev/null +++ b/app/Models/GoalType.php @@ -0,0 +1,34 @@ +belongsTo(User::class, 'created_by'); + } + + /** + * Get the employee goals for this goal type. + */ + public function goals() + { + return $this->hasMany(EmployeeGoal::class, 'goal_type_id'); + } +} \ No newline at end of file diff --git a/app/Models/Holiday.php b/app/Models/Holiday.php new file mode 100644 index 000000000..7a3b42127 --- /dev/null +++ b/app/Models/Holiday.php @@ -0,0 +1,67 @@ + 'date', + 'end_date' => 'date', + 'is_recurring' => 'boolean', + 'is_paid' => 'boolean', + 'is_half_day' => 'boolean', + ]; + + /** + * Get the user who created this holiday. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the branches associated with this holiday. + */ + public function branches() + { + return $this->belongsToMany(Branch::class, 'holiday_branch'); + } + + /** + * Check if the holiday is a multi-day holiday. + */ + public function isMultiDay() + { + return $this->end_date && $this->end_date->ne($this->start_date); + } + + /** + * Get the duration of the holiday in days. + */ + public function getDurationInDays() + { + if (!$this->end_date || $this->end_date->eq($this->start_date)) { + return 1; + } + + return $this->start_date->diffInDays($this->end_date) + 1; + } +} \ No newline at end of file diff --git a/app/Models/HrDocument.php b/app/Models/HrDocument.php new file mode 100644 index 000000000..e06fbe6ee --- /dev/null +++ b/app/Models/HrDocument.php @@ -0,0 +1,88 @@ + 'date', + 'expiry_date' => 'date', + 'approved_at' => 'datetime', + 'requires_acknowledgment' => 'boolean', + ]; + + public function category() + { + return $this->belongsTo(DocumentCategory::class); + } + + public function uploader() + { + return $this->belongsTo(User::class, 'uploaded_by'); + } + + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function accessControls() + { + return $this->hasMany(DocumentAccessControl::class, 'document_id'); + } + + public function acknowledgments() + { + return $this->hasMany(DocumentAcknowledgment::class, 'document_id'); + } + + public function getIsExpiredAttribute() + { + return $this->expiry_date && $this->expiry_date < Carbon::today(); + } + + public function getFileSizeFormattedAttribute() + { + $bytes = $this->file_size; + if ($bytes >= 1073741824) { + return number_format($bytes / 1073741824, 2) . ' GB'; + } elseif ($bytes >= 1048576) { + return number_format($bytes / 1048576, 2) . ' MB'; + } elseif ($bytes >= 1024) { + return number_format($bytes / 1024, 2) . ' KB'; + } else { + return $bytes . ' bytes'; + } + } +} \ No newline at end of file diff --git a/app/Models/Interview.php b/app/Models/Interview.php new file mode 100644 index 000000000..cdd164ba6 --- /dev/null +++ b/app/Models/Interview.php @@ -0,0 +1,63 @@ + 'date', + 'interviewers' => 'array', + 'feedback_submitted' => 'boolean', + ]; + + public function candidate() + { + return $this->belongsTo(Candidate::class); + } + + public function job() + { + return $this->belongsTo(JobPosting::class); + } + + public function round() + { + return $this->belongsTo(InterviewRound::class); + } + + public function interviewType() + { + return $this->belongsTo(InterviewType::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function feedback() + { + return $this->hasMany(InterviewFeedback::class); + } +} \ No newline at end of file diff --git a/app/Models/InterviewFeedback.php b/app/Models/InterviewFeedback.php new file mode 100644 index 000000000..f55b83ba1 --- /dev/null +++ b/app/Models/InterviewFeedback.php @@ -0,0 +1,58 @@ +belongsTo(Interview::class); + } + + public function getInterviewerNamesAttribute() + { + if (!$this->interviewer_id) { + return ''; + } + + $interviewerIds = explode(',', $this->interviewer_id); + $interviewers = \App\Models\User::whereIn('id', $interviewerIds) + ->pluck('name') + ->toArray(); + + return implode(', ', $interviewers); + } + + public function getInterviewerIdsArrayAttribute() + { + if (!$this->interviewer_id) { + return []; + } + + return explode(',', $this->interviewer_id); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/InterviewRound.php b/app/Models/InterviewRound.php new file mode 100644 index 000000000..2974eba80 --- /dev/null +++ b/app/Models/InterviewRound.php @@ -0,0 +1,35 @@ +belongsTo(JobPosting::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function interviews() + { + return $this->hasMany(Interview::class, 'round_id'); + } +} \ No newline at end of file diff --git a/app/Models/InterviewType.php b/app/Models/InterviewType.php new file mode 100644 index 000000000..439253d16 --- /dev/null +++ b/app/Models/InterviewType.php @@ -0,0 +1,28 @@ +belongsTo(User::class, 'created_by'); + } + + public function interviews() + { + return $this->hasMany(Interview::class); + } +} \ No newline at end of file diff --git a/app/Models/IpRestriction.php b/app/Models/IpRestriction.php new file mode 100644 index 000000000..f360bcecd --- /dev/null +++ b/app/Models/IpRestriction.php @@ -0,0 +1,22 @@ +belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/JobCategory.php b/app/Models/JobCategory.php new file mode 100644 index 000000000..8bb3bf7dc --- /dev/null +++ b/app/Models/JobCategory.php @@ -0,0 +1,34 @@ +belongsTo(User::class, 'created_by'); + } + + /** + * Get the job requisitions for this category. + */ + public function jobRequisitions() + { + return $this->hasMany(JobRequisition::class); + } +} \ No newline at end of file diff --git a/app/Models/JobLocation.php b/app/Models/JobLocation.php new file mode 100644 index 000000000..a697b654c --- /dev/null +++ b/app/Models/JobLocation.php @@ -0,0 +1,37 @@ + 'boolean', + ]; + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function jobPostings() + { + return $this->hasMany(JobPosting::class, 'location_id'); + } +} \ No newline at end of file diff --git a/app/Models/JobPosting.php b/app/Models/JobPosting.php new file mode 100644 index 000000000..1b8b23dfc --- /dev/null +++ b/app/Models/JobPosting.php @@ -0,0 +1,96 @@ + 'date', + 'start_date' => 'date', + 'publish_date' => 'datetime', + 'is_published' => 'boolean', + 'is_featured' => 'boolean', + 'min_salary' => 'decimal:2', + 'max_salary' => 'decimal:2', + 'skills' => 'array', + 'custom_question' => 'array', + 'applicant' => 'array', + 'visibility' => 'array', + ]; + + public function requisition() + { + return $this->belongsTo(JobRequisition::class); + } + + public function jobType() + { + return $this->belongsTo(JobType::class); + } + + public function location() + { + return $this->belongsTo(JobLocation::class); + } + + public function department() + { + return $this->belongsTo(Department::class); + } + + public function branch() + { + return $this->belongsTo(Branch::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function candidates() + { + return $this->hasMany(Candidate::class, 'job_id'); + } + + public static function generateJobCode($jobPostingId = null) + { + $nextId = $jobPostingId ?: (self::max('id') + 1); + return 'JOB-' . creatorId() . '-' . str_pad($nextId, 5, '0', STR_PAD_LEFT); + } +} diff --git a/app/Models/JobRequisition.php b/app/Models/JobRequisition.php new file mode 100644 index 000000000..e4b9545f9 --- /dev/null +++ b/app/Models/JobRequisition.php @@ -0,0 +1,62 @@ + 'datetime', + 'budget_min' => 'decimal:2', + 'budget_max' => 'decimal:2', + ]; + + public function jobCategory() + { + return $this->belongsTo(JobCategory::class); + } + + public function department() + { + return $this->belongsTo(Department::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function jobPostings() + { + return $this->hasMany(JobPosting::class, 'requisition_id'); + } +} \ No newline at end of file diff --git a/app/Models/JobType.php b/app/Models/JobType.php new file mode 100644 index 000000000..2118f7d2e --- /dev/null +++ b/app/Models/JobType.php @@ -0,0 +1,28 @@ +belongsTo(User::class, 'created_by'); + } + + public function jobPostings() + { + return $this->hasMany(JobPosting::class); + } +} \ No newline at end of file diff --git a/app/Models/JoiningLetterTemplate.php b/app/Models/JoiningLetterTemplate.php new file mode 100644 index 000000000..b286d9bd4 --- /dev/null +++ b/app/Models/JoiningLetterTemplate.php @@ -0,0 +1,74 @@ +belongsTo(User::class, 'created_by'); + } + + public static function getTemplate($language, $createdBy = null) + { + return self::where('language', $language) + ->where('created_by', $createdBy) + ->first(); + } + + public static function createTemplatesForCompany($companyId) + { + $templates = [ + 'ar' => '

خطاب الانضمام

التاريخ: {date}

عزيزي/عزيزتي {employee_name}،

يسعدنا أن نرحب بك في {company_name} بصفتك {designation}.

تاريخ بدء العمل: {joining_date}
الراتب: {salary}
القسم: {department}

نتطلع إلى مساهمتك القيمة في نجاح شركتنا.

مع أطيب التحيات،
قسم الموارد البشرية
{company_name}

', + 'da' => '

Tiltrædelsesbreve

Dato: {date}

Kære {employee_name},

Vi er glade for at byde dig velkommen til {company_name} som {designation}.

Startdato: {joining_date}
Løn: {salary}
Afdeling: {department}

Vi ser frem til dit værdifulde bidrag til vores virksomheds succes.

Med venlig hilsen,
HR-afdelingen
{company_name}

', + 'de' => '

Beitrittsschreiben

Datum: {date}

Liebe/r {employee_name},

Wir freuen uns, Sie bei {company_name} als {designation} willkommen zu heißen.

Startdatum: {joining_date}
Gehalt: {salary}
Abteilung: {department}

Wir freuen uns auf Ihren wertvollen Beitrag zum Erfolg unseres Unternehmens.

Mit freundlichen Grüßen,
Personalabteilung
{company_name}

', + 'en' => '

Joining Letter

Date: {date}

Dear {employee_name},

We are pleased to welcome you to {company_name} as {designation}.

Joining Date: {joining_date}
Salary: {salary}
Department: {department}

We look forward to your valuable contribution to our company\'s success.

Best regards,
HR Department
{company_name}

', + 'es' => '

Carta de Incorporación

Fecha: {date}

Estimado/a {employee_name},

Nos complace darle la bienvenida a {company_name} como {designation}.

Fecha de Incorporación: {joining_date}
Salario: {salary}
Departamento: {department}

Esperamos con interés su valiosa contribución al éxito de nuestra empresa.

Saludos cordiales,
Departamento de RRHH
{company_name}

', + 'fr' => '

Lettre d\'Adhésion

Date: {date}

Cher/Chère {employee_name},

Nous sommes heureux de vous accueillir chez {company_name} en tant que {designation}.

Date d\'Entrée: {joining_date}
Salaire: {salary}
Département: {department}

Nous attendons avec impatience votre précieuse contribution au succès de notre entreprise.

Cordialement,
Département RH
{company_name}

', + 'he' => '

מכתב הצטרפות

תאריך: {date}

{employee_name} יקר/ה,

אנו שמחים לקבל אותך ל-{company_name} בתפקיד {designation}.

תאריך התחלה: {joining_date}
משכורת: {salary}
מחלקה: {department}

אנו מצפים לתרומתך החשובה להצלחת החברה שלנו.

בברכה,
מחלקת משאבי אנוש
{company_name}

', + 'it' => '

Lettera di Adesione

Data: {date}

Caro/a {employee_name},

Siamo lieti di darti il benvenuto in {company_name} come {designation}.

Data di Inizio: {joining_date}
Stipendio: {salary}
Dipartimento: {department}

Non vediamo l\'ora del tuo prezioso contributo al successo della nostra azienda.

Cordiali saluti,
Dipartimento HR
{company_name}

', + 'ja' => '

入社通知書

日付: {date}

{employee_name}

{company_name}に{designation}としてご入社いただき、心より歓迎いたします。

入社日: {joining_date}
給与: {salary}
部署: {department}

弊社の成功への貴重な貢献を楽しみにしております。

敬具
人事部
{company_name}

', + 'nl' => '

Toetredingsbrief

Datum: {date}

Beste {employee_name},

We zijn verheugd u te verwelkomen bij {company_name} als {designation}.

Startdatum: {joining_date}
Salaris: {salary}
Afdeling: {department}

We kijken uit naar uw waardevolle bijdrage aan het succes van ons bedrijf.

Met vriendelijke groet,
HR Afdeling
{company_name}

', + 'pl' => '

List Dołączenia

Data: {date}

Drogi/a {employee_name},

Mamy przyjemność powitać Cię w {company_name} na stanowisku {designation}.

Data Rozpoczęcia: {joining_date}
Wynagrodzenie: {salary}
Dział: {department}

Czekamy na Twój cenny wkład w sukces naszej firmy.

Z poważaniem,
Dział HR
{company_name}

', + 'pt' => '

Carta de Adesão

Data: {date}

Caro/a {employee_name},

Temos o prazer de dar-lhe as boas-vindas à {company_name} como {designation}.

Data de Início: {joining_date}
Salário: {salary}
Departamento: {department}

Esperamos ansiosamente sua valiosa contribuição para o sucesso da nossa empresa.

Atenciosamente,
Departamento de RH
{company_name}

', + 'pt-BR' => '

Carta de Adesão

Data: {date}

Caro/a {employee_name},

Temos o prazer de dar-lhe as boas-vindas à {company_name} como {designation}.

Data de Início: {joining_date}
Salário: {salary}
Departamento: {department}

Esperamos ansiosamente sua valiosa contribuição para o sucesso da nossa empresa.

Atenciosamente,
Departamento de RH
{company_name}

', + 'ru' => '

Письмо о Присоединении

Дата: {date}

Уважаемый/ая {employee_name},

Мы рады приветствовать вас в {company_name} на должности {designation}.

Дата Начала Работы: {joining_date}
Зарплата: {salary}
Отдел: {department}

Мы с нетерпением ждем вашего ценного вклада в успех нашей компании.

С уважением,
Отдел кадров
{company_name}

', + 'tr' => '

Katılım Mektubu

Tarih: {date}

Sayın {employee_name},

Sizi {company_name} şirketinde {designation} pozisyonunda karşılamaktan memnuniyet duyuyoruz.

İşe Başlama Tarihi: {joining_date}
Maaş: {salary}
Departman: {department}

Şirketimizin başarısına değerli katkınızı dört gözle bekliyoruz.

Saygılarımızla,
İnsan Kaynakları Departmanı
{company_name}

', + 'zh' => '

入职信

日期:{date}

亲爱的{employee_name}

我们很高兴欢迎您加入{company_name},担任{designation}职位。

入职日期:{joining_date}
薪资:{salary}
部门:{department}

我们期待您为公司成功做出宝贵贡献。

此致
人力资源部
{company_name}

' + ]; + + $variables = json_encode(['date', 'company_name', 'employee_name', 'designation', 'joining_date', 'salary', 'department']); + + try { + foreach ($templates as $language => $content) { + self::updateOrCreate( + [ + 'language' => $language, + 'created_by' => $companyId + ], + [ + 'content' => $content, + 'variables' => $variables + ] + ); + } + return true; + } catch (\Exception $e) { + Log::error('Failed to create Joining Letter templates for company ID: ' . $companyId . '. Error: ' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/app/Models/LandingPageCustomPage.php b/app/Models/LandingPageCustomPage.php new file mode 100644 index 000000000..0b8281a76 --- /dev/null +++ b/app/Models/LandingPageCustomPage.php @@ -0,0 +1,54 @@ + 'boolean', + 'sort_order' => 'integer' + ]; + + public function scopeOrdered($query) + { + return $query->orderBy('sort_order')->orderBy('created_at'); + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + protected static function boot() + { + parent::boot(); + + static::creating(function ($page) { + if (empty($page->slug)) { + $page->slug = Str::slug($page->title); + } + if (is_null($page->sort_order)) { + $page->sort_order = static::max('sort_order') + 1; + } + }); + + static::updating(function ($page) { + if ($page->isDirty('title') && empty($page->slug)) { + $page->slug = Str::slug($page->title); + } + }); + } +} \ No newline at end of file diff --git a/app/Models/LandingPageSetting.php b/app/Models/LandingPageSetting.php new file mode 100644 index 000000000..370e438c5 --- /dev/null +++ b/app/Models/LandingPageSetting.php @@ -0,0 +1,516 @@ + '', + 'contact_email' => '', + 'contact_phone' => '', + 'contact_address' => '' + ]; + + protected $casts = [ + 'config_sections' => 'array' + ]; + + public static function getSettings() + { + $settings = self::first(); + + if (!$settings) { + $defaultConfig = isSaas() ? self::getSaasConfig() : self::getNonSaasConfig(); + $companyName = isSaas() ? 'HRM SaaS' : 'HRM'; + + $settings = self::create([ + 'company_name' => $companyName, + 'contact_email' => 'support@hrm.com', + 'contact_phone' => '+1 (555) 123-4567', + 'contact_address' => 'San Francisco, CA', + 'config_sections' => $defaultConfig + ]); + } + return $settings; + } + + private static function getSaasConfig() + { + return [ + 'sections' => [ + [ + 'key' => 'header', + 'transparent' => false, + 'background_color' => '#ffffff', + 'text_color' => '#1f2937', + 'button_style' => 'gradient' + ], + [ + 'key' => 'hero', + 'title' => 'Simplify HR Management Effortlessly', + 'subtitle' => 'Manage employees, payroll, attendance, and more in one powerful platform.', + 'announcement_text' => '📢 New: Smart Leave & Attendance Tracking Launched!', + 'primary_button_text' => 'Start Free Trial', + 'secondary_button_text' => 'Login', + 'image' => '', + 'background_color' => '#f8fafc', + 'text_color' => '#1f2937', + 'layout' => 'image-right', + 'height' => 600, + 'stats' => [ + ['value' => '10K+', 'label' => 'Active Users'], + ['value' => '50+', 'label' => 'Countries'], + ['value' => '99%', 'label' => 'Satisfaction'] + ], + ], + [ + 'key' => 'features', + 'title' => 'Empowering Businesses with Smart HR Solutions', + 'description' => 'All-in-one platform to manage employees, payroll, attendance, and performance with ease.', + 'background_color' => '#ffffff', + 'layout' => 'grid', + 'columns' => 3, + 'image' => '', + 'show_icons' => true, + 'features_list' => [ + ['title' => 'Employee Management', 'description' => 'Centralized profiles with personal, job, and document details.', 'icon' => 'users'], + ['title' => 'Payroll Automation', 'description' => 'Generate accurate payslips with tax, allowances, and deductions.', 'icon' => 'dollar-sign'], + ['title' => 'Leave & Attendance', 'description' => 'Smart tracking of leaves, shifts, and attendance logs.', 'icon' => 'clock'], + ['title' => 'Recruitment & Onboarding', 'description' => 'Streamline hiring with applicant tracking and digital onboarding.', 'icon' => 'user-plus'], + ['title' => 'Performance Management', 'description' => 'Set goals, run evaluations, and track employee growth.', 'icon' => 'award'], + ['title' => 'Reports & Analytics', 'description' => 'Get actionable insights on workforce productivity and HR metrics.', 'icon' => 'bar-chart-2'], + ] + ], + [ + 'key' => 'screenshots', + 'title' => 'See HRM SaaS in Action', + 'subtitle' => 'Discover how our modern HRM SaaS platform helps you manage employees, payroll, attendance, and performance — all in one place.', + 'screenshots_list' => [ + ['src' => '/screenshots/saas/dashboard.png', 'alt' => 'HRM Dashboard Overview', 'title' => 'Dashboard Overview', 'description' => 'Get a complete overview of employee data, payroll, and HR activities in one unified dashboard.'], + ['src' => '/screenshots/saas/employee-management.png', 'alt' => 'Employee Management Module', 'title' => 'Employee Management', 'description' => 'Centralized employee profiles with personal details, documents, and job history.'], + ['src' => '/screenshots/saas/payroll-payslip.png', 'alt' => 'Payroll Automation', 'title' => 'Payroll & Payslips', 'description' => 'Automated payroll processing with tax calculations, allowances, and downloadable payslips.'], + ['src' => '/screenshots/saas/leave.png', 'alt' => 'Leave Management', 'title' => 'Leave Management', 'description' => 'Easily apply, approve, and track employee leave requests with proper workflows and policies.'], + ['src' => '/screenshots/saas/attendance.png', 'alt' => 'Attendance Tracking', 'title' => 'Attendance Tracking', 'description' => 'Monitor employee check-ins, check-outs, and shifts with automated attendance logs.'], + ['src' => '/screenshots/saas/recruitment.png', 'alt' => 'Recruitment & Onboarding', 'title' => 'Recruitment & Onboarding', 'description' => 'Streamline hiring with applicant tracking and digital onboarding.'], + ] + ], + [ + 'key' => 'why_choose_us', + 'title' => 'Why Choose HRM SaaS?', + 'subtitle' => 'Smart, simple, and powerful HR solutions for every business.', + 'reasons' => [ + ['title' => 'All-in-One HR Solution', 'description' => 'Manage employees, payroll, attendance, recruitment, and performance from a single platform.', 'icon' => 'layers'], + ['title' => 'Time-Saving Automation', 'description' => 'Automate repetitive HR tasks to focus on strategic decision-making.', 'icon' => 'clock'], + ['title' => 'Data-Driven Insights', 'description' => 'Make informed decisions with advanced analytics and reports.', 'icon' => 'bar-chart'], + ['title' => 'Secure & Reliable', 'description' => 'Keep sensitive HR data safe with enterprise-grade security.', 'icon' => 'shield'] + ], + 'stats' => [ + ['value' => '500+', 'label' => 'Companies Using HRM', 'color' => 'blue'], + ['value' => '20K+', 'label' => 'Employees Managed', 'color' => 'green'], + ['value' => '98%', 'label' => 'Customer Satisfaction', 'color' => 'orange'] + ] + ], + [ + 'key' => 'about', + 'title' => 'About HRM SaaS', + 'description' => 'We are passionate about simplifying HR management for businesses of all sizes.', + 'story_title' => 'We are passionate about simplifying HR management for businesses of all sizes.', + 'story_content' => 'Founded by HR and tech enthusiasts, HRM SaaS was created to replace cumbersome spreadsheets and manual processes with a modern, all-in-one HR platform.', + 'image' => '', + 'background_color' => '#f9fafb', + 'layout' => 'image-right', + 'stats' => [ + ['value' => '3+ Years', 'label' => 'Experience', 'color' => 'blue'], + ['value' => '500+', 'label' => 'Companies Served', 'color' => 'green'], + ['value' => '20K+', 'label' => 'Employees Managed', 'color' => 'purple'] + ] + ], + [ + 'key' => 'team', + 'title' => 'Meet Our Team', + 'subtitle' => 'We\'re a dedicated team of HR and technology experts.', + 'cta_title' => 'Want to Join Our Team?', + 'cta_description' => 'We\'re always looking for talented individuals to shape the future of HR management.', + 'cta_button_text' => 'View Open Positions', + 'members' => [ + ['name' => 'John Doe', 'role' => 'CEO & Founder', 'bio' => 'Experienced HR tech entrepreneur passionate about building intuitive HR solutions.', 'image' => '', 'linkedin' => '#', 'email' => 'john@example.com'], + ['name' => 'Jane Smith', 'role' => 'CTO', 'bio' => 'Leads the tech team to create scalable and secure HR platforms.', 'image' => '', 'linkedin' => '#', 'email' => 'jane@example.com'], + ['name' => 'Michael Lee', 'role' => 'Head of Product', 'bio' => 'Designs user-centric features to simplify HR processes.', 'image' => '', 'linkedin' => '#', 'email' => 'michael@example.com'], + ['name' => 'Emily Davis', 'role' => 'HR Manager', 'bio' => 'Oversees employee engagement, recruitment, and HR operations.', 'image' => '', 'linkedin' => '#', 'email' => 'emily@example.com'] + ] + ], + [ + 'key' => 'testimonials', + 'title' => 'What Our Clients Say', + 'subtitle' => 'Hear from HR leaders who trust our platform.', + 'trust_title' => 'Trusted by HR Professionals Worldwide', + 'trust_stats' => [ + ['value' => '4.9/5', 'label' => 'Average Rating', 'color' => 'blue'], + ['value' => '500+', 'label' => 'Companies Served', 'color' => 'green'] + ], + 'testimonials' => [ + ['name' => 'Alice Johnson', 'role' => 'HR Manager', 'company' => 'GlobalTech Ltd.', 'content' => 'HRM has made managing employee records and attendance effortless. Our HR team saves hours every week!', 'rating' => 5], + ['name' => 'Robert Smith', 'role' => 'Operations Head', 'company' => 'Innovate Solutions', 'content' => 'The payroll automation is incredibly accurate and easy to use. No more manual calculations or errors!', 'rating' => 5], + ['name' => 'Maria Davis', 'role' => 'CEO', 'company' => 'BrightFuture Corp.', 'content' => 'From recruitment to performance management, HRM covers everything we need in one platform.', 'rating' => 5], + ['name' => 'David Lee', 'role' => 'Talent Acquisition Lead', 'company' => 'NextGen Enterprises', 'content' => 'Recruitment and onboarding have never been smoother. HRM platform is intuitive and efficient.', 'rating' => 5], + ['name' => 'Samantha Green', 'role' => 'Payroll Specialist', 'company' => 'BrightSolutions Inc.', 'content' => 'Payroll processing is now quick and error-free thanks to HRM. It has transformed our monthly workflow.', 'rating' => 5], + ['name' => 'Michael Brown', 'role' => 'HR Coordinator', 'company' => 'TechWave Ltd.', 'content' => 'The performance management module helps us track employee goals and progress effortlessly.', 'rating' => 5] + ] + ], + [ + 'key' => 'plans', + 'title' => 'Choose Your HRM SaaS Plan', + 'subtitle' => 'Start with our free plan and upgrade as your team grows.', + 'faq_text' => 'Have questions about our plans? Reach out to our sales team for guidance.' + ], + [ + 'key' => 'faq', + 'title' => 'Frequently Asked Questions', + 'subtitle' => 'Got questions? We\'ve got answers.', + 'cta_text' => 'Still have questions?', + 'button_text' => 'Contact Support', + 'faqs' => [ + ['question' => 'How does HRM SaaS work?', 'answer' => 'HRM SaaS is an all-in-one HR platform that helps you manage employees, payroll, attendance, recruitment, and performance efficiently.'], + ['question' => 'Can I automate payroll and leave tracking?', 'answer' => 'Yes! HRM SaaS allows you to automate payroll calculations, generate payslips, and track employee leaves and attendance seamlessly.'], + ['question' => 'Is my employee data secure?', 'answer' => 'Absolutely. HRM SaaS uses enterprise-grade security measures to keep all sensitive HR data safe and confidential.'], + ['question' => 'Can I manage recruitment and onboarding?', 'answer' => 'Yes, HRM SaaS provides applicant tracking, interview management, and digital onboarding tools to simplify hiring.'], + ['question' => 'Does HRM SaaS support performance evaluations?', 'answer' => 'Yes, you can set goals, track KPIs, and run performance reviews directly within the platform.'], + ['question' => 'Can HRM SaaS generate HR reports?', 'answer' => 'HRM offers advanced analytics and reporting features to give insights on attendance, payroll, and workforce performance.'], + ['question' => 'What plans are available and can I upgrade anytime?', 'answer' => 'We offer flexible plans for different team sizes. You can start with the free plan and upgrade as your organization grows.'] + ] + ], + [ + 'key' => 'newsletter', + 'title' => 'Stay Updated with HRM SaaS', + 'subtitle' => 'Get the latest updates, HR tips, and feature announcements.', + 'privacy_text' => 'No spam, unsubscribe at any time.', + 'benefits' => [ + ['icon' => '📧', 'title' => 'Weekly Updates', 'description' => 'Stay informed about the latest HRM SaaS features and improvements.'], + ['icon' => '💡', 'title' => 'HR Insights', 'description' => 'Get tips and best practices to optimize your HR operations.'], + ['icon' => '📊', 'title' => 'Reports & Trends', 'description' => 'Receive analytics insights and industry trends directly to your inbox.'] + ] + ], + [ + 'key' => 'contact', + 'title' => 'Get in Touch', + 'subtitle' => 'Have questions about HRM SaaS? We\'d love to hear from you.', + 'form_title' => 'Send us a Message', + 'info_title' => 'Contact Information', + 'info_description' => 'We\'re here to help and answer any questions you might have about managing your HR processes efficiently.', + 'layout' => 'split', + 'background_color' => '#f9fafb' + ], + [ + 'key' => 'footer', + 'description' => 'Simplifying HR management with an all-in-one modern platform.', + 'newsletter_title' => 'Stay Updated', + 'newsletter_subtitle' => 'Join our newsletter for HR tips and product updates', + 'links' => [ + 'product' => [ + ['name' => 'Features', 'href' => '#features'], + ['name' => 'Pricing', 'href' => '#pricing'] + ], + 'company' => [ + ['name' => 'About Us', 'href' => '#about'], + ['name' => 'Contact', 'href' => '#contact'] + ], + 'support' => [ + ['name' => 'Help Center', 'href' => '#help-center'], + ['name' => 'FAQ', 'href' => '#faq'], + ['name' => 'Refund Policy', 'href' => '#refund-policy'] + ], + 'legal' => [ + ['name' => 'Terms of Service', 'href' => '#terms'], + ['name' => 'Privacy Policy', 'href' => '#privacy'] + ] + ], + 'social_links' => [ + ['name' => 'Facebook', 'icon' => 'Facebook', 'href' => '#'], + ['name' => 'Twitter', 'icon' => 'Twitter', 'href' => '#'], + ['name' => 'LinkedIn', 'icon' => 'Linkedin', 'href' => '#'] + ], + 'section_titles' => [ + 'product' => 'Product', + 'company' => 'Company' + ] + ] + ], + 'theme' => [ + 'primary_color' => '#10b77f', + 'secondary_color' => '#ffffff', + 'accent_color' => '#f7f7f7', + 'logo_light' => '', + 'logo_dark' => '', + 'favicon' => '' + ], + 'seo' => [ + 'meta_title' => 'HRM SaaS - All-in-One HR Management Software', + 'meta_description' => 'Simplify employee management, payroll, attendance, recruitment, and performance with HRM SaaS, a modern HR platform.', + 'meta_keywords' => 'HR software, HRM, employee management, payroll, attendance tracking, recruitment, performance management' + ], + 'custom_css' => '', + 'custom_js' => '', + 'section_order' => ['header', 'hero', 'features', 'screenshots', 'why_choose_us', 'about', 'team', 'testimonials', 'plans', 'faq', 'newsletter', 'contact', 'footer'], + 'section_visibility' => [ + 'header' => true, + 'hero' => true, + 'features' => true, + 'screenshots' => true, + 'why_choose_us' => true, + 'about' => true, + 'team' => true, + 'testimonials' => true, + 'plans' => true, + 'faq' => true, + 'newsletter' => true, + 'contact' => true, + 'footer' => true + ] + ]; + } + + private static function getNonSaasConfig() + { + return [ + 'sections' => [ + [ + 'key' => 'header', + 'transparent' => false, + 'background_color' => '#ffffff', + 'text_color' => '#1f2937', + 'button_style' => 'gradient' + ], + [ + 'key' => 'hero', + 'title' => 'Simplify HR Management Effortlessly', + 'subtitle' => 'Manage employees, payroll, attendance, and more in one powerful platform.', + 'announcement_text' => '📢 New: Smart Leave & Attendance Tracking Launched!', + 'primary_button_text' => 'Get Started', + 'secondary_button_text' => 'Login', + 'image' => '', + 'background_color' => '#f8fafc', + 'text_color' => '#1f2937', + 'layout' => 'image-right', + 'height' => 600, + 'stats' => [ + ['value' => '10K+', 'label' => 'Active Users'], + ['value' => '50+', 'label' => 'Countries'], + ['value' => '99%', 'label' => 'Satisfaction'] + ], + ], + [ + 'key' => 'features', + 'title' => 'Empowering Businesses with Smart HR Solutions', + 'description' => 'All-in-one platform to manage employees, payroll, attendance, and performance with ease.', + 'background_color' => '#ffffff', + 'layout' => 'grid', + 'columns' => 3, + 'image' => '', + 'show_icons' => true, + 'features_list' => [ + ['title' => 'Employee Management', 'description' => 'Centralized profiles with personal, job, and document details.', 'icon' => 'users'], + ['title' => 'Payroll Automation', 'description' => 'Generate accurate payslips with tax, allowances, and deductions.', 'icon' => 'dollar-sign'], + ['title' => 'Leave & Attendance', 'description' => 'Smart tracking of leaves, shifts, and attendance logs.', 'icon' => 'clock'], + ['title' => 'Recruitment & Onboarding', 'description' => 'Streamline hiring with applicant tracking and digital onboarding.', 'icon' => 'user-plus'], + ['title' => 'Performance Management', 'description' => 'Set goals, run evaluations, and track employee growth.', 'icon' => 'award'], + ['title' => 'Reports & Analytics', 'description' => 'Get actionable insights on workforce productivity and HR metrics.', 'icon' => 'bar-chart-2'], + ] + ], + [ + 'key' => 'screenshots', + 'title' => 'See HRM in Action', + 'subtitle' => 'Discover how our modern HRM platform helps you manage employees, payroll, attendance, and performance — all in one place.', + 'screenshots_list' => [ + ['src' => '/screenshots/non-saas/dashboard.png', 'alt' => 'HRM Dashboard Overview', 'title' => 'Dashboard Overview', 'description' => 'Get a complete overview of employee data, payroll, and HR activities in one unified dashboard.'], + ['src' => '/screenshots/non-saas/employee-management.png', 'alt' => 'Employee Management Module', 'title' => 'Employee Management', 'description' => 'Centralized employee profiles with personal details, documents, and job history.'], + ['src' => '/screenshots/non-saas/payroll-payslip.png', 'alt' => 'Payroll Automation', 'title' => 'Payroll & Payslips', 'description' => 'Automated payroll processing with tax calculations, allowances, and downloadable payslips.'], + ['src' => '/screenshots/non-saas/leave.png', 'alt' => 'Leave Management', 'title' => 'Leave Management', 'description' => 'Easily apply, approve, and track employee leave requests with proper workflows and policies.'], + ['src' => '/screenshots/non-saas/attendance.png', 'alt' => 'Attendance Tracking', 'title' => 'Attendance Tracking', 'description' => 'Monitor employee check-ins, check-outs, and shifts with automated attendance logs.'], + ['src' => '/screenshots/non-saas/recruitment.png', 'alt' => 'Recruitment & Onboarding', 'title' => 'Recruitment & Onboarding', 'description' => 'Streamline hiring with applicant tracking and digital onboarding.'], + ] + ], + [ + 'key' => 'why_choose_us', + 'title' => 'Why Choose HRM?', + 'subtitle' => 'Smart, simple, and powerful HR solutions for every business.', + 'reasons' => [ + ['title' => 'All-in-One HR Solution', 'description' => 'Manage employees, payroll, attendance, recruitment, and performance from a single platform.', 'icon' => 'layers'], + ['title' => 'Time-Saving Automation', 'description' => 'Automate repetitive HR tasks to focus on strategic decision-making.', 'icon' => 'clock'], + ['title' => 'Data-Driven Insights', 'description' => 'Make informed decisions with advanced analytics and reports.', 'icon' => 'bar-chart'], + ['title' => 'Secure & Reliable', 'description' => 'Keep sensitive HR data safe with enterprise-grade security.', 'icon' => 'shield'] + ], + 'stats' => [ + ['value' => '500+', 'label' => 'Companies Using HRM', 'color' => 'blue'], + ['value' => '20K+', 'label' => 'Employees Managed', 'color' => 'green'], + ['value' => '98%', 'label' => 'Customer Satisfaction', 'color' => 'orange'] + ] + ], + [ + 'key' => 'about', + 'title' => 'About HRM', + 'description' => 'We are passionate about simplifying HR management for businesses of all sizes.', + 'story_title' => 'We are passionate about simplifying HR management for businesses of all sizes.', + 'story_content' => 'Founded by HR and tech enthusiasts, HRM was created to replace cumbersome spreadsheets and manual processes with a modern, all-in-one HR platform.', + 'image' => '', + 'background_color' => '#f9fafb', + 'layout' => 'image-right', + 'stats' => [ + ['value' => '3+ Years', 'label' => 'Experience', 'color' => 'blue'], + ['value' => '500+', 'label' => 'Companies Served', 'color' => 'green'], + ['value' => '20K+', 'label' => 'Employees Managed', 'color' => 'purple'] + ] + ], + [ + 'key' => 'team', + 'title' => 'Meet Our Team', + 'subtitle' => 'We\'re a dedicated team of HR and technology experts.', + 'cta_title' => 'Want to Join Our Team?', + 'cta_description' => 'We\'re always looking for talented individuals to shape the future of HR management.', + 'cta_button_text' => 'View Open Positions', + 'members' => [ + ['name' => 'John Doe', 'role' => 'CEO & Founder', 'bio' => 'Experienced HR tech entrepreneur passionate about building intuitive HR solutions.', 'image' => '', 'linkedin' => '#', 'email' => 'john@example.com'], + ['name' => 'Jane Smith', 'role' => 'CTO', 'bio' => 'Leads the tech team to create scalable and secure HR platforms.', 'image' => '', 'linkedin' => '#', 'email' => 'jane@example.com'], + ['name' => 'Michael Lee', 'role' => 'Head of Product', 'bio' => 'Designs user-centric features to simplify HR processes.', 'image' => '', 'linkedin' => '#', 'email' => 'michael@example.com'], + ['name' => 'Emily Davis', 'role' => 'HR Manager', 'bio' => 'Oversees employee engagement, recruitment, and HR operations.', 'image' => '', 'linkedin' => '#', 'email' => 'emily@example.com'] + ] + ], + [ + 'key' => 'testimonials', + 'title' => 'What Our Clients Say', + 'subtitle' => 'Hear from HR leaders who trust our platform.', + 'trust_title' => 'Trusted by HR Professionals Worldwide', + 'trust_stats' => [ + ['value' => '4.9/5', 'label' => 'Average Rating', 'color' => 'blue'], + ['value' => '500+', 'label' => 'Companies Served', 'color' => 'green'] + ], + 'testimonials' => [ + ['name' => 'Alice Johnson', 'role' => 'HR Manager', 'company' => 'GlobalTech Ltd.', 'content' => 'HRM has made managing employee records and attendance effortless. Our HR team saves hours every week!', 'rating' => 5], + ['name' => 'Robert Smith', 'role' => 'Operations Head', 'company' => 'Innovate Solutions', 'content' => 'The payroll automation is incredibly accurate and easy to use. No more manual calculations or errors!', 'rating' => 5], + ['name' => 'Maria Davis', 'role' => 'CEO', 'company' => 'BrightFuture Corp.', 'content' => 'From recruitment to performance management, HRM covers everything we need in one platform.', 'rating' => 5], + ['name' => 'David Lee', 'role' => 'Talent Acquisition Lead', 'company' => 'NextGen Enterprises', 'content' => 'Recruitment and onboarding have never been smoother. HRM platform is intuitive and efficient.', 'rating' => 5], + ['name' => 'Samantha Green', 'role' => 'Payroll Specialist', 'company' => 'BrightSolutions Inc.', 'content' => 'Payroll processing is now quick and error-free thanks to HRM. It has transformed our monthly workflow.', 'rating' => 5], + ['name' => 'Michael Brown', 'role' => 'HR Coordinator', 'company' => 'TechWave Ltd.', 'content' => 'The performance management module helps us track employee goals and progress effortlessly.', 'rating' => 5] + ] + ], + [ + 'key' => 'plans', + 'title' => 'Get Started with HRM', + 'subtitle' => 'Contact us to learn more about our HR management solution.', + 'faq_text' => 'Have questions? Contact our team for more information.' + ], + [ + 'key' => 'faq', + 'title' => 'Frequently Asked Questions', + 'subtitle' => 'Got questions? We\'ve got answers.', + 'cta_text' => 'Still have questions?', + 'button_text' => 'Contact Support', + 'faqs' => [ + ['question' => 'How does HRM work?', 'answer' => 'HRM is an all-in-one HR platform that helps you manage employees, payroll, attendance, recruitment, and performance efficiently.'], + ['question' => 'Can I automate payroll and leave tracking?', 'answer' => 'Yes! HRM allows you to automate payroll calculations, generate payslips, and track employee leaves and attendance seamlessly.'], + ['question' => 'Is my employee data secure?', 'answer' => 'Absolutely. HRM uses enterprise-grade security measures to keep all sensitive HR data safe and confidential.'], + ['question' => 'Can I manage recruitment and onboarding?', 'answer' => 'Yes, HRM provides applicant tracking, interview management, and digital onboarding tools to simplify hiring.'], + ['question' => 'Does HRM support performance evaluations?', 'answer' => 'Yes, you can set goals, track KPIs, and run performance reviews directly within the platform.'], + ['question' => 'Can HRM generate HR reports?', 'answer' => 'HRM offers advanced analytics and reporting features to give insights on attendance, payroll, and workforce performance.'], + ['question' => 'How can I get started?', 'answer' => 'Contact our team to learn more about implementing HRM for your organization.'] + ] + ], + [ + 'key' => 'newsletter', + 'title' => 'Stay Updated with HRM', + 'subtitle' => 'Get the latest updates, HR tips, and feature announcements.', + 'privacy_text' => 'No spam, unsubscribe at any time.', + 'benefits' => [ + ['icon' => '📧', 'title' => 'Weekly Updates', 'description' => 'Stay informed about the latest HRM features and improvements.'], + ['icon' => '💡', 'title' => 'HR Insights', 'description' => 'Get tips and best practices to optimize your HR operations.'], + ['icon' => '📊', 'title' => 'Reports & Trends', 'description' => 'Receive analytics insights and industry trends directly to your inbox.'] + ] + ], + [ + 'key' => 'contact', + 'title' => 'Get in Touch', + 'subtitle' => 'Have questions about HRM? We\'d love to hear from you.', + 'form_title' => 'Send us a Message', + 'info_title' => 'Contact Information', + 'info_description' => 'We\'re here to help and answer any questions you might have about managing your HR processes efficiently.', + 'layout' => 'split', + 'background_color' => '#f9fafb' + ], + [ + 'key' => 'footer', + 'description' => 'Simplifying HR management with an all-in-one modern platform.', + 'newsletter_title' => 'Stay Updated', + 'newsletter_subtitle' => 'Join our newsletter for HR tips and product updates', + 'links' => [ + 'product' => [ + ['name' => 'Features', 'href' => '#features'], + ['name' => 'Pricing', 'href' => '#pricing'] + ], + 'company' => [ + ['name' => 'About Us', 'href' => '#about'], + ['name' => 'Contact', 'href' => '#contact'] + ], + 'support' => [ + ['name' => 'Help Center', 'href' => '#help-center'], + ['name' => 'FAQ', 'href' => '#faq'], + ['name' => 'Refund Policy', 'href' => '#refund-policy'] + ], + 'legal' => [ + ['name' => 'Terms of Service', 'href' => '#terms'], + ['name' => 'Privacy Policy', 'href' => '#privacy'] + ] + ], + 'social_links' => [ + ['name' => 'Facebook', 'icon' => 'Facebook', 'href' => '#'], + ['name' => 'Twitter', 'icon' => 'Twitter', 'href' => '#'], + ['name' => 'LinkedIn', 'icon' => 'Linkedin', 'href' => '#'] + ], + 'section_titles' => [ + 'product' => 'Product', + 'company' => 'Company' + ] + ] + ], + 'theme' => [ + 'primary_color' => '#10b77f', + 'secondary_color' => '#ffffff', + 'accent_color' => '#f7f7f7', + 'logo_light' => '', + 'logo_dark' => '', + 'favicon' => '' + ], + 'seo' => [ + 'meta_title' => 'HRM - All-in-One HR Management Software', + 'meta_description' => 'Simplify employee management, payroll, attendance, recruitment, and performance with HRM, a modern HR platform.', + 'meta_keywords' => 'HR software, HRM, employee management, payroll, attendance tracking, recruitment, performance management' + ], + 'custom_css' => '', + 'custom_js' => '', + 'section_order' => ['header', 'hero', 'features', 'screenshots', 'why_choose_us', 'about', 'team', 'testimonials', 'plans', 'faq', 'newsletter', 'contact', 'footer'], + 'section_visibility' => [ + 'header' => true, + 'hero' => true, + 'features' => true, + 'screenshots' => true, + 'why_choose_us' => true, + 'about' => true, + 'team' => true, + 'testimonials' => true, + 'plans' => false, + 'faq' => true, + 'newsletter' => true, + 'contact' => true, + 'footer' => true + ] + ]; + } +} diff --git a/app/Models/LeaveApplication.php b/app/Models/LeaveApplication.php new file mode 100644 index 000000000..ff34a36ed --- /dev/null +++ b/app/Models/LeaveApplication.php @@ -0,0 +1,147 @@ + 'date', + 'end_date' => 'date', + 'approved_at' => 'datetime', + ]; + + /** + * Get the employee who applied for leave. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the leave type. + */ + public function leaveType() + { + return $this->belongsTo(LeaveType::class); + } + + /** + * Get the leave policy. + */ + public function leavePolicy() + { + return $this->belongsTo(LeavePolicy::class); + } + + /** + * Get the manager who approved/rejected. + */ + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Get the user who created the application. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Create attendance records and update leave balance when leave is approved. + */ + public function createAttendanceRecords() + { + if ($this->status === 'approved') { + $startDate = $this->start_date; + $endDate = $this->end_date; + + // Loop through each day of leave + for ($date = $startDate->copy(); $date->lte($endDate); $date->addDay()) { + // Skip weekends (optional - depends on company policy) + if ($date->isWeekend()) { + continue; + } + + // Check if attendance record already exists + $existingRecord = \App\Models\AttendanceRecord::where('employee_id', $this->employee_id) + ->where('date', $date->format('Y-m-d')) + ->first(); + + if (!$existingRecord) { + \App\Models\AttendanceRecord::create([ + 'employee_id' => $this->employee_id, + 'date' => $date->format('Y-m-d'), + 'status' => 'on_leave', + 'is_absent' => false, + 'total_hours' => 0, + 'notes' => 'Leave: ' . $this->leaveType->name, + 'created_by' => $this->created_by, + ]); + } else { + // Update existing record to on_leave + $existingRecord->update([ + 'status' => 'on_leave', + 'notes' => 'Leave: ' . $this->leaveType->name, + ]); + } + } + + // Update leave balance - deduct used days + $this->updateLeaveBalance(); + } + } + + /** + * Update employee leave balance when leave is approved. + */ + public function updateLeaveBalance() + { + $currentYear = now()->year; + + // Find or create leave balance for this employee, leave type, and year + $leaveBalance = \App\Models\LeaveBalance::firstOrCreate( + [ + 'employee_id' => $this->employee_id, + 'leave_type_id' => $this->leave_type_id, + 'year' => $currentYear, + ], + [ + 'leave_policy_id' => $this->leave_policy_id, + 'allocated_days' => $this->leavePolicy->max_days_per_year ?? 10, + 'used_days' => 0, + 'remaining_days' => $this->leavePolicy->max_days_per_year ?? 10, + 'created_by' => $this->created_by, + ] + ); + + // Deduct the leave days + $leaveBalance->used_days += $this->total_days; + $leaveBalance->remaining_days = $leaveBalance->allocated_days - $leaveBalance->used_days; + $leaveBalance->save(); + } +} \ No newline at end of file diff --git a/app/Models/LeaveBalance.php b/app/Models/LeaveBalance.php new file mode 100644 index 000000000..acddb2612 --- /dev/null +++ b/app/Models/LeaveBalance.php @@ -0,0 +1,74 @@ + 'decimal:2', + 'used_days' => 'decimal:2', + 'remaining_days' => 'decimal:2', + 'carried_forward' => 'decimal:2', + 'manual_adjustment' => 'decimal:2', + ]; + + /** + * Get the employee. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the leave type. + */ + public function leaveType() + { + return $this->belongsTo(LeaveType::class); + } + + /** + * Get the leave policy. + */ + public function leavePolicy() + { + return $this->belongsTo(LeavePolicy::class); + } + + /** + * Get the user who created the balance. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Calculate remaining days. + */ + public function calculateRemainingDays() + { + $this->remaining_days = ($this->allocated_days + $this->carried_forward + $this->manual_adjustment) - $this->used_days; + return $this->remaining_days; + } +} \ No newline at end of file diff --git a/app/Models/LeavePolicy.php b/app/Models/LeavePolicy.php new file mode 100644 index 000000000..70cabb12e --- /dev/null +++ b/app/Models/LeavePolicy.php @@ -0,0 +1,46 @@ + 'decimal:2', + 'requires_approval' => 'boolean', + ]; + + /** + * Get the leave type that owns the policy. + */ + public function leaveType() + { + return $this->belongsTo(LeaveType::class); + } + + /** + * Get the user who created the policy. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/LeaveType.php b/app/Models/LeaveType.php new file mode 100644 index 000000000..bbe972929 --- /dev/null +++ b/app/Models/LeaveType.php @@ -0,0 +1,33 @@ + 'boolean', + ]; + + /** + * Get the user who created the leave type. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/LoginHistory.php b/app/Models/LoginHistory.php new file mode 100644 index 000000000..5a3d105a5 --- /dev/null +++ b/app/Models/LoginHistory.php @@ -0,0 +1,26 @@ + 'datetime' + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/MediaDirectory.php b/app/Models/MediaDirectory.php new file mode 100644 index 000000000..8efdcb059 --- /dev/null +++ b/app/Models/MediaDirectory.php @@ -0,0 +1,15 @@ +addMediaCollection('images') + ->acceptsFile(function ($file) use ($allowedExtensions, $maxSizeBytes) { + // Check file extension + $fileName = $file->name ?? $file->getFilename(); + $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION)); + + if (!in_array($extension, $allowedExtensions)) { + return false; + } + + // Check file size + $fileSize = $file->size ?? filesize($file->getPathname()); + if ($fileSize > $maxSizeBytes) { + return false; + } + + return true; + }) + ->useDisk(StorageConfigService::getActiveDisk()); + } + + public function registerMediaConversions(Media $media = null): void + { + $this->addMediaConversion('thumb') + ->width(300) + ->height(300) + ->sharpen(10) + ->performOnCollections('images') + ->nonQueued(); + } +} \ No newline at end of file diff --git a/app/Models/Meeting.php b/app/Models/Meeting.php new file mode 100644 index 000000000..dd24d2401 --- /dev/null +++ b/app/Models/Meeting.php @@ -0,0 +1,68 @@ + 'date', + 'recurrence_end_date' => 'date', + ]; + + public function type() + { + return $this->belongsTo(MeetingType::class); + } + + public function room() + { + return $this->belongsTo(MeetingRoom::class); + } + + public function organizer() + { + return $this->belongsTo(User::class, 'organizer_id'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function attendees() + { + return $this->hasMany(MeetingAttendee::class); + } + + public function minutes() + { + return $this->hasMany(MeetingMinute::class); + } + + public function actionItems() + { + return $this->hasMany(ActionItem::class); + } +} \ No newline at end of file diff --git a/app/Models/MeetingAttendee.php b/app/Models/MeetingAttendee.php new file mode 100644 index 000000000..f5eecf98e --- /dev/null +++ b/app/Models/MeetingAttendee.php @@ -0,0 +1,41 @@ + 'datetime', + ]; + + public function meeting() + { + return $this->belongsTo(Meeting::class); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/MeetingMinute.php b/app/Models/MeetingMinute.php new file mode 100644 index 000000000..92b65f594 --- /dev/null +++ b/app/Models/MeetingMinute.php @@ -0,0 +1,40 @@ + 'datetime', + ]; + + public function meeting() + { + return $this->belongsTo(Meeting::class); + } + + public function recorder() + { + return $this->belongsTo(User::class, 'recorded_by'); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/MeetingRoom.php b/app/Models/MeetingRoom.php new file mode 100644 index 000000000..368b5eaed --- /dev/null +++ b/app/Models/MeetingRoom.php @@ -0,0 +1,37 @@ + 'array', + ]; + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function meetings() + { + return $this->hasMany(Meeting::class, 'room_id'); + } +} \ No newline at end of file diff --git a/app/Models/MeetingType.php b/app/Models/MeetingType.php new file mode 100644 index 000000000..49ddc8583 --- /dev/null +++ b/app/Models/MeetingType.php @@ -0,0 +1,30 @@ +belongsTo(User::class, 'created_by'); + } + + public function meetings() + { + return $this->hasMany(Meeting::class, 'type_id'); + } +} \ No newline at end of file diff --git a/app/Models/NewsLetter.php b/app/Models/NewsLetter.php new file mode 100644 index 000000000..c027a5588 --- /dev/null +++ b/app/Models/NewsLetter.php @@ -0,0 +1,13 @@ +belongsTo(User::class, 'created_by'); + } + + public static function getTemplate($language, $createdBy = null) + { + return self::where('language', $language) + ->where('created_by', $createdBy) + ->first(); + } + + public static function createTemplatesForCompany($companyId) + { + $templates = [ + 'ar' => '

شهادة عدم ممانعة

التاريخ: {date}

إلى من يهمه الأمر،

نشهد بأن {employee_name} يعمل حالياً لدى {company_name} بمنصب {designation}.

ليس لدينا أي اعتراض على الموظف المذكور أعلاه لأي أغراض رسمية.

مع خالص التقدير،
قسم الموارد البشرية
{company_name}

', + 'da' => '

Ingen indsigelse certifikat

Dato: {date}

Til hvem det måtte vedkomme,

Dette er for at bekræfte, at {employee_name} i øjeblikket er ansat hos {company_name} som {designation}.

Vi har ingen indvendinger mod ovennævnte medarbejder til officielle formål.

Med venlig hilsen,
HR-afdelingen
{company_name}

', + 'de' => '

Unbedenklichkeitsbescheinigung

Datum: {date}

An wen es betrifft,

Hiermit wird bestätigt, dass {employee_name} derzeit bei {company_name} als {designation} beschäftigt ist.

Wir haben keine Einwände gegen den oben genannten Mitarbeiter für offizielle Zwecke.

Mit freundlichen Grüßen,
Personalabteilung
{company_name}

', + 'en' => '

No Objection Certificate

Date: {date}

To Whom It May Concern,

This is to certify that {employee_name} is currently employed with {company_name} as {designation}.

We have no objection to the above mentioned employee for any official purposes.

Sincerely,
HR Department
{company_name}

', + 'es' => '

Certificado de No Objeción

Fecha: {date}

A quien corresponda,

Por la presente certificamos que {employee_name} está actualmente empleado en {company_name} como {designation}.

No tenemos objeción alguna al empleado mencionado anteriormente para cualquier propósito oficial.

Atentamente,
Departamento de RRHH
{company_name}

', + 'fr' => '

Certificat de Non-Objection

Date: {date}

À qui de droit,

Ceci certifie que {employee_name} est actuellement employé chez {company_name} en tant que {designation}.

Nous n\'avons aucune objection concernant l\'employé mentionné ci-dessus à des fins officielles.

Cordialement,
Département RH
{company_name}

', + 'he' => '

תעודת אי התנגדות

תאריך: {date}

למי שזה נוגע,

זאת להעיד כי {employee_name} מועסק כעת ב-{company_name} בתפקיד {designation}.

אין לנו התנגדות לעובד הנ"ל לכל מטרה רשמית.

בכבוד רב,
מחלקת משאבי אנוש
{company_name}

', + 'it' => '

Certificato di Non Obiezione

Data: {date}

A chi di competenza,

Si certifica che {employee_name} è attualmente impiegato presso {company_name} come {designation}.

Non abbiamo obiezioni riguardo al suddetto dipendente per scopi ufficiali.

Cordiali saluti,
Dipartimento HR
{company_name}

', + 'ja' => '

異議なし証明書

日付: {date}

関係者各位

{employee_name}が現在{company_name}で{designation}として雇用されていることを証明いたします。

上記従業員に関して、公的な目的での異議はございません。

敬具
人事部
{company_name}

', + 'nl' => '

Geen Bezwaar Certificaat

Datum: {date}

Aan wie het betreft,

Hierbij wordt bevestigd dat {employee_name} momenteel werkzaam is bij {company_name} als {designation}.

Wij hebben geen bezwaar tegen bovengenoemde werknemer voor officiële doeleinden.

Met vriendelijke groet,
HR Afdeling
{company_name}

', + 'pl' => '

Certyfikat Braku Sprzeciwu

Data: {date}

Do kogo to dotyczy,

Niniejszym poświadczamy, że {employee_name} jest obecnie zatrudniony w {company_name} na stanowisku {designation}.

Nie mamy sprzeciwu wobec wyżej wymienionego pracownika w celach urzędowych.

Z poważaniem,
Dział HR
{company_name}

', + 'pt' => '

Certificado de Não Objeção

Data: {date}

A quem possa interessar,

Certificamos que {employee_name} está atualmente empregado na {company_name} como {designation}.

Não temos objeção ao funcionário mencionado acima para fins oficiais.

Atenciosamente,
Departamento de RH
{company_name}

', + 'pt-BR' => '

Certificado de Não Objeção

Data: {date}

A quem possa interessar,

Certificamos que {employee_name} está atualmente empregado na {company_name} como {designation}.

Não temos objeção ao funcionário mencionado acima para fins oficiais.

Atenciosamente,
Departamento de RH
{company_name}

', + 'ru' => '

Справка об отсутствии возражений

Дата: {date}

Кого это касается,

Настоящим подтверждаем, что {employee_name} в настоящее время работает в {company_name} в должности {designation}.

У нас нет возражений против вышеупомянутого сотрудника для официальных целей.

С уважением,
Отдел кадров
{company_name}

', + 'tr' => '

İtiraz Yok Belgesi

Tarih: {date}

İlgili Makama,

{employee_name} adlı kişinin {company_name} şirketinde {designation} pozisyonunda çalıştığını onaylarız.

Yukarıda belirtilen çalışanımız için resmi amaçlar doğrultusunda herhangi bir itirazımız bulunmamaktadır.

Saygılarımızla,
İnsan Kaynakları Departmanı
{company_name}

', + 'zh' => '

无异议证明

日期:{date}

致相关人员:

兹证明{employee_name}目前在{company_name}担任{designation}职位。

我们对上述员工用于官方目的无任何异议。

此致
人力资源部
{company_name}

' + ]; + + $variables = json_encode(['date', 'company_name', 'employee_name', 'designation']); + + try { + foreach ($templates as $language => $content) { + self::updateOrCreate( + [ + 'language' => $language, + 'created_by' => $companyId + ], + [ + 'content' => $content, + 'variables' => $variables + ] + ); + } + return true; + } catch (\Exception $e) { + Log::error('Failed to create NOC templates for company ID: ' . $companyId . '. Error: ' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/app/Models/Offer.php b/app/Models/Offer.php new file mode 100644 index 000000000..da6c02ac1 --- /dev/null +++ b/app/Models/Offer.php @@ -0,0 +1,65 @@ + 'date', + 'start_date' => 'date', + 'expiration_date' => 'date', + 'response_date' => 'date', + 'salary' => 'decimal:2', + 'bonus' => 'decimal:2', + ]; + + public function candidate() + { + return $this->belongsTo(Candidate::class); + } + + public function job() + { + return $this->belongsTo(JobPosting::class); + } + + public function department() + { + return $this->belongsTo(Department::class); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } +} \ No newline at end of file diff --git a/app/Models/OfferTemplate.php b/app/Models/OfferTemplate.php new file mode 100644 index 000000000..5a0e953f8 --- /dev/null +++ b/app/Models/OfferTemplate.php @@ -0,0 +1,30 @@ + 'array', + ]; + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + +} \ No newline at end of file diff --git a/app/Models/OnboardingChecklist.php b/app/Models/OnboardingChecklist.php new file mode 100644 index 000000000..96d18cc1a --- /dev/null +++ b/app/Models/OnboardingChecklist.php @@ -0,0 +1,40 @@ + 'boolean', + ]; + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function checklistItems() + { + return $this->hasMany(ChecklistItem::class, 'checklist_id'); + } + + public function candidateOnboardings() + { + return $this->hasMany(CandidateOnboarding::class, 'checklist_id'); + } + + +} \ No newline at end of file diff --git a/app/Models/PaymentSetting.php b/app/Models/PaymentSetting.php new file mode 100644 index 000000000..d2b6f9371 --- /dev/null +++ b/app/Models/PaymentSetting.php @@ -0,0 +1,195 @@ + 'integer', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function setValueAttribute($value) + { + $sensitiveKeys = [ + 'stripe_secret', + 'paypal_secret_key', + 'stripe_key', + 'paypal_client_id', + 'razorpay_key', + 'razorpay_secret', + 'mercadopago_access_token', + 'paystack_public_key', + 'paystack_secret_key', + 'flutterwave_public_key', + 'flutterwave_secret_key', + 'paytabs_server_key', + 'paytabs_profile_id', + 'paytabs_region', + 'skrill_merchant_id', + 'skrill_secret_word', + 'coingate_api_token', + 'payfast_merchant_id', + 'payfast_merchant_key', + 'payfast_passphrase', + 'tap_secret_key', + 'xendit_api_key', + 'paytr_merchant_key', + 'paytr_merchant_salt', + 'mollie_api_key', + 'toyyibpay_secret_key', + 'paymentwall_public_key', + 'paymentwall_private_key', + 'sspay_secret_key', + 'benefit_secret_key', + 'benefit_public_key', + 'iyzipay_secret_key', + 'iyzipay_public_key', + 'aamarpay_signature', + 'midtrans_secret_key', + 'yookassa_secret_key', + 'nepalste_secret_key', + 'nepalste_public_key', + 'cinetpay_api_key', + 'cinetpay_secret_key', + 'payhere_merchant_secret', + 'payhere_app_secret', + 'fedapay_secret_key', + 'fedapay_public_key', + 'authorizenet_transaction_key', + 'khalti_secret_key', + 'khalti_public_key', + 'easebuzz_merchant_key', + 'easebuzz_salt_key', + 'ozow_private_key', + 'ozow_api_key', + 'cashfree_secret_key', + 'cashfree_public_key' + ]; + + // if (in_array($this->key, $sensitiveKeys) && $value) { + // $this->attributes['value'] = Crypt::encryptString($value); + // } else { + $this->attributes['value'] = is_bool($value) ? ($value ? '1' : '0') : $value; + // } + } + + public function getValueAttribute($value) + { + $sensitiveKeys = [ + 'stripe_secret', + 'paypal_secret_key', + 'stripe_key', + 'paypal_client_id', + 'razorpay_key', + 'razorpay_secret', + 'mercadopago_access_token', + 'paystack_public_key', + 'paystack_secret_key', + 'flutterwave_public_key', + 'flutterwave_secret_key', + 'paytabs_profile_id', + 'paytabs_server_key', + 'paytabs_region', + 'skrill_merchant_id', + 'skrill_secret_word', + 'coingate_api_token', + 'payfast_merchant_id', + 'payfast_merchant_key', + 'payfast_passphrase' + ]; + + $booleanKeys = [ + 'is_manually_enabled', + 'is_bank_enabled', + 'is_stripe_enabled', + 'is_paypal_enabled', + 'is_razorpay_enabled', + 'is_mercadopago_enabled', + 'is_paystack_enabled', + 'is_flutterwave_enabled', + 'is_paytabs_enabled', + 'is_skrill_enabled', + 'is_coingate_enabled', + 'is_payfast_enabled', + 'is_tap_enabled', + 'is_xendit_enabled', + 'is_paytr_enabled', + 'is_mollie_enabled', + 'is_toyyibpay_enabled', + 'is_paymentwall_enabled', + 'is_sspay_enabled', + 'is_benefit_enabled', + 'is_iyzipay_enabled', + 'is_aamarpay_enabled', + 'is_midtrans_enabled', + 'is_yookassa_enabled', + 'is_nepalste_enabled', + 'is_paiement_enabled', + 'is_cinetpay_enabled', + 'is_payhere_enabled', + 'is_fedapay_enabled', + 'is_authorizenet_enabled', + 'is_khalti_enabled', + 'is_easebuzz_enabled', + 'is_ozow_enabled', + 'is_cashfree_enabled' + ]; + + // if (in_array($this->key, $sensitiveKeys) && $value) { + // try { + // return Crypt::decryptString($value); + // } catch (\Exception $e) { + // return null; + // } + // } + + if (in_array($this->key, $booleanKeys)) { + return $value === '1' || $value === 1 || $value === true; + } + + return $value; + } + + public static function updateOrCreateSetting($userId, $key, $value) + { + return self::updateOrCreate( + ['user_id' => $userId, 'key' => $key], + ['value' => $value] + ); + } + + public static function getUserSettings($userId) + { + if (!$userId) { + return []; + } + + $settings = self::where('user_id', $userId)->pluck('value', 'key')->toArray(); + + // If no settings found for this user and it's not a superadmin, try to get from superadmin + if (empty($settings)) { + $user = \App\Models\User::find($userId); + if ($user && $user->type !== 'superadmin') { + $superAdmin = \App\Models\User::where('type', 'superadmin')->first(); + if ($superAdmin) { + $superAdminSettings = self::where('user_id', $superAdmin->id)->pluck('value', 'key')->toArray(); + // Merge settings, prioritizing user settings over superadmin settings + $settings = array_merge($superAdminSettings, $settings); + } + } + } + + return $settings; + } +} \ No newline at end of file diff --git a/app/Models/PayoutRequest.php b/app/Models/PayoutRequest.php new file mode 100644 index 000000000..c1c192d94 --- /dev/null +++ b/app/Models/PayoutRequest.php @@ -0,0 +1,25 @@ + 'decimal:2', + ]; + + public function company(): BelongsTo + { + return $this->belongsTo(User::class, 'company_id'); + } +} \ No newline at end of file diff --git a/app/Models/PayrollEntry.php b/app/Models/PayrollEntry.php new file mode 100644 index 000000000..334317760 --- /dev/null +++ b/app/Models/PayrollEntry.php @@ -0,0 +1,130 @@ + 'decimal:2', + 'component_earnings' => 'decimal:2', + 'total_earnings' => 'decimal:2', + 'total_deductions' => 'decimal:2', + 'gross_pay' => 'decimal:2', + 'net_pay' => 'decimal:2', + 'present_days' => 'decimal:2', + 'half_days' => 'decimal:2', + 'overtime_hours' => 'decimal:2', + 'overtime_amount' => 'decimal:2', + 'per_day_salary' => 'decimal:2', + 'unpaid_leave_deduction' => 'decimal:2', + 'earnings_breakdown' => 'array', + 'deductions_breakdown' => 'array', + ]; + + /** + * Get the payroll run. + */ + public function payrollRun() + { + return $this->belongsTo(PayrollRun::class); + } + + /** + * Get the employee. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the user who created the entry. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get attendance percentage. + */ + public function getAttendancePercentageAttribute() + { + if ($this->working_days == 0) { + return 0; + } + + return round(($this->present_days / $this->working_days) * 100, 2); + } + + /** + * Get complete salary breakdown showing all interconnections. + */ + public function getCompleteSalaryBreakdown() + { + $breakdown = [ + 'employee_name' => $this->employee->name, + 'pay_period' => $this->payrollRun->pay_period_start->format('M Y'), + + // Attendance Data (from Attendance Management) + 'attendance' => [ + 'total_working_days' => $this->working_days, + 'present_days' => $this->present_days, + 'attendance_percentage' => $this->attendance_percentage . '%', + 'overtime_hours' => $this->overtime_hours, + ], + + // Leave Data (from Leave Management) + 'leave_info' => [ + 'leave_days_taken' => $this->working_days - $this->present_days, + 'note' => 'Leave days are counted as present for salary calculation' + ], + + // Salary Components (from Payroll Management) + 'earnings' => $this->earnings_breakdown, + 'deductions' => $this->deductions_breakdown, + + // Final Calculation + 'calculation' => [ + 'gross_pay' => $this->gross_pay, + 'total_deductions' => $this->total_deductions, + 'net_pay' => $this->net_pay, + 'formula' => 'Net Pay = Gross Pay - Total Deductions' + ] + ]; + + return $breakdown; + } +} \ No newline at end of file diff --git a/app/Models/PayrollRun.php b/app/Models/PayrollRun.php new file mode 100644 index 000000000..9a677b279 --- /dev/null +++ b/app/Models/PayrollRun.php @@ -0,0 +1,310 @@ + 'date:Y-m-d', + 'pay_period_end' => 'date:Y-m-d', + 'pay_date' => 'date:Y-m-d', + 'total_gross_pay' => 'decimal:2', + 'total_deductions' => 'decimal:2', + 'total_net_pay' => 'decimal:2', + ]; + + /** + * Get the payroll entries. + */ + public function payrollEntries() + { + return $this->hasMany(PayrollEntry::class); + } + + /** + * Get the payslips through payroll entries. + */ + public function payslips() + { + return $this->hasManyThrough(Payslip::class, PayrollEntry::class); + } + + /** + * Get the user who created the payroll run. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Calculate and update totals. + */ + public function calculateTotals() + { + $entries = $this->payrollEntries; + + $this->total_gross_pay = $entries->sum('gross_pay'); + $this->total_deductions = $entries->sum('total_deductions'); + $this->total_net_pay = $entries->sum('net_pay'); + $this->employee_count = $entries->count(); + + $this->save(); + } + + /** + * Process payroll for all employees. + */ + public function processPayroll() + { + if ($this->status !== 'draft') { + return false; + } + + $this->status = 'draft'; + $this->save(); + + try { + // Get all active employees + $employees = User::with('employee')->where('type', 'employee') + ->whereIn('created_by', getCompanyAndUsersId()) + ->whereHas('employee', function ($q) { + $q->whereIn('employee_status', ['active', 'probation']); + }) + ->orderby('id', 'desc') + ->get(); + + foreach ($employees as $employee) { + $this->processEmployeePayroll($employee); + } + + $this->calculateTotals(); + $this->status = 'completed'; + $this->save(); + + return true; + } catch (\Exception $e) { + $this->status = 'draft'; + $this->save(); + throw $e; + } + } + + /** + * Process payroll for individual employee. + */ + private function processEmployeePayroll($employee) + { + // Skip if entry already exists for this employee in this run + $existingEntry = PayrollEntry::where('payroll_run_id', $this->id) + ->where('employee_id', $employee->id) + ->exists(); + + if ($existingEntry) { + return; + } + + // Working days config from global settings + $globalSettings = settings(); + $workingDaysIndices = json_decode($globalSettings['working_days'] ?? '[]', true); + + if (empty($workingDaysIndices)) { + throw new \Exception(__('Please configure working days first.')); + } + + // Get active salary record — holds the components list (earnings/deductions) + // calculateAllComponents will resolve base_salary from employees.base_salary internally + $employeeSalary = EmployeeSalary::getActiveSalary($employee->id); + + if (! $employeeSalary) { + return; + } + + // Pass $employee so calculateAllComponents resolves base_salary from employees table + // Returns null if employee has no base_salary configured + $salaryBreakdown = $employeeSalary->calculateAllComponents($employee); + + if (! $salaryBreakdown) { + return; + } + + $basicSalary = (float) $salaryBreakdown['basic_salary']; + $totalEarnings = (float) $salaryBreakdown['total_earnings']; // basic + all earning components + $totalDeductions = (float) $salaryBreakdown['total_deductions']; // sum of deduction components + + // --------------------------------------------------------------- + // STEP 1: Calculate total working days in pay period + // Only days matching configured working day indices are counted + // --------------------------------------------------------------- + $startDate = new \DateTime($this->pay_period_start); + $endDate = new \DateTime($this->pay_period_end); + $totalWorkingDays = 0; + + for ($date = clone $startDate; $date <= $endDate; $date->modify('+1 day')) { + if (in_array((int) $date->format('w'), $workingDaysIndices)) { + $totalWorkingDays++; + } + } + + // --------------------------------------------------------------- + // STEP 2: Attendance summary from attendance records + // --------------------------------------------------------------- + $attendanceRecords = AttendanceRecord::where('employee_id', $employee->id) + ->whereBetween('date', [$this->pay_period_start, $this->pay_period_end]) + ->get(); + + $fullPresentDays = (int) $attendanceRecords->where('status', 'present')->count(); + $halfDays = (int) $attendanceRecords->where('status', 'half_day')->count(); + $absentDays = (int) $attendanceRecords->where('status', 'absent')->count(); + $holidayDays = (int) $attendanceRecords->where('status', 'holiday')->count(); + $overtimeHours = (float) $attendanceRecords->sum('overtime_hours'); + $overtimeAmount = (float) $attendanceRecords->sum('overtime_amount'); + + // present_days stored = full present days + holiday days (both are fully paid) + $presentDays = $fullPresentDays + $holidayDays; + + // --------------------------------------------------------------- + // STEP 3: Leave data for pay period + // --------------------------------------------------------------- + $leaveData = $this->getEmployeeLeaveData($employee->id); + $paidLeaveDays = (float) $leaveData['paid_leave_days']; + $unpaidLeaveDays = (float) $leaveData['unpaid_leave_days']; + + // --------------------------------------------------------------- + // STEP 4: Effective paid days + // These are the days the employee is entitled to be paid for: + // - Full present days + // - Holiday days (company holidays = paid) + // - Approved paid leave days + // - Half days count as 0.5 each + // --------------------------------------------------------------- + $effectivePaidDays = $fullPresentDays + $holidayDays + $paidLeaveDays + ($halfDays * 0.5); + + // Cap effectivePaidDays to totalWorkingDays (cannot exceed total) + $effectivePaidDays = min((float) $effectivePaidDays, (float) $totalWorkingDays); + + // --------------------------------------------------------------- + // STEP 5: LOP (Loss of Pay) calculation + // LOP days = working days not covered by effective paid days + // This naturally covers absent days + any unaccounted days + // --------------------------------------------------------------- + $lopDays = max(0.0, $totalWorkingDays - $effectivePaidDays); + + // --------------------------------------------------------------- + // STEP 6: Per day salary — based on basic salary only + // Used for LOP deduction and unpaid leave deduction + // --------------------------------------------------------------- + $perDaySalary = $totalWorkingDays > 0 ? round($basicSalary / $totalWorkingDays, 4) : 0.0; + + // --------------------------------------------------------------- + // STEP 7: Deductions from salary + // lopDeduction = perDaySalary × lopDays + // unpaidLeaveDeduction = perDaySalary × unpaidLeaveDays + // (unpaid leaves are on top of LOP — explicit leave without pay) + // --------------------------------------------------------------- + $lopDeduction = round($perDaySalary * $lopDays, 2); + $unpaidLeaveDeduction = round($perDaySalary * $unpaidLeaveDays, 2); + + // --------------------------------------------------------------- + // STEP 8: Gross and Net salary + // grossSalary = total_earnings - lopDeduction - unpaidLeaveDeduction + overtime + // netSalary = grossSalary - component deductions + // Both clamped to 0 (cannot be negative) + // --------------------------------------------------------------- + $grossSalary = $totalEarnings - $lopDeduction - $unpaidLeaveDeduction + $overtimeAmount; + $grossSalary = max(0.0, round($grossSalary, 2)); + + $netSalary = max(0.0, round($grossSalary - $totalDeductions, 2)); + + // component_earnings = all earning components excluding basic salary + $componentEarnings = round($totalEarnings - $basicSalary, 2); + + // --------------------------------------------------------------- + // STEP 9: Persist payroll entry + // --------------------------------------------------------------- + PayrollEntry::create([ + 'payroll_run_id' => $this->id, + 'employee_id' => $employee->id, + 'basic_salary' => $basicSalary, + 'component_earnings' => $componentEarnings, + 'total_earnings' => $totalEarnings, + 'total_deductions' => $totalDeductions, + 'gross_pay' => $grossSalary, + 'net_pay' => $netSalary, + 'working_days' => $totalWorkingDays, + 'present_days' => $presentDays, + 'full_present_days' => $fullPresentDays, + 'half_days' => $halfDays, + 'holiday_days' => $holidayDays, + 'paid_leave_days' => $paidLeaveDays, + 'unpaid_leave_days' => $unpaidLeaveDays, + 'absent_days' => $absentDays, + 'overtime_hours' => $overtimeHours, + 'overtime_amount' => $overtimeAmount, + 'per_day_salary' => $perDaySalary, + 'unpaid_leave_deduction' => $unpaidLeaveDeduction, + 'earnings_breakdown' => $salaryBreakdown['earnings'], + 'deductions_breakdown' => $salaryBreakdown['deductions'], + 'created_by' => $this->created_by, + ]); + } + + /** + * Get employee leave data for pay period. + */ + private function getEmployeeLeaveData($employeeId) + { + $leaveApplications = \App\Models\LeaveApplication::where('employee_id', $employeeId) + ->where('status', 'approved') + ->where(function ($query) { + $query->whereBetween('start_date', [$this->pay_period_start, $this->pay_period_end]) + ->orWhereBetween('end_date', [$this->pay_period_start, $this->pay_period_end]) + ->orWhere(function ($q) { + $q->where('start_date', '<=', $this->pay_period_start) + ->where('end_date', '>=', $this->pay_period_end); + }); + }) + ->with('leaveType') + ->get(); + + $paidLeaveDays = 0; + $unpaidLeaveDays = 0; + + foreach ($leaveApplications as $leave) { + // Calculate days within pay period + $leaveStart = max($leave->start_date, $this->pay_period_start); + $leaveEnd = min($leave->end_date, $this->pay_period_end); + $leaveDays = $leaveStart->diffInDays($leaveEnd) + 1; + + if ($leave->leaveType->is_paid) { + $paidLeaveDays += $leaveDays; + } else { + $unpaidLeaveDays += $leaveDays; + } + } + + return [ + 'paid_leave_days' => $paidLeaveDays, + 'unpaid_leave_days' => $unpaidLeaveDays, + ]; + } +} diff --git a/app/Models/Payslip.php b/app/Models/Payslip.php new file mode 100644 index 000000000..b0a9ebb0c --- /dev/null +++ b/app/Models/Payslip.php @@ -0,0 +1,148 @@ + 'date', + 'pay_period_end' => 'date', + 'pay_date' => 'date', + 'sent_at' => 'datetime', + 'downloaded_at' => 'datetime', + ]; + + /** + * Get the payroll entry. + */ + public function payrollEntry() + { + return $this->belongsTo(PayrollEntry::class); + } + + /** + * Get the employee. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the user who created the payslip. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Generate payslip number. + */ + public static function generatePayslipNumber($employeeId, $payDate) + { + $date = \Carbon\Carbon::parse($payDate); + $prefix = 'PS-'.$date->format('Ym').'-'; + $employeeCode = str_pad($employeeId, 4, '0', STR_PAD_LEFT); + + return $prefix.$employeeCode; + } + + /** + * Generate PDF payslip. + */ + public function generatePDF() + { + $payrollEntry = $this->payrollEntry()->with(['employee', 'payrollRun'])->first(); + + if (! $payrollEntry) { + throw new \Exception('Payroll entry not found'); + } + + $companySettings = settings(); + $companyUser = User::find(getCompanyId(Auth::user()->id)); + + if ($companyUser) { + $companySettings = array_merge($companySettings, [ + 'companyEmail' => $companyUser->email ?? null, + ]); + } + + $data = [ + 'payslip' => $this, + 'payrollEntry' => $payrollEntry, + 'employee' => $payrollEntry->employee, + 'payrollRun' => $payrollEntry->payrollRun, + 'earnings' => $payrollEntry->earnings_breakdown ?? [], + 'deductions' => $payrollEntry->deductions_breakdown ?? [], + 'employeeData' => $payrollEntry->employee->employee, + 'companySettings' => $companySettings, + ]; + + $pdf = Pdf::loadView('payslips.template', $data); + + $fileName = 'payslip-'.$this->payslip_number.'.pdf'; + $filePath = 'payslips/'.$fileName; + + Storage::disk('public')->put($filePath, $pdf->output()); + + $this->update(['file_path' => $filePath]); + + return $filePath; + } + + /** + * Get download URL. + */ + public function getDownloadUrlAttribute() + { + if ($this->file_path) { + return Storage::disk('public')->url($this->file_path); + } + + return null; + } + + /** + * Mark as downloaded. + */ + public function markAsDownloaded() + { + $this->update([ + 'status' => 'downloaded', + 'downloaded_at' => now(), + ]); + } + + /** + * Mark as sent. + */ + public function markAsSent() + { + $this->update([ + 'status' => 'sent', + 'sent_at' => now(), + ]); + } +} diff --git a/app/Models/PerformanceIndicator.php b/app/Models/PerformanceIndicator.php new file mode 100644 index 000000000..c70674896 --- /dev/null +++ b/app/Models/PerformanceIndicator.php @@ -0,0 +1,37 @@ +belongsTo(User::class, 'created_by'); + } + + /** + * Get the category that this indicator belongs to. + */ + public function category() + { + return $this->belongsTo(PerformanceIndicatorCategory::class, 'category_id'); + } +} \ No newline at end of file diff --git a/app/Models/PerformanceIndicatorCategory.php b/app/Models/PerformanceIndicatorCategory.php new file mode 100644 index 000000000..8ee15518b --- /dev/null +++ b/app/Models/PerformanceIndicatorCategory.php @@ -0,0 +1,34 @@ +belongsTo(User::class, 'created_by'); + } + + /** + * Get the performance indicators for this category. + */ + public function indicators() + { + return $this->hasMany(PerformanceIndicator::class, 'category_id'); + } +} \ No newline at end of file diff --git a/app/Models/Permission.php b/app/Models/Permission.php new file mode 100644 index 000000000..81dfe26ed --- /dev/null +++ b/app/Models/Permission.php @@ -0,0 +1,16 @@ + 'boolean', + 'price' => 'float', + 'yearly_price' => 'float', + 'max_users' => 'integer', + 'max_employees' => 'integer', + 'trial_day' => 'integer', + ]; + + /** + * Get the default plan + * + * @return Plan|null + */ + public static function getDefaultPlan() + { + if (!isSaas()) { + return null; // No plans in non-SaaS + } + return self::where('is_default', true)->first(); + } + + /** + * Check if the plan is the default plan + * + * @return bool + */ + public function isDefault() + { + return (bool) $this->is_default; + } + + /** + * Get the price based on billing cycle + * + * @param string $cycle 'monthly' or 'yearly' + * @return float + */ + public function getPriceForCycle($cycle = 'monthly') + { + if ($cycle === 'yearly' && $this->yearly_price) { + return $this->yearly_price; + } + + return $this->price; + } + + /** + * Get users subscribed to this plan + */ + public function users() + { + return $this->hasMany(User::class); + } +} \ No newline at end of file diff --git a/app/Models/PlanOrder.php b/app/Models/PlanOrder.php new file mode 100644 index 000000000..91a010acf --- /dev/null +++ b/app/Models/PlanOrder.php @@ -0,0 +1,130 @@ + 'datetime', + 'processed_at' => 'datetime', + 'original_price' => 'decimal:2', + 'discount_amount' => 'decimal:2', + 'final_price' => 'decimal:2' + ]; + + protected static function boot() + { + parent::boot(); + + static::creating(function ($planOrder) { + if (empty($planOrder->order_number)) { + $planOrder->order_number = 'PO-' . strtoupper(Str::random(8)); + } + if (empty($planOrder->ordered_at)) { + $planOrder->ordered_at = now(); + } + }); + } + + public function user() + { + return $this->belongsTo(User::class); + } + + public function plan() + { + return $this->belongsTo(Plan::class); + } + + public function processedBy() + { + return $this->belongsTo(User::class, 'processed_by'); + } + + public function coupon() + { + return $this->belongsTo(Coupon::class); + } + + public function approve($processedBy = null) + { + $this->update([ + 'status' => 'approved', + 'processed_at' => now(), + 'processed_by' => $processedBy + ]); + + // Assign plan to user when approved + $expiresAt = $this->billing_cycle === 'yearly' ? now()->addYear() : now()->addMonth(); + $this->user->update([ + 'plan_id' => $this->plan_id, + 'plan_expire_date' => $expiresAt, + 'plan_is_active' => 1, + ]); + + // Create referral record if user was referred, passing billing cycle information + \App\Http\Controllers\ReferralController::createReferralRecord($this->user->fresh(), $this->billing_cycle); + } + + public function reject($processedBy = null, $notes = null) + { + $this->update([ + 'status' => 'rejected', + 'processed_at' => now(), + 'processed_by' => $processedBy, + 'notes' => $notes + ]); + } + + public function activateSubscription() + { + // Assign plan to user when payment is completed + $expiresAt = $this->billing_cycle === 'yearly' ? now()->addYear() : now()->addMonth(); + $this->user->update([ + 'plan_id' => $this->plan_id, + 'plan_expire_date' => $expiresAt, + 'plan_is_active' => 1, + ]); + } + + public function calculatePrices($planPrice, $coupon = null) + { + $this->original_price = $planPrice; + $this->discount_amount = 0; + $this->final_price = $planPrice; + + if ($coupon && $coupon->status) { + if ($coupon->type === 'percentage') { + $this->discount_amount = ($planPrice * $coupon->discount_amount) / 100; + } else { + $this->discount_amount = min($coupon->discount_amount, $planPrice); + } + + $this->final_price = $planPrice - $this->discount_amount; + $this->coupon_id = $coupon->id; + $this->coupon_code = $coupon->code; + } + } +} diff --git a/app/Models/PlanRequest.php b/app/Models/PlanRequest.php new file mode 100644 index 000000000..6e80444f8 --- /dev/null +++ b/app/Models/PlanRequest.php @@ -0,0 +1,44 @@ + 'datetime', + 'rejected_at' => 'datetime', + ]; + + public function user() + { + return $this->belongsTo(User::class); + } + + public function plan() + { + return $this->belongsTo(Plan::class); + } + + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + public function rejector() + { + return $this->belongsTo(User::class, 'rejected_by'); + } +} diff --git a/app/Models/Promotion.php b/app/Models/Promotion.php new file mode 100644 index 000000000..1d8117e79 --- /dev/null +++ b/app/Models/Promotion.php @@ -0,0 +1,48 @@ +belongsTo(User::class, 'employee_id'); + } + + /** + * Get the designation for this promotion. + */ + public function designation() + { + return $this->belongsTo(Designation::class); + } + + /** + * Get the user who created this promotion. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/Referral.php b/app/Models/Referral.php new file mode 100644 index 000000000..abc3746e8 --- /dev/null +++ b/app/Models/Referral.php @@ -0,0 +1,37 @@ + 'decimal:2', + 'amount' => 'decimal:2', + ]; + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function company(): BelongsTo + { + return $this->belongsTo(User::class, 'company_id'); + } + + public function plan(): BelongsTo + { + return $this->belongsTo(Plan::class); + } +} \ No newline at end of file diff --git a/app/Models/ReferralSetting.php b/app/Models/ReferralSetting.php new file mode 100644 index 000000000..6076663c1 --- /dev/null +++ b/app/Models/ReferralSetting.php @@ -0,0 +1,30 @@ + 'boolean', + 'commission_percentage' => 'decimal:2', + 'threshold_amount' => 'decimal:2', + ]; + + public static function current() + { + return static::first() ?? static::create([ + 'is_enabled' => true, + 'commission_percentage' => 10.00, + 'threshold_amount' => 50.00, + ]); + } +} \ No newline at end of file diff --git a/app/Models/Resignation.php b/app/Models/Resignation.php new file mode 100644 index 000000000..4a2dc2cf3 --- /dev/null +++ b/app/Models/Resignation.php @@ -0,0 +1,60 @@ + 'date:Y-m-d', + 'last_working_day' => 'date:Y-m-d', + 'approved_at' => 'datetime', + 'exit_interview_conducted' => 'boolean', + 'exit_interview_date' => 'date:Y-m-d', + ]; + + /** + * Get the employee who submitted this resignation. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the user who approved this resignation. + */ + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Get the user who created this resignation. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/ReviewCycle.php b/app/Models/ReviewCycle.php new file mode 100644 index 000000000..acd96e01b --- /dev/null +++ b/app/Models/ReviewCycle.php @@ -0,0 +1,35 @@ +belongsTo(User::class, 'created_by'); + } + + /** + * Get the employee reviews for this review cycle. + */ + public function reviews() + { + return $this->hasMany(EmployeeReview::class, 'review_cycle_id'); + } +} \ No newline at end of file diff --git a/app/Models/Role.php b/app/Models/Role.php new file mode 100644 index 000000000..8376cb502 --- /dev/null +++ b/app/Models/Role.php @@ -0,0 +1,33 @@ +belongsTo(User::class, 'created_by'); + } + + /** + * Check if this is a system role that shouldn't be deleted + * + * @return bool + */ + public function getIsSystemRoleAttribute() + { + $systemRoles = ['superadmin', 'super-admin', 'company']; + return in_array(strtolower($this->name), $systemRoles); + } +} \ No newline at end of file diff --git a/app/Models/SalaryComponent.php b/app/Models/SalaryComponent.php new file mode 100644 index 000000000..5c08a721e --- /dev/null +++ b/app/Models/SalaryComponent.php @@ -0,0 +1,71 @@ + 'decimal:2', + 'percentage_of_basic' => 'decimal:2', + 'is_taxable' => 'boolean', + 'is_mandatory' => 'boolean', + ]; + + /** + * Get the user who created the component. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Calculate component amount based on basic salary. + */ + public function calculateAmount($basicSalary = 0) + { + if ($this->calculation_type === 'percentage' && $this->percentage_of_basic) { + return ($basicSalary * $this->percentage_of_basic) / 100; + } + + return $this->default_amount; + } + + /** + * Get earnings components. + */ + public static function getEarnings() + { + return static::where('type', 'earning') + ->where('status', 'active') + ->get(); + } + + /** + * Get deductions components. + */ + public static function getDeductions() + { + return static::where('type', 'deduction') + ->where('status', 'active') + ->get(); + } +} \ No newline at end of file diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 000000000..6be27c748 --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,30 @@ +belongsTo(User::class); + } + + public static function getUserSettings($userId) + { + return self::where('user_id', $userId)->pluck('value', 'key')->toArray(); + } +} \ No newline at end of file diff --git a/app/Models/Shift.php b/app/Models/Shift.php new file mode 100644 index 000000000..ed49f0988 --- /dev/null +++ b/app/Models/Shift.php @@ -0,0 +1,86 @@ + 'boolean', + ]; + + /** + * Get the user who created the shift. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Calculate working hours for this shift. + */ + public function getWorkingHoursAttribute() + { + $start = \Carbon\Carbon::parse($this->start_time); + $end = \Carbon\Carbon::parse($this->end_time); + + // Handle night shifts + if ($this->is_night_shift && $end->lt($start)) { + $end->addDay(); + } + + $totalMinutes = abs($end->diffInMinutes($start)) - $this->break_duration; + return round(max(0, $totalMinutes) / 60, 2); + } + + /** + * Format start time for frontend (H:i format). + */ + public function getStartTimeAttribute($value) + { + return $value ? \Carbon\Carbon::parse($value)->format('H:i') : null; + } + + /** + * Format end time for frontend (H:i format). + */ + public function getEndTimeAttribute($value) + { + return $value ? \Carbon\Carbon::parse($value)->format('H:i') : null; + } + + /** + * Format break start time for frontend (H:i format). + */ + public function getBreakStartTimeAttribute($value) + { + return $value ? \Carbon\Carbon::parse($value)->format('H:i') : null; + } + + /** + * Format break end time for frontend (H:i format). + */ + public function getBreakEndTimeAttribute($value) + { + return $value ? \Carbon\Carbon::parse($value)->format('H:i') : null; + } +} \ No newline at end of file diff --git a/app/Models/Termination.php b/app/Models/Termination.php new file mode 100644 index 000000000..d10072969 --- /dev/null +++ b/app/Models/Termination.php @@ -0,0 +1,61 @@ + 'date', + 'notice_date' => 'date', + 'approved_at' => 'datetime', + 'exit_interview_conducted' => 'boolean', + 'exit_interview_date' => 'date', + ]; + + /** + * Get the employee who is being terminated. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the user who approved this termination. + */ + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Get the user who created this termination. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/TimeEntry.php b/app/Models/TimeEntry.php new file mode 100644 index 000000000..a1b878af2 --- /dev/null +++ b/app/Models/TimeEntry.php @@ -0,0 +1,78 @@ + 'date', + 'hours' => 'decimal:2', + 'approved_at' => 'datetime', + ]; + + /** + * Get the employee. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the manager who approved/rejected. + */ + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Get the user who created the entry. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get total hours for employee in date range. + */ + public static function getTotalHours($employeeId, $startDate, $endDate) + { + return static::where('employee_id', $employeeId) + ->where('status', 'approved') + ->whereBetween('date', [$startDate, $endDate]) + ->sum('hours'); + } + + /** + * Get project-wise hours for employee. + */ + public static function getProjectHours($employeeId, $startDate, $endDate) + { + return static::where('employee_id', $employeeId) + ->where('status', 'approved') + ->whereBetween('date', [$startDate, $endDate]) + ->selectRaw('project, SUM(hours) as total_hours') + ->groupBy('project') + ->get(); + } +} \ No newline at end of file diff --git a/app/Models/TrainingAssessment.php b/app/Models/TrainingAssessment.php new file mode 100644 index 000000000..38dc64906 --- /dev/null +++ b/app/Models/TrainingAssessment.php @@ -0,0 +1,73 @@ + 'decimal:2', + ]; + + /** + * Get the training program for this assessment. + */ + public function trainingProgram() + { + return $this->belongsTo(TrainingProgram::class); + } + + /** + * Get the user who created this assessment. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the employee assessment results for this assessment. + */ + public function employeeResults() + { + return $this->hasMany(EmployeeAssessmentResult::class); + } + + /** + * Scope a query to only include quiz assessments. + */ + public function scopeQuiz($query) + { + return $query->where('type', 'quiz'); + } + + /** + * Scope a query to only include practical assessments. + */ + public function scopePractical($query) + { + return $query->where('type', 'practical'); + } + + /** + * Scope a query to only include presentation assessments. + */ + public function scopePresentation($query) + { + return $query->where('type', 'presentation'); + } +} \ No newline at end of file diff --git a/app/Models/TrainingProgram.php b/app/Models/TrainingProgram.php new file mode 100644 index 000000000..aa0926d92 --- /dev/null +++ b/app/Models/TrainingProgram.php @@ -0,0 +1,120 @@ + 'decimal:2', + 'is_mandatory' => 'boolean', + 'is_self_enrollment' => 'boolean', + ]; + + /** + * Get the training type of this program. + */ + public function trainingType() + { + return $this->belongsTo(TrainingType::class); + } + + /** + * Get the user who created this training program. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the sessions for this training program. + */ + public function sessions() + { + return $this->hasMany(TrainingSession::class); + } + + /** + * Get the employee trainings for this program. + */ + public function employeeTrainings() + { + return $this->hasMany(EmployeeTraining::class); + } + + /** + * Get the assessments for this training program. + */ + public function assessments() + { + return $this->hasMany(TrainingAssessment::class); + } + + /** + * Scope a query to only include active training programs. + */ + public function scopeActive($query) + { + return $query->where('status', 'active'); + } + + /** + * Scope a query to only include draft training programs. + */ + public function scopeDraft($query) + { + return $query->where('status', 'draft'); + } + + /** + * Scope a query to only include completed training programs. + */ + public function scopeCompleted($query) + { + return $query->where('status', 'completed'); + } + + /** + * Scope a query to only include cancelled training programs. + */ + public function scopeCancelled($query) + { + return $query->where('status', 'cancelled'); + } + + /** + * Scope a query to only include mandatory training programs. + */ + public function scopeMandatory($query) + { + return $query->where('is_mandatory', true); + } + + /** + * Scope a query to only include self-enrollment training programs. + */ + public function scopeSelfEnrollment($query) + { + return $query->where('is_self_enrollment', true); + } +} \ No newline at end of file diff --git a/app/Models/TrainingSession.php b/app/Models/TrainingSession.php new file mode 100644 index 000000000..7d63a0197 --- /dev/null +++ b/app/Models/TrainingSession.php @@ -0,0 +1,138 @@ + 'datetime', + 'end_date' => 'datetime', + 'is_recurring' => 'boolean', + ]; + + /** + * Get the training program for this session. + */ + public function trainingProgram() + { + return $this->belongsTo(TrainingProgram::class); + } + + /** + * Get the user who created this training session. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the trainers for this session. + */ + public function trainers() + { + return $this->belongsToMany(User::class, 'training_session_trainer', 'training_session_id', 'employee_id'); + } + + /** + * Get the attendance records for this session. + */ + public function attendance() + { + return $this->hasMany(TrainingSessionAttendance::class); + } + + /** + * Scope a query to only include scheduled sessions. + */ + public function scopeScheduled($query) + { + return $query->where('status', 'scheduled'); + } + + /** + * Scope a query to only include in-progress sessions. + */ + public function scopeInProgress($query) + { + return $query->where('status', 'in_progress'); + } + + /** + * Scope a query to only include completed sessions. + */ + public function scopeCompleted($query) + { + return $query->where('status', 'completed'); + } + + /** + * Scope a query to only include cancelled sessions. + */ + public function scopeCancelled($query) + { + return $query->where('status', 'cancelled'); + } + + /** + * Scope a query to only include upcoming sessions. + */ + public function scopeUpcoming($query) + { + return $query->where('start_date', '>', now()) + ->where('status', 'scheduled'); + } + + /** + * Scope a query to only include sessions for today. + */ + public function scopeToday($query) + { + return $query->whereDate('start_date', now()->toDateString()); + } + + /** + * Check if the session is virtual. + */ + public function isVirtual() + { + return $this->location_type === 'virtual'; + } + + /** + * Check if the session is physical. + */ + public function isPhysical() + { + return $this->location_type === 'physical'; + } + + /** + * Get the duration of the session in hours. + */ + public function getDurationInHours() + { + return $this->start_date->diffInHours($this->end_date); + } +} \ No newline at end of file diff --git a/app/Models/TrainingSessionAttendance.php b/app/Models/TrainingSessionAttendance.php new file mode 100644 index 000000000..2b459228f --- /dev/null +++ b/app/Models/TrainingSessionAttendance.php @@ -0,0 +1,56 @@ + 'boolean', + ]; + + /** + * Get the training session for this attendance record. + */ + public function trainingSession() + { + return $this->belongsTo(TrainingSession::class); + } + + /** + * Get the employee for this attendance record. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Scope a query to only include present attendances. + */ + public function scopePresent($query) + { + return $query->where('is_present', true); + } + + /** + * Scope a query to only include absent attendances. + */ + public function scopeAbsent($query) + { + return $query->where('is_present', false); + } +} \ No newline at end of file diff --git a/app/Models/TrainingType.php b/app/Models/TrainingType.php new file mode 100644 index 000000000..394ced537 --- /dev/null +++ b/app/Models/TrainingType.php @@ -0,0 +1,50 @@ +belongsTo(User::class, 'created_by'); + } + + /** + * Get the branch this training type belongs to. + */ + public function branch() + { + return $this->belongsTo(Branch::class); + } + + /** + * Get the departments associated with this training type. + */ + public function departments() + { + return $this->belongsToMany(Department::class, 'training_type_department'); + } + + /** + * Get the training programs of this type. + */ + public function trainingPrograms() + { + return $this->hasMany(TrainingProgram::class); + } +} \ No newline at end of file diff --git a/app/Models/Trip.php b/app/Models/Trip.php new file mode 100644 index 000000000..2a01f47d6 --- /dev/null +++ b/app/Models/Trip.php @@ -0,0 +1,71 @@ + 'date', + 'end_date' => 'date', + 'approved_at' => 'datetime', + 'advance_amount' => 'decimal:2', + 'total_expenses' => 'decimal:2', + ]; + + /** + * Get the employee associated with this trip. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the user who approved this trip. + */ + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Get the user who created this trip. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the expenses for this trip. + */ + public function expenses() + { + return $this->hasMany(TripExpense::class); + } +} \ No newline at end of file diff --git a/app/Models/TripExpense.php b/app/Models/TripExpense.php new file mode 100644 index 000000000..1fba04651 --- /dev/null +++ b/app/Models/TripExpense.php @@ -0,0 +1,46 @@ + 'date', + 'amount' => 'decimal:2', + 'is_reimbursable' => 'boolean', + ]; + + /** + * Get the trip that owns this expense. + */ + public function trip() + { + return $this->belongsTo(Trip::class); + } + + /** + * Get the user who created this expense. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 000000000..30f313426 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,1399 @@ + 'datetime', + 'password' => 'hashed', + 'plan_expire_date' => 'date', + 'trial_expire_date' => 'date', + 'plan_is_active' => 'integer', + 'is_active' => 'integer', + 'is_enable_login' => 'integer', + 'google2fa_enable' => 'integer', + 'storage_limit' => 'float', + ]; + } + + public $not_emp_type = [ + 'superadmin', + 'company', + ]; + + public function scopeEmp($query, $additionalTypes = [], $includeTypes = []) + { + $excludeTypes = array_diff(array_merge($this->not_emp_type, $additionalTypes), $includeTypes); + + return $query->whereNotIn('type', $excludeTypes); + } + + /** + * Get the creator ID based on user type + */ + public function creatorId() + { + if (isSaas()) { + if ($this->type == 'superadmin' || $this->type == 'super admin' || $this->type == 'admin') { + return $this->id; + } else { + return $this->created_by; + } + } else { + // Non-SaaS: Company is the top level + if ($this->type == 'company') { + return $this->id; + } else { + return $this->created_by; + } + } + } + + /** + * Check if user is super admin + */ + public function isSuperAdmin() + { + if (! isSaas()) { + return false; // No super admin in non-SaaS + } + + return $this->type === 'superadmin' || $this->type === 'super admin'; + } + + /** + * Check if user is admin + */ + public function isAdmin() + { + return $this->type === 'admin'; + } + + // Businesses relationship removed + + /** + * Get the plan associated with the user. + */ + public function plan() + { + if (! isSaas()) { + return null; // No plans in non-SaaS + } + + return $this->belongsTo(Plan::class); + } + + /** + * Check if user is on free plan + */ + public function isOnFreePlan() + { + if (! isSaas()) { + return false; // No plans in non-SaaS + } + + return $this->plan && $this->plan->is_default; + } + + /** + * Get current plan or default plan + */ + public function getCurrentPlan() + { + if (! isSaas()) { + return null; // No plans in non-SaaS + } + + if ($this->plan) { + return $this->plan; + } + + return Plan::getDefaultPlan(); + } + + /** + * Check if user has an active plan subscription + */ + public function hasActivePlan() + { + if (! isSaas()) { + return true; // Always active in non-SaaS + } + + return $this->plan_id && + $this->plan_is_active && + ($this->plan_expire_date === null || $this->plan_expire_date > now()); + } + + /** + * Check if user's plan has expired + */ + public function isPlanExpired() + { + if (! isSaas()) { + return false; // No expiration in non-SaaS + } + + return $this->plan_expire_date && $this->plan_expire_date < now(); + } + + /** + * Check if user's trial has expired + */ + public function isTrialExpired() + { + if (! isSaas()) { + return false; // No trials in non-SaaS + } + + return $this->is_trial && $this->trial_expire_date && $this->trial_expire_date < now(); + } + + /** + * Check if user needs to subscribe to a plan + */ + public function needsPlanSubscription() + { + if (! isSaas()) { + return false; // No subscriptions in non-SaaS + } + + if ($this->isSuperAdmin()) { + return false; + } + + if ($this->type !== 'company') { + return false; + } + + // Check if user has no plan and no default plan exists + if (! $this->plan_id) { + return ! Plan::getDefaultPlan(); + } + + // Check if trial is expired + if ($this->isTrialExpired()) { + return true; + } + + // Check if plan is expired (but not on trial) + if (! $this->is_trial && $this->isPlanExpired()) { + return true; + } + + return false; + } + + /** + * Check if user can be impersonated + */ + public function canBeImpersonated() + { + return $this->type === 'company'; + } + + /** + * Check if user can impersonate others + */ + public function canImpersonate() + { + if (! isSaas()) { + return false; // No impersonation in non-SaaS + } + + return $this->isSuperAdmin(); + } + + /** + * Get referrals made by this company + */ + public function referrals() + { + if (! isSaas()) { + return $this->hasMany(Referral::class, 'user_id')->whereRaw('1 = 0'); // Empty relation in non-SaaS + } + + return $this->hasMany(Referral::class, 'user_id'); + } + + /** + * Get payout requests made by this company + */ + public function payoutRequests() + { + if (! isSaas()) { + return $this->hasMany(PayoutRequest::class, 'company_id')->whereRaw('1 = 0'); // Empty relation in non-SaaS + } + + return $this->hasMany(PayoutRequest::class, 'company_id'); + } + + /** + * Get the user who created this user + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Get the employee record associated with the user + */ + public function employee() + { + return $this->hasOne(Employee::class, 'user_id'); + } + + /** + * Get referral balance for company + */ + public function getReferralBalance() + { + if (! isSaas()) { + return 0; // No referrals in non-SaaS + } + + $totalEarned = $this->referrals()->sum('amount'); + $totalRequested = $this->payoutRequests()->whereIn('status', ['pending', 'approved'])->sum('amount'); + + return $totalEarned - $totalRequested; + } + + /** + * Send the email verification notification with dynamic config. + */ + public function sendEmailVerificationNotification() + { + try { + MailConfigService::setDynamicConfig(); + parent::sendEmailVerificationNotification(); + + return ['success' => true, 'message' => 'Verification email sent successfully']; + } catch (\Exception $e) { + Log::error('Email verification failed', [ + 'user_id' => $this->id, + 'email' => $this->email, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return ['success' => false, 'message' => 'Failed to send verification email: '.$e->getMessage()]; + } + } + + /** + * Boot method to handle model events + */ + protected static function boot() + { + parent::boot(); + + static::creating(function ($user) { + if (isSaas() && $user->type === 'company' && ! $user->referral_code) { + // Generate referral code after the user is saved to get the ID + static::created(function ($createdUser) { + if (! $createdUser->referral_code) { + $createdUser->referral_code = 'REF'.str_pad($createdUser->id, 6, '0', STR_PAD_LEFT); + $createdUser->save(); + } + }); + } + }); + + static::created(function ($user) { + // Assign default plan to company users only in SaaS mode + if (isSaas() && $user->type === 'company' && ! $user->plan_id) { + $defaultPlan = Plan::getDefaultPlan(); + if ($defaultPlan) { + $user->plan_id = $defaultPlan->id; + $user->plan_is_active = 1; + $user->save(); + } + } + }); + + // Generate Slug When New user Creating + static::creating(function ($user) { + if (empty($user->slug)) { + $user->slug = static::generateUniqueSlug($user->name); + } + }); + + // Generate Slug When Update the User if Slug is Empty then only + static::updating(function ($user) { + if (empty($user->slug) && ! empty($user->name)) { + $user->slug = static::generateUniqueSlug($user->name); + } + }); + } + + public static function generateUniqueSlug($name) + { + $slug = Str::slug($name); + $originalSlug = $slug; + $counter = 1; + + while (static::where('slug', $slug)->exists()) { + $slug = $originalSlug.'-'.$counter; + $counter++; + } + + return $slug; + } + + public function planOrders() + { + if (! isSaas()) { + return $this->hasMany(PlanOrder::class)->whereRaw('1 = 0'); // Empty relation in non-SaaS + } + + return $this->hasMany(PlanOrder::class); + } + + public function companyDefaultData($company) + { + $roles = [ + 'employee' => [ + 'label' => 'Employee', + 'description' => 'Employee Role', + 'permissions' => $this->getEmployeePermissions(), + ], + 'manager' => [ + 'label' => 'Manager', + 'description' => 'Manager Role', + 'permissions' => $this->getManagerPermissions(), + ], + 'hr' => [ + 'label' => 'HR', + 'description' => 'HR Role', + 'permissions' => $this->getHRPermissions(), + ], + ]; + + foreach ($roles as $name => $data) { + $role = Role::firstOrCreate( + [ + 'name' => $name, + 'guard_name' => 'web', + 'created_by' => $company->id, + ], + [ + 'label' => $data['label'], + 'description' => $data['description'], + 'created_by' => $company->id, + ] + ); + + $permissions = Permission::whereIn('name', $data['permissions'])->get(); + $role->syncPermissions($permissions); + } + } + + private function getEmployeePermissions(): array + { + return [ + 'manage-dashboard', + 'view-dashboard', + + 'manage-calendar', + 'view-calendar', + 'manage-analytics', + + // Media Permissions + 'manage-media', + 'manage-any-media', + 'create-media', + 'edit-media', + 'delete-media', + 'view-media', + 'download-media', + + // Media Directory + 'manage-media-directories', + 'manage-any-media-directories', + 'create-media-directories', + 'edit-media-directories', + 'delete-media-directories', + + // Employee permissions + 'manage-employees', + 'manage-own-employees', + 'view-employees', + + // Award permissions + 'manage-awards', + 'manage-own-awards', + 'view-awards', + + // Promotion permissions + 'manage-promotions', + 'manage-own-promotions', + 'view-promotions', + + // Resignation permissions + 'manage-resignations', + 'view-resignations', + 'manage-own-resignations', + 'create-resignations', + 'edit-resignations', + 'delete-resignations', + + // Termination permissions + 'manage-terminations', + 'manage-own-terminations', + 'view-terminations', + + // Warning permissions + 'manage-warnings', + 'manage-own-warnings', + 'view-warnings', + + // Trip permissions + 'manage-trips', + 'manage-own-trips', + 'view-trips', + + // Complaint permissions + 'manage-complaints', + 'manage-own-complaints', + 'view-complaints', + 'create-complaints', + 'edit-complaints', + 'delete-complaints', + + // Employee Transfer permissions + 'manage-employee-transfers', + 'manage-own-employee-transfers', + 'view-employee-transfers', + + // Holiday permissions + 'manage-holidays', + 'manage-any-holidays', + 'view-holidays', + + // Announcement permissions + 'manage-announcements', + 'manage-any-announcements', + 'view-announcements', + + // Asset Type permissions + 'manage-asset-types', + 'manage-any-asset-types', + 'view-asset-types', + + // Asset permissions + 'manage-assets', + 'view-assets', + + // Training Program permissions + 'manage-training-programs', + 'manage-any-training-programs', + 'view-training-programs', + + // Training Session permissions + 'manage-training-sessions', + 'manage-own-training-sessions', + 'view-training-sessions', + 'manage-attendance', + + // Employee Training permissions + 'manage-employee-trainings', + 'manage-own-employee-trainings', + 'view-employee-trainings', + 'assign-trainings', + 'manage-assessments', + 'record-assessment-results', + + // Performance Indicators + 'manage-performance-indicators', + 'manage-own-performance-indicators', + 'view-performance-indicators', + + // Employee Goals + 'manage-employee-goals', + 'manage-own-employee-goals', + 'view-employee-goals', + + // Review Cycles + 'manage-review-cycles', + 'manage-own-review-cycles', + 'view-review-cycles', + + // Employee Reviews + 'manage-employee-reviews', + 'manage-own-employee-reviews', + 'view-employee-reviews', + + // Job Requisitions management + 'manage-job-requisitions', + 'manage- -job-requisitions', + 'view-job-requisitions', + + // Job Locations management + 'manage-job-locations', + 'manage-any-job-locations', + 'view-job-locations', + + // Job Postings management + 'manage-job-postings', + 'manage-any-job-postings', + 'view-job-postings', + + // Interview Rounds management + 'manage-interview-rounds', + 'manage-any-interview-rounds', + 'view-interview-rounds', + + // Interviews management + 'manage-interviews', + 'manage-own-interviews', + 'view-interviews', + + // Interview Feedback management + 'manage-interview-feedback', + 'manage-own-interview-feedback', + 'view-interview-feedback', + 'create-interview-feedback', + 'edit-interview-feedback', + 'delete-interview-feedback', + + // Candidate Assessments management + 'manage-candidate-assessments', + 'manage-own-candidate-assessments', + 'view-candidate-assessments', + 'create-candidate-assessments', + 'edit-candidate-assessments', + 'delete-candidate-assessments', + + // Candidate Onboarding management + 'manage-candidate-onboarding', + 'manage-own-candidate-onboarding', + 'view-candidate-onboarding', + + // Meetings management + 'manage-meetings', + 'manage-own-meetings', + 'view-meetings', + + // Meeting Attendees management + 'manage-meeting-attendees', + 'manage-any-meeting-attendees', + 'view-meeting-attendees', + 'edit-meeting-attendees', + + // Meeting Minutes management + 'manage-meeting-minutes', + 'manage-own-meeting-minutes', + 'view-meeting-minutes', + + // Action Items management + 'manage-action-items', + 'manage-own-action-items', + 'view-action-items', + + // Employee Contracts management + 'manage-employee-contracts', + 'manage-own-employee-contracts', + 'view-employee-contracts', + + // Contract Renewals management + 'manage-contract-renewals', + 'view-contract-renewals', + + // HR Documents management + 'manage-hr-documents', + 'manage-any-hr-documents', + 'view-hr-documents', + + // Document Acknowledgments management + 'manage-document-acknowledgments', + 'manage-own-document-acknowledgments', + 'view-document-acknowledgments', + + // Leave Policies management + 'manage-leave-policies', + 'manage-any-leave-policies', + + // Leave Applications management + 'manage-leave-applications', + 'manage-own-leave-applications', + 'view-leave-applications', + 'create-leave-applications', + 'edit-leave-applications', + 'delete-leave-applications', + + // Leave Balances management + 'manage-leave-balances', + 'manage-own-leave-balances', + 'view-leave-balances', + + // Shifts management + 'manage-shifts', + 'manage-any-shifts', + 'view-shifts', + + // Attendance Policies management + 'manage-attendance-policies', + 'manage-any-attendance-policies', + 'view-attendance-policies', + + // Attendance Records management + 'manage-attendance-records', + 'manage-own-attendance-records', + 'view-attendance-records', + 'create-attendance-records', + 'edit-attendance-records', + 'delete-attendance-records', + 'clock-in-out', + + // Attendance Regularizations management + 'manage-attendance-regularizations', + 'manage-own-attendance-regularizations', + 'view-attendance-regularizations', + 'create-attendance-regularizations', + 'edit-attendance-regularizations', + 'delete-attendance-regularizations', + + // Time Entries management + 'manage-time-entries', + 'manage-own-time-entries', + 'view-time-entries', + 'create-time-entries', + 'edit-time-entries', + + // Employee Salaries management + 'manage-employee-salaries', + 'manage-own-employee-salaries', + 'view-employee-salaries', + + // Payslips management + 'manage-payslips', + 'manage-own-payslips', + 'download-payslips', + ]; + } + + private function getManagerPermissions(): array + { + return [ + 'manage-dashboard', + 'view-dashboard', + + 'manage-calendar', + 'view-calendar', + 'manage-analytics', + + // Media Permissions + 'manage-media', + 'manage-any-media', + 'create-media', + 'edit-media', + 'delete-media', + 'view-media', + 'download-media', + + // Media Directory + 'manage-media-directories', + 'manage-any-media-directories', + 'create-media-directories', + 'edit-media-directories', + 'delete-media-directories', + + // Branch Permissions + 'manage-branches', + 'manage-any-branches', + 'view-branches', + 'create-branches', + 'edit-branches', + 'delete-branches', + 'toggle-status-branches', + + // Department Permissions + 'manage-departments', + 'manage-any-departments', + 'view-departments', + 'create-departments', + 'edit-departments', + 'delete-departments', + 'toggle-status-departments', + + // Designation Permissions + 'manage-designations', + 'manage-any-designations', + 'view-designations', + 'create-designations', + 'edit-designations', + 'delete-designations', + 'toggle-status-designations', + + // Document Type Permissions + 'manage-document-types', + 'manage-any-document-types', + 'view-document-types', + 'create-document-types', + 'edit-document-types', + 'delete-document-types', + + // Employee permissions + 'manage-employees', + 'manage-any-employees', + 'view-employees', + 'create-employees', + 'edit-employees', + + // Award Type management + 'manage-award-types', + 'manage-any-award-types', + 'view-award-types', + 'create-award-types', + 'edit-award-types', + 'delete-award-types', + + // Award Type management + 'manage-award-types', + 'manage-any-award-types', + 'view-award-types', + 'create-award-types', + 'edit-award-types', + 'delete-award-types', + + // Award management + 'manage-awards', + 'manage-any-awards', + 'view-awards', + 'create-awards', + 'edit-awards', + 'delete-awards', + + // Promotion management + 'manage-promotions', + 'manage-any-promotions', + 'view-promotions', + 'create-promotions', + 'edit-promotions', + 'delete-promotions', + 'approve-promotions', + 'reject-promotions', + + // Resignation management + 'manage-resignations', + 'manage-any-resignations', + 'view-resignations', + 'create-resignations', + 'edit-resignations', + 'delete-resignations', + 'approve-resignations', + 'reject-resignations', + + // Termination management + 'manage-terminations', + 'manage-any-terminations', + 'view-terminations', + 'create-terminations', + 'edit-terminations', + 'delete-terminations', + 'approve-terminations', + 'reject-terminations', + + // Warning management + 'manage-warnings', + 'manage-any-warnings', + 'view-warnings', + 'create-warnings', + 'edit-warnings', + 'delete-warnings', + 'approve-warnings', + 'acknowledge-warnings', + + // Trip management + 'manage-trips', + 'manage-any-trips', + 'view-trips', + 'create-trips', + 'edit-trips', + 'delete-trips', + 'approve-trips', + 'manage-trip-expenses', + 'approve-trip-expenses', + + // Complaint management + 'manage-complaints', + 'manage-any-complaints', + 'view-complaints', + 'create-complaints', + 'edit-complaints', + 'delete-complaints', + 'assign-complaints', + 'resolve-complaints', + + // Employee Transfer management + 'manage-employee-transfers', + 'manage-any-employee-transfers', + 'view-employee-transfers', + 'create-employee-transfers', + 'edit-employee-transfers', + 'delete-employee-transfers', + 'approve-employee-transfers', + 'reject-employee-transfers', + + // Holiday management + 'manage-holidays', + 'manage-any-holidays', + 'view-holidays', + 'create-holidays', + 'edit-holidays', + 'delete-holidays', + + // Announcement management + 'manage-announcements', + 'manage-any-announcements', + 'view-announcements', + 'create-announcements', + 'edit-announcements', + 'delete-announcements', + + // Asset Type management + 'manage-asset-types', + 'manage-any-asset-types', + 'view-asset-types', + 'create-asset-types', + 'edit-asset-types', + 'delete-asset-types', + + // Asset management + 'manage-assets', + 'manage-any-assets', + 'view-assets', + 'create-assets', + 'edit-assets', + 'delete-assets', + 'assign-assets', + 'manage-asset-maintenance', + + // Training Type management + 'manage-training-types', + 'manage-any-training-types', + 'view-training-types', + 'create-training-types', + 'edit-training-types', + 'delete-training-types', + + // Training Program management + 'manage-training-programs', + 'manage-any-training-programs', + 'view-training-programs', + 'create-training-programs', + 'edit-training-programs', + 'delete-training-programs', + + // Training Session management + 'manage-training-sessions', + 'manage-any-training-sessions', + 'view-training-sessions', + 'create-training-sessions', + 'edit-training-sessions', + 'delete-training-sessions', + 'manage-attendance', + + // Employee Training management + 'manage-employee-trainings', + 'manage-any-employee-trainings', + 'view-employee-trainings', + 'create-employee-trainings', + 'edit-employee-trainings', + 'delete-employee-trainings', + 'assign-trainings', + 'manage-assessments', + 'record-assessment-results', + + // Performance Indicator Category management + 'manage-performance-indicator-categories', + 'manage-any-performance-indicator-categories', + 'view-performance-indicator-categories', + 'create-performance-indicator-categories', + 'edit-performance-indicator-categories', + 'delete-performance-indicator-categories', + + // Performance Indicators management + 'manage-performance-indicators', + 'manage-any-performance-indicators', + 'view-performance-indicators', + 'create-performance-indicators', + 'edit-performance-indicators', + 'delete-performance-indicators', + + // Goal Types + 'manage-goal-types', + 'manage-any-goal-types', + 'view-goal-types', + 'create-goal-types', + 'edit-goal-types', + 'delete-goal-types', + + // Employee Goals + 'manage-employee-goals', + 'manage-any-employee-goals', + 'view-employee-goals', + 'create-employee-goals', + 'edit-employee-goals', + 'delete-employee-goals', + + // Review Cycles + 'manage-review-cycles', + 'manage-any-review-cycles', + 'view-review-cycles', + 'create-review-cycles', + 'edit-review-cycles', + 'delete-review-cycles', + + // Employee Reviews + 'manage-employee-reviews', + 'manage-any-employee-reviews', + 'view-employee-reviews', + 'create-employee-reviews', + 'edit-employee-reviews', + 'delete-employee-reviews', + + // Job Categories + 'manage-job-categories', + 'manage-any-job-categories', + 'view-job-categories', + 'create-job-categories', + 'edit-job-categories', + 'delete-job-categories', + + // Job Requisitions + 'manage-job-requisitions', + 'manage-any-job-requisitions', + 'view-job-requisitions', + 'create-job-requisitions', + 'edit-job-requisitions', + 'delete-job-requisitions', + 'approve-job-requisitions', + + // Job Types + 'manage-job-types', + 'manage-any-job-types', + 'view-job-types', + 'create-job-types', + 'edit-job-types', + 'delete-job-types', + + // Job Locations + 'manage-job-locations', + 'manage-any-job-locations', + 'view-job-locations', + 'create-job-locations', + 'edit-job-locations', + 'delete-job-locations', + + // Job Postings + 'manage-job-postings', + 'manage-any-job-postings', + 'view-job-postings', + 'create-job-postings', + 'edit-job-postings', + 'delete-job-postings', + 'publish-job-postings', + + // Candidate Sources + 'manage-candidate-sources', + 'manage-any-candidate-sources', + 'view-candidate-sources', + 'create-candidate-sources', + 'edit-candidate-sources', + 'delete-candidate-sources', + + // Candidates + 'manage-candidates', + 'manage-any-candidates', + 'view-candidates', + // 'create-candidates', + 'edit-candidates', + 'delete-candidates', + + // Interview Types + 'manage-interview-types', + 'manage-any-interview-types', + 'view-interview-types', + 'create-interview-types', + 'edit-interview-types', + 'delete-interview-types', + + // Interview Rounds + 'manage-interview-rounds', + 'manage-any-interview-rounds', + 'view-interview-rounds', + 'create-interview-rounds', + 'edit-interview-rounds', + 'delete-interview-rounds', + + // Interviews + 'manage-interviews', + 'manage-any-interviews', + 'view-interviews', + 'create-interviews', + 'edit-interviews', + 'delete-interviews', + + // Interview Feedback + 'manage-interview-feedback', + 'manage-any-interview-feedback', + 'view-interview-feedback', + 'create-interview-feedback', + 'edit-interview-feedback', + 'delete-interview-feedback', + + // Candidate Assessments + 'manage-candidate-assessments', + 'manage-any-candidate-assessments', + 'view-candidate-assessments', + 'create-candidate-assessments', + 'edit-candidate-assessments', + 'delete-candidate-assessments', + + // Offer Templates + 'manage-offer-templates', + 'manage-any-offer-templates', + 'view-offer-templates', + 'create-offer-templates', + 'edit-offer-templates', + 'delete-offer-templates', + + // Offers + 'manage-offers', + 'manage-any-offers', + 'view-offers', + 'create-offers', + 'edit-offers', + 'delete-offers', + 'approve-offers', + + // Onboarding Checklists management + 'manage-onboarding-checklists', + 'manage-any-onboarding-checklists', + 'view-onboarding-checklists', + 'create-onboarding-checklists', + 'edit-onboarding-checklists', + 'delete-onboarding-checklists', + + // Checklist Items management + 'manage-checklist-items', + 'manage-any-checklist-items', + 'view-checklist-items', + 'create-checklist-items', + 'edit-checklist-items', + 'delete-checklist-items', + + // Candidate Onboarding management + 'manage-candidate-onboarding', + 'manage-any-candidate-onboarding', + 'view-candidate-onboarding', + 'create-candidate-onboarding', + 'edit-candidate-onboarding', + 'delete-candidate-onboarding', + + // Meeting Types management + 'manage-meeting-types', + 'manage-any-meeting-types', + 'view-meeting-types', + 'create-meeting-types', + 'edit-meeting-types', + 'delete-meeting-types', + + // Meeting Rooms management + 'manage-meeting-rooms', + 'manage-any-meeting-rooms', + 'view-meeting-rooms', + 'create-meeting-rooms', + 'edit-meeting-rooms', + 'delete-meeting-rooms', + + // Meetings management + 'manage-meetings', + 'manage-any-meetings', + 'view-meetings', + 'create-meetings', + 'edit-meetings', + 'delete-meetings', + 'manage-meeting-status', + + // Meeting Attendees management + 'manage-meeting-attendees', + 'manage-any-meeting-attendees', + 'view-meeting-attendees', + 'create-meeting-attendees', + 'edit-meeting-attendees', + 'delete-meeting-attendees', + 'manage-meeting-rsvp-status', + 'manage-meeting-attendance', + + // Meeting Minutes management + 'manage-meeting-minutes', + 'manage-any-meeting-minutes', + 'view-meeting-minutes', + 'create-meeting-minutes', + 'edit-meeting-minutes', + 'delete-meeting-minutes', + + // Action Items management + 'manage-action-items', + 'manage-any-action-items', + 'view-action-items', + 'create-action-items', + 'edit-action-items', + 'delete-action-items', + + // Contract Types management + 'manage-contract-types', + 'manage-any-contract-types', + 'view-contract-types', + 'create-contract-types', + 'edit-contract-types', + 'delete-contract-types', + + // Employee Contracts management + 'manage-employee-contracts', + 'manage-any-employee-contracts', + 'view-employee-contracts', + 'create-employee-contracts', + 'edit-employee-contracts', + 'delete-employee-contracts', + 'approve-employee-contracts', + 'reject-employee-contracts', + + // Contract Renewals management + 'manage-contract-renewals', + 'manage-any-contract-renewals', + 'view-contract-renewals', + 'create-contract-renewals', + 'edit-contract-renewals', + 'delete-contract-renewals', + 'approve-contract-renewals', + 'reject-contract-renewals', + + // Contract Templates management + 'manage-contract-templates', + 'manage-any-contract-templates', + 'view-contract-templates', + 'create-contract-templates', + 'edit-contract-templates', + 'delete-contract-templates', + + // Document Categories management + 'manage-document-categories', + 'manage-any-document-categories', + 'view-document-categories', + 'create-document-categories', + 'edit-document-categories', + 'delete-document-categories', + + // HR Documents management + 'manage-hr-documents', + 'manage-any-hr-documents', + 'view-hr-documents', + 'create-hr-documents', + 'edit-hr-documents', + 'delete-hr-documents', + + // Document Acknowledgments management + 'manage-document-acknowledgments', + 'manage-any-document-acknowledgments', + 'view-document-acknowledgments', + 'create-document-acknowledgments', + 'edit-document-acknowledgments', + 'delete-document-acknowledgments', + 'acknowledge-document-acknowledgments', + + // Document Templates management + 'manage-document-templates', + 'manage-any-document-templates', + 'view-document-templates', + 'create-document-templates', + 'edit-document-templates', + 'delete-document-templates', + + // Leave Types management + 'manage-leave-types', + 'manage-any-leave-types', + 'view-leave-types', + 'create-leave-types', + 'edit-leave-types', + 'delete-leave-types', + + // Leave Policies management + 'manage-leave-policies', + 'manage-any-leave-policies', + 'view-leave-policies', + 'create-leave-policies', + 'edit-leave-policies', + 'delete-leave-policies', + + // Leave Applications management + 'manage-leave-applications', + 'manage-any-leave-applications', + 'view-leave-applications', + 'create-leave-applications', + 'edit-leave-applications', + 'delete-leave-applications', + 'approve-leave-applications', + 'reject-leave-applications', + + // Leave Balances management + 'manage-leave-balances', + 'manage-any-leave-balances', + 'view-leave-balances', + 'create-leave-balances', + 'edit-leave-balances', + 'delete-leave-balances', + 'adjust-leave-balances', + + // Shifts management + 'manage-shifts', + 'manage-any-shifts', + 'view-shifts', + 'create-shifts', + 'edit-shifts', + 'delete-shifts', + + // Attendance Policies management + 'manage-attendance-policies', + 'manage-any-attendance-policies', + 'view-attendance-policies', + 'create-attendance-policies', + 'edit-attendance-policies', + 'delete-attendance-policies', + + // Attendance Records management + 'manage-attendance-records', + 'manage-any-attendance-records', + 'view-attendance-records', + 'create-attendance-records', + 'edit-attendance-records', + 'delete-attendance-records', + 'clock-in-out', + + // Attendance Regularizations management + 'manage-attendance-regularizations', + 'manage-any-attendance-regularizations', + 'view-attendance-regularizations', + 'create-attendance-regularizations', + 'edit-attendance-regularizations', + 'delete-attendance-regularizations', + 'approve-attendance-regularizations', + 'reject-attendance-regularizations', + + // Time Entries management + 'manage-time-entries', + 'manage-any-time-entries', + 'view-time-entries', + 'create-time-entries', + 'edit-time-entries', + 'delete-time-entries', + 'approve-time-entries', + 'reject-time-entries', + + // Salary Components management + 'manage-salary-components', + 'manage-any-salary-components', + 'view-salary-components', + 'create-salary-components', + 'edit-salary-components', + 'delete-salary-components', + + // Employee Salaries management + 'manage-employee-salaries', + 'manage-any-employee-salaries', + 'view-employee-salaries', + 'create-employee-salaries', + 'edit-employee-salaries', + 'delete-employee-salaries', + + // Payroll Runs management + 'manage-payroll-runs', + 'manage-any-payroll-runs', + 'view-payroll-runs', + 'create-payroll-runs', + 'edit-payroll-runs', + 'delete-payroll-runs', + 'process-payroll-runs', + + // Payslips management + 'manage-payslips', + 'manage-any-payslips', + 'view-payslips', + 'create-payslips', + 'download-payslips', + 'send-payslips', + ]; + } + + private function getHRPermissions(): array + { + return $this->getManagerPermissions(); + } +} diff --git a/app/Models/UserEmailTemplate.php b/app/Models/UserEmailTemplate.php new file mode 100644 index 000000000..56ae02411 --- /dev/null +++ b/app/Models/UserEmailTemplate.php @@ -0,0 +1,24 @@ + 'boolean', + ]; + + public function emailTemplate(): BelongsTo + { + return $this->belongsTo(EmailTemplate::class, 'template_id'); + } +} diff --git a/app/Models/Warning.php b/app/Models/Warning.php new file mode 100644 index 000000000..82a2c7ae2 --- /dev/null +++ b/app/Models/Warning.php @@ -0,0 +1,76 @@ + 'date', + 'acknowledgment_date' => 'date', + 'approved_at' => 'datetime', + 'expiry_date' => 'date', + 'has_improvement_plan' => 'boolean', + 'improvement_plan_start_date' => 'date', + 'improvement_plan_end_date' => 'date', + ]; + + /** + * Get the employee who received this warning. + */ + public function employee() + { + return $this->belongsTo(User::class, 'employee_id'); + } + + /** + * Get the user who issued this warning. + */ + public function issuer() + { + return $this->belongsTo(User::class, 'warning_by'); + } + + /** + * Get the user who approved this warning. + */ + public function approver() + { + return $this->belongsTo(User::class, 'approved_by'); + } + + /** + * Get the user who created this warning. + */ + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } +} \ No newline at end of file diff --git a/app/Models/Webhook.php b/app/Models/Webhook.php new file mode 100644 index 000000000..d7a199948 --- /dev/null +++ b/app/Models/Webhook.php @@ -0,0 +1,24 @@ +belongsTo(User::class); + } +} \ No newline at end of file diff --git a/app/Observers/PlanObserver.php b/app/Observers/PlanObserver.php new file mode 100644 index 000000000..ea3eeab77 --- /dev/null +++ b/app/Observers/PlanObserver.php @@ -0,0 +1,21 @@ +is_default) { + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php new file mode 100644 index 000000000..1b518bb2b --- /dev/null +++ b/app/Observers/UserObserver.php @@ -0,0 +1,47 @@ +type === 'company' && is_null($user->plan_id)) { + $defaultPlan = Plan::getDefaultPlan(); + if ($defaultPlan) { + $user->plan_id = $defaultPlan->id; + $user->plan_is_active = 1; + } + } + } + + /** + * Handle the User "created" event. + */ + public function created(User $user): void + { + // Generate a unique referral code only in SaaS mode + if (isSaas() && $user->type === 'company' && empty($user->referral_code)) { + do { + $code = rand(100000, 999999); + } while (User::where('referral_code', $code)->exists()); + + $user->referral_code = $code; + $user->save(); + } + + // Create default settings for new users + if (isSaas() && $user->type === 'superadmin') { + createDefaultSettings($user->id); + } elseif ($user->type === 'company') { + copySettingsFromSuperAdmin($user->id); + } + } +} \ No newline at end of file diff --git a/app/PathGenerators/MediaPathGenerator.php b/app/PathGenerators/MediaPathGenerator.php new file mode 100644 index 000000000..203b20a90 --- /dev/null +++ b/app/PathGenerators/MediaPathGenerator.php @@ -0,0 +1,24 @@ +model_id . '/'; + } + + public function getPathForConversions(Media $media): string + { + return 'media/' . $media->model_id . '/conversions/'; + } + + public function getPathForResponsiveImages(Media $media): string + { + return 'media/' . $media->model_id . '/responsive-images/'; + } +} \ No newline at end of file diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 000000000..87d87f5bb --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,43 @@ +app->singleton(\App\Services\WebhookService::class); + + // Register our AssetServiceProvider + $this->app->register(AssetServiceProvider::class); + } + + /** + * Bootstrap any application services. + */ + public function boot(): void + { + // Register the UserObserver + User::observe(UserObserver::class); + + // Register the PlanObserver + Plan::observe(PlanObserver::class); + + // Configure dynamic storage disks + try { + // \App\Services\DynamicStorageService::configureDynamicDisks(); + } catch (\Exception $e) { + // Silently fail during migrations or when database is not ready + } + } +} \ No newline at end of file diff --git a/app/Providers/AssetServiceProvider.php b/app/Providers/AssetServiceProvider.php new file mode 100644 index 000000000..83953051b --- /dev/null +++ b/app/Providers/AssetServiceProvider.php @@ -0,0 +1,34 @@ +"; + }); + + // Register a custom Blade directive for Vite assets + Blade::directive('dynamicVite', function ($expression) { + return ""; + }); + } +} \ No newline at end of file diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php new file mode 100644 index 000000000..b2e095b35 --- /dev/null +++ b/app/Providers/EventServiceProvider.php @@ -0,0 +1,37 @@ +> + */ + protected $listen = [ + UserCreated::class => [ + SendUserCreatedEmail::class, + ], + ]; + + /** + * Register any events for your application. + */ + public function boot(): void + { + // + } + + /** + * Determine if events and listeners should be automatically discovered. + */ + public function shouldDiscoverEvents(): bool + { + return false; + } +} \ No newline at end of file diff --git a/app/Services/DynamicStorageService.php b/app/Services/DynamicStorageService.php new file mode 100644 index 000000000..b9fee35ec --- /dev/null +++ b/app/Services/DynamicStorageService.php @@ -0,0 +1,128 @@ + $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + } + } + + // private static function configureS3Disk(array $s3Config): void + // { + // // For standard AWS S3, endpoint should be null + // $endpoint = null; + // if (!empty($s3Config['endpoint']) && !str_contains($s3Config['endpoint'], 'amazonaws.com')) { + // $endpoint = $s3Config['endpoint']; + // } + + // Config::set('filesystems.disks.s3', [ + // 'driver' => 's3', + // 'key' => $s3Config['key'], + // 'secret' => $s3Config['secret'], + // 'region' => $s3Config['region'], + // 'bucket' => $s3Config['bucket'], + // 'url' => $s3Config['url'] ?: null, + // 'endpoint' => $endpoint, + // 'use_path_style_endpoint' => false, + // 'visibility' => 'public', + // ]); + // } + + private static function configureS3Disk(array $s3Config): void + { + config( + [ + 'filesystems.disks.s3.key' => $s3Config['key'], + 'filesystems.disks.s3.secret' => $s3Config['secret'], + 'filesystems.disks.s3.region' => $s3Config['region'], + 'filesystems.disks.s3.bucket' => $s3Config['bucket'], + // 'filesystems.disks.s3.url' => $storage_settings['s3_url'], + // 'filesystems.disks.s3.endpoint' => $storage_settings['s3_endpoint'], + ] + ); + } + + private static function configureWasabiDisk(array $wasabiConfig): void + { + $region = $wasabiConfig['region'] ?: 'us-east-1'; + $endpoint = $wasabiConfig['url'] ?: ('https://s3.'.$region.'.wasabisys.com'); + + Config::set('filesystems.disks.wasabi', [ + 'driver' => 's3', + 'key' => $wasabiConfig['key'], + 'secret' => $wasabiConfig['secret'], + 'region' => $region, + 'bucket' => $wasabiConfig['bucket'], + 'endpoint' => $endpoint, + 'use_path_style_endpoint' => false, + 'visibility' => 'public', + ]); + } + + /** + * Get the active storage disk instance + */ + public static function getActiveDiskInstance() + { + $diskName = StorageConfigService::getActiveDisk(); + + // Ensure disk is configured + self::configureDynamicDisks(); + + try { + return Storage::disk($diskName); + } catch (\Exception $e) { + // Fallback to public disk + return Storage::disk('public'); + } + } + + /** + * Test storage connection + */ + public static function testConnection(string $diskName): bool + { + try { + self::configureDynamicDisks(); + $disk = Storage::disk($diskName); + + // Try to write and read a test file + $testContent = 'test-'.time(); + $testPath = 'test-connection.txt'; + + $disk->put($testPath, $testContent); + $retrieved = $disk->get($testPath); + $disk->delete($testPath); + + return $retrieved === $testContent; + } catch (\Exception $e) { + return false; + } + } +} diff --git a/app/Services/EmailTemplateService.php b/app/Services/EmailTemplateService.php new file mode 100644 index 000000000..a87e90bd3 --- /dev/null +++ b/app/Services/EmailTemplateService.php @@ -0,0 +1,167 @@ +first(); + + if (!$template) { + throw new Exception("Email template '{$templateName}' not found"); + } + + // Get user's language or default to 'en' + $language = 'en'; // default + if ($business && $business->user) { + $language = $business->user->lang ?? 'en'; + } + + // Get template content for the language + $templateLang = $template->emailTemplateLangs() + ->where('lang', $language) + ->first(); + + // Fallback to English if language not found + if (!$templateLang) { + $templateLang = $template->emailTemplateLangs() + ->where('lang', 'en') + ->first(); + } + + if (!$templateLang) { + throw new Exception("No content found for template '{$templateName}'"); + } + + // Replace variables in subject and content + $subject = $this->replaceVariables($templateLang->subject, $variables); + $content = $this->replaceVariables($templateLang->content, $variables); + $fromName = $this->replaceVariables($template->from, $variables); + + // Configure SMTP settings + $this->configureBusinessSMTP($business); + + // Get final email settings + $fromEmail = getSetting('email_from_address') ?: config('mail.from.address'); + $finalFromName = getSetting('email_from_name') ? $this->replaceVariables(getSetting('email_from_name'), $variables) : $fromName; + + // Send email + Mail::send([], [], function ($message) use ($subject, $content, $toEmail, $toName, $fromEmail, $finalFromName) { + $message->to($toEmail, $toName) + ->subject($subject) + ->html($content) + ->from($fromEmail, $finalFromName); + }); + + return true; + } catch (Exception $e) { + \Log::error('Email sending failed: ' . $e->getMessage()); + throw $e; + } + } + + private function replaceVariables(string $content, array $variables): string + { + return str_replace(array_keys($variables), array_values($variables), $content); + } + + public function sendTemplateEmailWithLanguage(string $templateName, array $variables, string $toEmail, string $toName = null, string $language = 'en') + { + try { + \Log::info('=== EMAIL TEMPLATE LANGUAGE DEBUG ===', [ + 'template_name' => $templateName, + 'requested_language' => $language, + 'to_email' => $toEmail + ]); + + // Get email template + $template = EmailTemplate::where('name', $templateName)->first(); + + if (!$template) { + throw new Exception("Email template '{$templateName}' not found"); + } + + // Get template content for the specified language + $templateLang = $template->emailTemplateLangs() + ->where('lang', $language) + ->first(); + + \Log::info('Template language lookup', [ + 'requested_lang' => $language, + 'found_template' => $templateLang ? true : false, + 'template_id' => $templateLang?->id ?? null + ]); + + // Fallback to English if language not found + if (!$templateLang) { + $templateLang = $template->emailTemplateLangs() + ->where('lang', 'en') + ->first(); + } + + if (!$templateLang) { + throw new Exception("No content found for template '{$templateName}'"); + } + + // Replace variables in subject and content + $subject = $this->replaceVariables($templateLang->subject, $variables); + $content = $this->replaceVariables($templateLang->content, $variables); + $fromName = $this->replaceVariables($template->from, $variables); + + // Configure SMTP settings + $this->configureBusinessSMTP(); + + // Get final email settings + $fromEmail = getSetting('email_from_address') ?: config('mail.from.address'); + $finalFromName = getSetting('email_from_name') ? $this->replaceVariables(getSetting('email_from_name'), $variables) : $fromName; + + // Send email + Mail::send([], [], function ($message) use ($subject, $content, $toEmail, $toName, $fromEmail, $finalFromName) { + $message->to($toEmail, $toName) + ->subject($subject) + ->html($content) + ->from($fromEmail, $finalFromName); + }); + + return true; + } catch (Exception $e) { + \Log::error('Email sending failed: ' . $e->getMessage()); + throw $e; + } + } + + private function configureBusinessSMTP(?Business $business = null) + { + // Get email settings from settings table + $emailDriver = getSetting('email_driver', 'smtp'); + $emailHost = getSetting('email_host'); + $emailUsername = getSetting('email_username'); + $emailPassword = getSetting('email_password'); + $emailPort = getSetting('email_port', 587); + $emailEncryption = getSetting('email_encryption', 'tls'); + + // Check if email settings are configured + if (!$emailHost || !$emailUsername || !$emailPassword) { + throw new Exception("Email settings not configured. Please configure email settings in system settings."); + } + + // Configure mail settings + Config::set([ + 'mail.default' => $emailDriver, + 'mail.mailers.smtp.host' => $emailHost, + 'mail.mailers.smtp.port' => $emailPort, + 'mail.mailers.smtp.username' => $emailUsername, + 'mail.mailers.smtp.password' => $emailPassword, + 'mail.mailers.smtp.encryption' => $emailEncryption, + ]); + } +} \ No newline at end of file diff --git a/app/Services/MailConfigService.php b/app/Services/MailConfigService.php new file mode 100644 index 000000000..f29635ef6 --- /dev/null +++ b/app/Services/MailConfigService.php @@ -0,0 +1,59 @@ +type == 'superadmin') { + $user = User::where('type', 'superadmin')->first(); + } else if ($user->type == 'company') { + $user = User::where('id', $user->created_by)->first(); + } else { + $user = User::where('id', $user->created_by)->first(); + } + } else { + if ($user->type == 'company') { + $user = Auth::user(); + } else { + $user = User::where('id', $user->created_by)->first(); + } + } + + $getSettings = settings($user->id); + + + $settings = [ + 'driver' => $getSettings['email_driver'] ?? 'smtp', + 'host' => $getSettings['email_host'] ?? 'smtp.example.com', + 'port' => $getSettings['email_port'] ?? '587', + 'username' => $getSettings['email_username'] ?? '', + 'password' => $getSettings['email_password'] ?? '', + 'encryption' => $getSettings['email_encryption'] ?? 'tls', + 'fromAddress' => $getSettings['email_from_address'] ?? 'noreply@example.com', + 'fromName' => $getSettings['email_from_name'] ?? 'WorkDo System' + ]; + + + Config::set([ + 'mail.default' => $settings['driver'], + 'mail.mailers.smtp.host' => $settings['host'], + 'mail.mailers.smtp.port' => $settings['port'], + 'mail.mailers.smtp.encryption' => $settings['encryption'] === 'none' ? null : $settings['encryption'], + 'mail.mailers.smtp.username' => $settings['username'], + 'mail.mailers.smtp.password' => $settings['password'], + 'mail.from.address' => $settings['fromAddress'], + 'mail.from.name' => $settings['fromName'], + ]); + } +} diff --git a/app/Services/StorageConfigService.php b/app/Services/StorageConfigService.php new file mode 100644 index 000000000..d0d2b1742 --- /dev/null +++ b/app/Services/StorageConfigService.php @@ -0,0 +1,204 @@ +type === 'superadmin') { + $userId = $user->id; + } else { + $userId = $user->created_by ?? null; + } + } else { + if ($user->type === 'company') { + $userId = $user->id; + } else { + $userId = $user->created_by ?? null; + } + } + + if (!$userId) { + return self::getDefaultConfig(); + } + + $cacheKey = 'active_storage_config_' . $userId; + // return Cache::remember($cacheKey, 300, function () use ($userId) { + return self::loadStorageConfigFromDB($userId); + // }); + } catch (\Exception $e) { + \Log::error('Error in getStorageConfig', ['error' => $e->getMessage()]); + return self::getDefaultConfig(); + } + } + + /** + * Clear storage configuration cache + */ + public static function clearCache(): void + { + Cache::forget('active_storage_config'); + Cache::forget('admin_settings'); + } + + /** + * Load storage configuration from database + */ + private static function loadStorageConfigFromDB($userId = null): array + { + try { + + if (!$userId) { + return self::getDefaultConfig(); + } + + $settings = DB::table('settings') + ->where('user_id', $userId) + ->whereIn('key', [ + 'storage_type', + 'storage_file_types', + 'storage_max_upload_size', + 'aws_access_key_id', + 'aws_secret_access_key', + 'aws_default_region', + 'aws_bucket', + 'aws_url', + 'aws_endpoint', + 'wasabi_access_key', + 'wasabi_secret_key', + 'wasabi_region', + 'wasabi_bucket', + 'wasabi_url', + 'wasabi_root' + ]) + ->pluck('value', 'key') + ->toArray(); + // Map storage_type to correct disk name + + if (isSaaS()) { + $superAdmin = User::where('type', 'superadmin')->first(); + if ($superAdmin) { + $superAdminSettings = DB::table('settings')->where('user_id', $superAdmin->id)->whereIn('key', [ + 'storage_file_types', + 'storage_max_upload_size' + ]) + ->pluck('value', 'key') + ->toArray(); + } + } else { + $superAdmin = User::where('type', 'company')->first(); + if ($superAdmin) { + $superAdminSettings = DB::table('settings')->where('user_id', $superAdmin->id)->whereIn('key', [ + 'storage_file_types', + 'storage_max_upload_size' + ]) + ->pluck('value', 'key') + ->toArray(); + } + } + + $storageType = $settings['storage_type'] ?? 'local'; + $diskName = match ($storageType) { + 'local' => 'public', + 'aws_s3' => 's3', + 'wasabi' => 'wasabi', + default => 'public' + }; + + return [ + 'disk' => $diskName, + 'allowed_file_types' => $superAdminSettings['storage_file_types'] ?? 'jpg,jpeg,png,webp,gif,pdf,doc,docx,csv,txt,zip,mp4,mp3', + 'max_file_size_mb' => (int)($superAdminSettings['storage_max_upload_size'] ?? 2), + 's3' => [ + 'key' => $settings['aws_access_key_id'] ?? '', + 'secret' => $settings['aws_secret_access_key'] ?? '', + 'bucket' => $settings['aws_bucket'] ?? '', + 'region' => $settings['aws_default_region'] ?? 'us-east-1', + 'url' => $settings['aws_url'] ?? '', + 'endpoint' => $settings['aws_endpoint'] ?? '', + ], + 'wasabi' => [ + 'key' => $settings['wasabi_access_key'] ?? '', + 'secret' => $settings['wasabi_secret_key'] ?? '', + 'bucket' => $settings['wasabi_bucket'] ?? '', + 'region' => $settings['wasabi_region'] ?? 'us-east-1', + 'url' => $settings['wasabi_url'] ?? '', + 'root' => $settings['wasabi_root'] ?? '', + ] + ]; + } catch (\Exception $e) { + \Log::error('Failed to load storage config from DB', ['error' => $e->getMessage()]); + return self::getDefaultConfig(); + } + } + + /** + * Get default storage configuration + */ + private static function getDefaultConfig(): array + { + return [ + 'disk' => 'public', + 'allowed_file_types' => 'jpg,png,webp,gif,png', + 'max_file_size_mb' => 2, + 's3' => [], + 'wasabi' => [] + ]; + } +} diff --git a/app/Services/UserService.php b/app/Services/UserService.php new file mode 100644 index 000000000..11971147b --- /dev/null +++ b/app/Services/UserService.php @@ -0,0 +1,57 @@ +type)) { + $user->type = 'company'; + $user->save(); + return true; + } + + return false; + } catch (\Exception $e) { + \Log::error('Failed to assign default role: ' . $e->getMessage()); + return false; + } + } + + /** + * Assign company role and permissions to user + * + * @param User $user + * @return bool + */ + public static function assignCompanyPermissions(User $user): bool + { + try { + // Get company role + $companyRole = Role::where('name', 'company')->first(); + + if ($companyRole) { + $user->assignRole($companyRole); + $user->type = 'company'; + $user->save(); + return true; + } + + return false; + } catch (\Exception $e) { + \Log::error('Failed to assign company role: ' . $e->getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/app/Services/WebhookService.php b/app/Services/WebhookService.php new file mode 100644 index 000000000..aaf07ba75 --- /dev/null +++ b/app/Services/WebhookService.php @@ -0,0 +1,59 @@ +webhookSetting($module, $userId); + + if ($webhook) { + $parameter = json_encode($data); + $status = $this->webhookCall($webhook['url'], $parameter, $webhook['method']); + } + } + + private function webhookSetting($module, $id) + { + $webhook = Webhook::where('module', $module)->where('user_id', $id)->first(); + + if (!empty($webhook)) { + $url = $webhook->url; + $method = $webhook->method; + $reference_url = "https://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]"; + + $data['method'] = $method; + $data['reference_url'] = $reference_url; + $data['url'] = $url; + return $data; + } + return false; + } + + private function webhookCall($url = null, $parameter = null, $method = 'POST') + { + if (!empty($url) && !empty($parameter)) { + try { + $curlHandle = curl_init($url); + curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $parameter); + curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true); + curl_setopt($curlHandle, CURLOPT_CUSTOMREQUEST, strtoupper($method)); + $curlResponse = curl_exec($curlHandle); + curl_close($curlHandle); + + if (empty($curlResponse)) { + return true; + } else { + return false; + } + } catch (\Throwable $th) { + return false; + } + } else { + return false; + } + } +} \ No newline at end of file diff --git a/app/Traits/AutoApplyPermissionCheck.php b/app/Traits/AutoApplyPermissionCheck.php new file mode 100644 index 000000000..becbd2816 --- /dev/null +++ b/app/Traits/AutoApplyPermissionCheck.php @@ -0,0 +1,224 @@ +check()) { + return $query; + } + + $user = auth()->user(); + + // Check if user is superadmin - they can see everything + if ($user->hasRole(['superadmin'])) { + return $query; + } + + // For company users, show their records and their employees' records + if ($user->hasRole(['company'])) { + if (Schema::hasColumn($query->getModel()->getTable(), 'created_by')) { + return $query->whereIn('created_by', getCompanyAndUsersId()); + } + } + + try { + // Check for specific permissions first (works for all roles) + // $module = str_replace('_', '-', $module); + // if ($user->hasPermissionTo("manage-own-{$module}")) { + // if (Schema::hasColumn($query->getModel()->getTable(), 'created_by')) { + // return $query->where('created_by', $user->id)->orWhere('employee_id', $user->id); + // } + // return $query; + // } + + $module = str_replace('_', '-', $module); + if ($user->hasPermissionTo("manage-own-{$module}")) { + $table = $query->getModel()->getTable(); + return $query->where(function ($q) use ($user, $table) { + if (Schema::hasColumn($table, 'created_by')) { + $q->where('created_by', $user->id); + } + if (Schema::hasColumn($table, 'employee_id')) { + // Use OR only if created_by already applied + $q->orWhere('employee_id', $user->id); + } + }); + } + + // If user has permission to list all items, return the query without filtering + if ($user->hasPermissionTo("manage-any-{$module}")) { + if (Schema::hasColumn($query->getModel()->getTable(), 'created_by')) { + return $query->whereIn('created_by', getCompanyAndUsersId()); + } + } + } catch (PermissionDoesNotExist $e) { + // Permission doesn't exist, check for access to module instead + if ($user->hasPermissionTo("access-{$module}-module")) { + // Default to showing only own records if they have module access + if (Schema::hasColumn($query->getModel()->getTable(), 'created_by')) { + return $query->where('created_by', $user->id); + } + return $query; + } + } + + // Check employee role after specific permissions + if ($user->hasRole(['employee'])) { + return $this->applyEmployeeRoleFiltering($query, $user, $permission = null, $module); + } + + // Check Default manage Permission + try { + if ($user->hasPermissionTo("manage-{$module}")) { + if (Schema::hasColumn($query->getModel()->getTable(), 'created_by')) { + return $query->whereIn('created_by', getCompanyAndUsersId()); + } + } + } catch (PermissionDoesNotExist $e) { + // Permission doesn't exist, check for access to module instead + if ($user->hasPermissionTo("access-{$module}-module")) { + // Default to showing only own records if they have module access + if (Schema::hasColumn($query->getModel()->getTable(), 'created_by')) { + return $query->where('created_by', $user->id); + } + return $query; + } + } + + return $query; + } + + + private function applyEmployeeRoleFiltering($query, $user, $permission = null, $module) + { + $module = str_replace('_', '-', $module); + + // Check if employee has manage permission + $hasManagePermission = false; + try { + $hasManagePermission = $user->hasPermissionTo("manage-{$module}"); + } catch (PermissionDoesNotExist $e) { + // Continue with model-specific filtering + } + + $modelClass = get_class($query->getModel()); + switch ($modelClass) { + case 'App\Models\User': + return $query->where('id', $user->id); + case 'App\Models\Award': + return $query->where('employee_id', $user->id); + case 'App\Models\Promotion': + return $query->where('employee_id', $user->id); + case 'App\Models\EmployeeGoal': + return $query->where('employee_id', $user->id); + case 'App\Models\Resignation': + return $query->where('employee_id', $user->id); + case 'App\Models\Termination': + return $query->where('employee_id', $user->id); + case 'App\Models\Warning': + return $query->where('employee_id', $user->id); + case 'App\Models\Trip': + return $query->where('employee_id', $user->id); + case 'App\Models\EmployeeTransfer': + return $query->where('employee_id', $user->id); + case 'App\Models\EmployeeReview': + return $query->where('employee_id', $user->id); + case 'App\Models\Resignation': + return $query->where('employee_id', $user->id); + case 'App\Models\Termination': + return $query->where('employee_id', $user->id); + case 'App\Models\Warning': + return $query->where('employee_id', $user->id); + case 'App\Models\Trip': + return $query->where('employee_id', $user->id); + case 'App\Models\Complaint': + return $query->where(function ($q) use ($user) { + $q->where('employee_id', $user->id) + ->orWhere('against_employee_id', $user->id); + }); + case 'App\Models\Asset': + return $query->join('asset_assignments', 'assets.id', '=', 'asset_assignments.asset_id') + ->where('asset_assignments.employee_id', $user->id) + ->select('assets.*'); + case 'App\Models\TrainingSession': + return $query->join('training_session_trainer', 'training_sessions.id', '=', 'training_session_trainer.training_session_id') + ->where('training_session_trainer.employee_id', $user->id) + ->select('training_sessions.*'); + case 'App\Models\EmployeeTraining': + return $query->where('employee_id', $user->id); + case 'App\Models\Interview': + return $query->whereJsonContains('interviewers', (string) $user->id); + case 'App\Models\CandidateAssessment': + return $query->where('conducted_by', $user->id); + case 'App\Models\CandidateOnboarding': + return $query->where('buddy_employee_id', $user->id); + case 'App\Models\EmployeeContract': + return $query->where('employee_id', $user->id); + case 'App\Models\ContractAmendment': + return $query->join('employee_contracts', 'employee_contracts.id', '=', 'contract_amendments.contract_id') + ->where('employee_contracts.employee_id', $user->id) + ->select('contract_amendments.*'); + case 'App\Models\ContractRenewal': + return $query->join('employee_contracts', 'employee_contracts.id', '=', 'contract_renewals.contract_id') + ->where('employee_contracts.employee_id', $user->id) + ->select('contract_renewals.*'); + case 'App\Models\HrDocument': + return $query->whereIn('created_by', getCompanyAndUsersId()); + case 'App\Models\DocumentAcknowledgment': + return $query->where('user_id', $user->id); + case 'App\Models\Meeting': + return $query->where('organizer_id', $user->id); + case 'App\Models\MeetingAttendee': + return $query->where('user_id', $user->id); + case 'App\Models\MeetingMinute': + return $query->where('recorded_by', $user->id); + case 'App\Models\ActionItem': + return $query->where('assigned_to', $user->id); + case 'App\Models\LeaveBalance': + return $query->where('employee_id', $user->id); + case 'App\Models\LeaveApplication': + return $query->where('employee_id', $user->id); + case 'App\Models\AttendanceRecord': + return $query->where('employee_id', $user->id); + case 'App\Models\AttendanceRegularization': + return $query->where('employee_id', $user->id); + case 'App\Models\TimeEntry': + return $query->where('employee_id', $user->id); + case 'App\Models\EmployeeSalary': + return $query->where('employee_id', $user->id); + case 'App\Models\Payslip': + return $query->where('employee_id', $user->id); + default: + if ($hasManagePermission && Schema::hasColumn($query->getModel()->getTable(), 'created_by')) { + return $query->whereIn('created_by', getCompanyAndUsersId()); + } + return $query; + } + } +} diff --git a/artisan b/artisan new file mode 100755 index 000000000..c35e31d6a --- /dev/null +++ b/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 000000000..f5599e72c --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,65 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + $middleware->encryptCookies(except: ['appearance']); + + $middleware->web(append: [ + CheckInstallation::class, + HandleAppearance::class, + ShareGlobalSettings::class, + HandleInertiaRequests::class, + AddLinkHeadersForPreloadedAssets::class, + DemoModeMiddleware::class, + ]); + + $middleware->alias([ + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + 'landing.enabled' => \App\Http\Middleware\CheckLandingPageEnabled::class, + 'verified' => App\Http\Middleware\EnsureEmailIsVerified::class, + 'plan.access' => \App\Http\Middleware\CheckPlanAccess::class, + 'setting' => \App\Http\Middleware\SettingMiddleware::class, + 'checksaas' => \App\Http\Middleware\CheckSaas::class, + 'career.shared' => \App\Http\Middleware\CareerSharedDataMiddleware::class, + ]); + + $middleware->validateCsrfTokens( + except: [ + 'install/*', + 'update/*', + 'cashfree/create-session', + 'cashfree/webhook', + 'ozow/create-payment', + 'payments/easebuzz/success', + 'payments/aamarpay/success', + 'payments/aamarpay/callback', + 'payments/tap/success', + 'payments/tap/callback', + 'payments/benefit/success', + 'payments/benefit/callback', + 'payments/paytabs/callback', + 'api/media/batch', + ], + ); + + }) + ->withExceptions(function (Exceptions $exceptions) { + // + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100755 index 000000000..d6b7ef32c --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 000000000..9fa93bd60 --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,7 @@ +=5.6" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "proprietary" + ], + "description": "Official PHP SDK for Authorize.Net", + "homepage": "http://developer.authorize.net", + "keywords": [ + "authorize.net", + "authorizenet", + "ecommerce", + "payment" + ], + "support": { + "issues": "https://github.com/AuthorizeNet/sdk-php/issues", + "source": "https://github.com/AuthorizeNet/sdk-php/tree/2.0.4" + }, + "time": "2024-09-18T06:23:52+00:00" + }, + { + "name": "aws/aws-crt-php", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/awslabs/aws-crt-php.git", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/awslabs/aws-crt-php/zipball/d71d9906c7bb63a28295447ba12e74723bd3730e", + "reference": "d71d9906c7bb63a28295447ba12e74723bd3730e", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5", + "yoast/phpunit-polyfills": "^1.0" + }, + "suggest": { + "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality." + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "AWS SDK Common Runtime Team", + "email": "aws-sdk-common-runtime@amazon.com" + } + ], + "description": "AWS Common Runtime for PHP", + "homepage": "https://github.com/awslabs/aws-crt-php", + "keywords": [ + "amazon", + "aws", + "crt", + "sdk" + ], + "support": { + "issues": "https://github.com/awslabs/aws-crt-php/issues", + "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7" + }, + "time": "2024-10-18T22:15:13+00:00" + }, + { + "name": "aws/aws-sdk-php", + "version": "3.369.2", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "5e3f541e344d71f3b9591fe1d94d9576530fa795" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5e3f541e344d71f3b9591fe1d94d9576530fa795", + "reference": "5e3f541e344d71f3b9591fe1d94d9576530fa795", + "shasum": "" + }, + "require": { + "aws/aws-crt-php": "^1.2.3", + "ext-json": "*", + "ext-pcre": "*", + "ext-simplexml": "*", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/promises": "^2.0", + "guzzlehttp/psr7": "^2.4.5", + "mtdowling/jmespath.php": "^2.8.0", + "php": ">=8.1", + "psr/http-message": "^1.0 || ^2.0", + "symfony/filesystem": "^v6.4.3 || ^v7.1.0 || ^v8.0.0" + }, + "require-dev": { + "andrewsville/php-token-reflection": "^1.4", + "aws/aws-php-sns-message-validator": "~1.0", + "behat/behat": "~3.0", + "composer/composer": "^2.7.8", + "dms/phpunit-arraysubset-asserts": "^0.4.0", + "doctrine/cache": "~1.4", + "ext-dom": "*", + "ext-openssl": "*", + "ext-sockets": "*", + "phpunit/phpunit": "^9.6", + "psr/cache": "^2.0 || ^3.0", + "psr/simple-cache": "^2.0 || ^3.0", + "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0", + "yoast/phpunit-polyfills": "^2.0" + }, + "suggest": { + "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications", + "doctrine/cache": "To use the DoctrineCacheAdapter", + "ext-curl": "To send requests using cURL", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "ext-pcntl": "To use client-side monitoring", + "ext-sockets": "To use client-side monitoring" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Aws\\": "src/" + }, + "exclude-from-classmap": [ + "src/data/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", + "keywords": [ + "amazon", + "aws", + "cloud", + "dynamodb", + "ec2", + "glacier", + "s3", + "sdk" + ], + "support": { + "forum": "https://github.com/aws/aws-sdk-php/discussions", + "issues": "https://github.com/aws/aws-sdk-php/issues", + "source": "https://github.com/aws/aws-sdk-php/tree/3.369.2" + }, + "time": "2025-12-23T19:21:43+00:00" + }, + { + "name": "bacon/bacon-qr-code", + "version": "2.0.8", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/8674e51bb65af933a5ffaf1c308a660387c35c22", + "reference": "8674e51bb65af933a5ffaf1c308a660387c35c22", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.1", + "phpunit/phpunit": "^7 | ^8 | ^9", + "spatie/phpunit-snapshot-assertions": "^4.2.9", + "squizlabs/php_codesniffer": "^3.4" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "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": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/2.0.8" + }, + "time": "2022-12-07T17:46:57+00:00" + }, + { + "name": "barryvdh/laravel-dompdf", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "reference": "8e71b99fc53bb8eb77f316c3c452dd74ab7cb25d", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^3.0", + "illuminate/support": "^9|^10|^11|^12", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.7|^3.0", + "orchestra/testbench": "^7|^8|^9|^10", + "phpro/grumphp": "^2.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf", + "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf" + }, + "providers": [ + "Barryvdh\\DomPDF\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\DomPDF\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "A DOMPDF Wrapper for Laravel", + "keywords": [ + "dompdf", + "laravel", + "pdf" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-dompdf/issues", + "source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-02-13T15:07:54+00:00" + }, + { + "name": "brick/math", + "version": "0.13.1", + "source": { + "type": "git", + "url": "https://github.com/brick/math.git", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/brick/math/zipball/fc7ed316430118cc7836bf45faff18d5dfc8de04", + "reference": "fc7ed316430118cc7836bf45faff18d5dfc8de04", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.2", + "phpunit/phpunit": "^10.1", + "vimeo/psalm": "6.8.8" + }, + "type": "library", + "autoload": { + "psr-4": { + "Brick\\Math\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Arbitrary-precision arithmetic library", + "keywords": [ + "Arbitrary-precision", + "BigInteger", + "BigRational", + "arithmetic", + "bigdecimal", + "bignum", + "bignumber", + "brick", + "decimal", + "integer", + "math", + "mathematics", + "rational" + ], + "support": { + "issues": "https://github.com/brick/math/issues", + "source": "https://github.com/brick/math/tree/0.13.1" + }, + "funding": [ + { + "url": "https://github.com/BenMorel", + "type": "github" + } + ], + "time": "2025-03-29T13:50:30+00:00" + }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=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": "cashfree/cashfree-pg", + "version": "5.0.3", + "source": { + "type": "git", + "url": "https://github.com/cashfree/cashfree-pg-sdk-php.git", + "reference": "94e8548bfae59ed7a4c84801b060ea8bb1974094" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/cashfree/cashfree-pg-sdk-php/zipball/94e8548bfae59ed7a4c84801b060ea8bb1974094", + "reference": "94e8548bfae59ed7a4c84801b060ea8bb1974094", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/guzzle": "^7.3", + "guzzlehttp/psr7": "^1.7 || ^2.0", + "php": "^7.2 || ^8.0", + "sentry/sdk": "^3.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.5", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Cashfree\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Cashfree Payments", + "homepage": "https://www.cashfree.com" + } + ], + "description": "Cashfree's Payment Gateway APIs provide developers with a streamlined pathway to integrate advanced payment processing capabilities into their applications, platforms and websites.", + "homepage": "https://www.cashfree.com", + "keywords": [ + "api", + "cashfree", + "payment gateway", + "php", + "rest", + "sdk" + ], + "support": { + "email": "care@cashfree.com", + "issues": "https://github.com/cashfree/cashfree-pg-sdk-php/issues", + "source": "https://github.com/cashfree/cashfree-pg-sdk-php" + }, + "time": "2025-04-04T11:10:45+00:00" + }, + { + "name": "clue/stream-filter", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/clue/stream-filter.git", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/clue/stream-filter/zipball/049509fef80032cb3f051595029ab75b49a3c2f7", + "reference": "049509fef80032cb3f051595029ab75b49a3c2f7", + "shasum": "" + }, + "require": { + "php": ">=5.3" + }, + "require-dev": { + "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "Clue\\StreamFilter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Christian Lück", + "email": "christian@clue.engineering" + } + ], + "description": "A simple and modern approach to stream filtering in PHP", + "homepage": "https://github.com/clue/stream-filter", + "keywords": [ + "bucket brigade", + "callback", + "filter", + "php_user_filter", + "stream", + "stream_filter_append", + "stream_filter_register" + ], + "support": { + "issues": "https://github.com/clue/stream-filter/issues", + "source": "https://github.com/clue/stream-filter/tree/v1.7.0" + }, + "funding": [ + { + "url": "https://clue.engineering/support", + "type": "custom" + }, + { + "url": "https://github.com/clue", + "type": "github" + } + ], + "time": "2023-12-20T15:40:13+00:00" + }, + { + "name": "coingate/coingate-php", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/coingate/coingate-php.git", + "reference": "e6758806364f00a75097ac7a0aee97c03d462085" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/coingate/coingate-php/zipball/e6758806364f00a75097ac7a0aee97c03d462085", + "reference": "e6758806364f00a75097ac7a0aee97c03d462085", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "php": ">=7.3.0" + }, + "require-dev": { + "phpstan/phpstan": "~1.4.10", + "phpunit/phpunit": "^5.7 || ^9.0", + "squizlabs/php_codesniffer": "~3.6.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "CoinGate\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "CoinGate and contributors", + "homepage": "https://github.com/coingate/coingate-php/graphs/contributors" + }, + { + "name": "Linas Pašviestis", + "email": "linas.pasviestis@gmail.com" + } + ], + "description": "CoinGate library for PHP", + "homepage": "https://coingate.com", + "keywords": [ + "altcoin", + "bitcoin", + "coingate", + "gateway", + "litecoin", + "merchant", + "payment" + ], + "support": { + "issues": "https://github.com/coingate/coingate-php/issues", + "source": "https://github.com/coingate/coingate-php/tree/v4.1.0" + }, + "time": "2022-05-20T10:19:03+00:00" + }, + { + "name": "composer/ca-bundle", + "version": "1.5.7", + "source": { + "type": "git", + "url": "https://github.com/composer/ca-bundle.git", + "reference": "d665d22c417056996c59019579f1967dfe5c1e82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/ca-bundle/zipball/d665d22c417056996c59019579f1967dfe5c1e82", + "reference": "d665d22c417056996c59019579f1967dfe5c1e82", + "shasum": "" + }, + "require": { + "ext-openssl": "*", + "ext-pcre": "*", + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^8 || ^9", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "symfony/process": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\CaBundle\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Lets you find a path to the system CA bundle, and includes a fallback to the Mozilla CA bundle.", + "keywords": [ + "cabundle", + "cacert", + "certificate", + "ssl", + "tls" + ], + "support": { + "irc": "irc://irc.freenode.org/composer", + "issues": "https://github.com/composer/ca-bundle/issues", + "source": "https://github.com/composer/ca-bundle/tree/1.5.7" + }, + "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": "2025-05-26T15:08:54+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.3", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "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.3" + }, + "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-09-19T14:15:21+00:00" + }, + { + "name": "dasprid/enum", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "reference": "8dfd07c6d2cf31c8da90c53b83c026c7696dda90", + "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.6" + }, + "time": "2024-08-09T14:30:48+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/annotations", + "version": "1.14.4", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/253dca476f70808a5aeed3a47cc2cc88c5cab915", + "reference": "253dca476f70808a5aeed3a47cc2cc88c5cab915", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^1 || ^2", + "ext-tokenizer": "*", + "php": "^7.1 || ^8.0", + "psr/cache": "^1 || ^2 || ^3" + }, + "require-dev": { + "doctrine/cache": "^1.11 || ^2.0", + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "~1.4.10 || ^1.10.28", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", + "symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7", + "vimeo/psalm": "^4.30 || ^5.14" + }, + "suggest": { + "php": "PHP 8.0 or higher comes with attributes, a native replacement for annotations" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "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": "Docblock Annotations Parser", + "homepage": "https://www.doctrine-project.org/projects/annotations.html", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "support": { + "issues": "https://github.com/doctrine/annotations/issues", + "source": "https://github.com/doctrine/annotations/tree/1.14.4" + }, + "time": "2024-09-05T10:15:52+00:00" + }, + { + "name": "doctrine/common", + "version": "3.5.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/common.git", + "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/common/zipball/d9ea4a54ca2586db781f0265d36bea731ac66ec5", + "reference": "d9ea4a54ca2586db781f0265d36bea731ac66ec5", + "shasum": "" + }, + "require": { + "doctrine/persistence": "^2.0 || ^3.0 || ^4.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9.0 || ^10.0", + "doctrine/collections": "^1", + "phpstan/phpstan": "^1.4.1", + "phpstan/phpstan-phpunit": "^1", + "phpunit/phpunit": "^7.5.20 || ^8.5 || ^9.0", + "squizlabs/php_codesniffer": "^3.0", + "symfony/phpunit-bridge": "^6.1", + "vimeo/psalm": "^4.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "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" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "PHP Doctrine Common project is a library that provides additional functionality that other Doctrine projects depend on such as better reflection support, proxies and much more.", + "homepage": "https://www.doctrine-project.org/projects/common.html", + "keywords": [ + "common", + "doctrine", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/common/issues", + "source": "https://github.com/doctrine/common/tree/3.5.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%2Fcommon", + "type": "tidelift" + } + ], + "time": "2025-01-01T22:12:03+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "doctrine/event-manager", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/event-manager.git", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/event-manager/zipball/b680156fa328f1dfd874fd48c7026c41570b9c6e", + "reference": "b680156fa328f1dfd874fd48c7026c41570b9c6e", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/common": "<2.9" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^10.5", + "vimeo/psalm": "^5.24" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\": "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" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Event Manager is a simple PHP event system that was built to be used with the various Doctrine projects.", + "homepage": "https://www.doctrine-project.org/projects/event-manager.html", + "keywords": [ + "event", + "event dispatcher", + "event manager", + "event system", + "events" + ], + "support": { + "issues": "https://github.com/doctrine/event-manager/issues", + "source": "https://github.com/doctrine/event-manager/tree/2.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%2Fevent-manager", + "type": "tidelift" + } + ], + "time": "2024-05-22T20:47:39+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.0.10", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^11.0", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpstan/phpstan-strict-rules": "^1.3", + "phpunit/phpunit": "^8.5 || ^9.5", + "vimeo/psalm": "^4.25 || ^5.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + } + }, + "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.0.10" + }, + "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": "2024-02-18T20:23:39+00:00" + }, + { + "name": "doctrine/lexer", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", + "reference": "861c870e8b75f7c8f69c146c7f89cc1c0f1b49b6", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12", + "phpstan/phpstan": "^1.3", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^4.11 || ^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/2.1.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:35:39+00:00" + }, + { + "name": "doctrine/persistence", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/persistence.git", + "reference": "45004aca79189474f113cbe3a53847c2115a55fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/persistence/zipball/45004aca79189474f113cbe3a53847c2115a55fa", + "reference": "45004aca79189474f113cbe3a53847c2115a55fa", + "shasum": "" + }, + "require": { + "doctrine/event-manager": "^1 || ^2", + "php": "^8.1", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "conflict": { + "doctrine/common": "<2.10" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "1.12.7", + "phpstan/phpstan-phpunit": "^1", + "phpstan/phpstan-strict-rules": "^1.1", + "phpunit/phpunit": "^9.6", + "symfony/cache": "^4.4 || ^5.4 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Persistence\\": "src/Persistence" + } + }, + "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" + }, + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com" + } + ], + "description": "The Doctrine Persistence project is a set of shared interfaces and functionality that the different Doctrine object mappers share.", + "homepage": "https://www.doctrine-project.org/projects/persistence.html", + "keywords": [ + "mapper", + "object", + "odm", + "orm", + "persistence" + ], + "support": { + "issues": "https://github.com/doctrine/persistence/issues", + "source": "https://github.com/doctrine/persistence/tree/4.0.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%2Fpersistence", + "type": "tidelift" + } + ], + "time": "2024-11-01T21:49:07+00:00" + }, + { + "name": "dompdf/dompdf", + "version": "v3.1.0", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "a51bd7a063a65499446919286fb18b518177155a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/a51bd7a063a65499446919286fb18b518177155a", + "reference": "a51bd7a063a65499446919286fb18b518177155a", + "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.0" + }, + "time": "2025-01-15T14:09:04+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": "fedapay/fedapay-php", + "version": "0.4.7", + "source": { + "type": "git", + "url": "https://github.com/fedapay/fedapay-php.git", + "reference": "150c196ae7778b10ab04dc82cd882cf1a3c332ce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fedapay/fedapay-php/zipball/150c196ae7778b10ab04dc82cd882cf1a3c332ce", + "reference": "150c196ae7778b10ab04dc82cd882cf1a3c332ce", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.5" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.7 || ^9.0", + "squizlabs/php_codesniffer": "^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "FedaPay\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "FedaPay and contributors", + "homepage": "https://github.com/fedapay/fedapay-php/contributors" + } + ], + "description": "PHP library for FedaPay https://fedapay.com", + "homepage": "https://fedapay.com/", + "keywords": [ + "api", + "fedapay", + "payment processing" + ], + "support": { + "issues": "https://github.com/fedapay/fedapay-php/issues", + "source": "https://github.com/fedapay/fedapay-php/tree/0.4.7" + }, + "time": "2025-05-17T08:05:46+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.9.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "reference": "7b2f29fe81dc4da0ca0ea7d42107a0845946ea77", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.5.3 || ^2.0.3", + "guzzlehttp/psr7": "^2.7.0", + "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.9.3" + }, + "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-03-27T13:37:11+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/7c69f28996b0a6920945dd20b3857e499d9ca96c", + "reference": "7c69f28996b0a6920945dd20b3857e499d9ca96c", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20" + }, + "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.2.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-03-27T13:27:01+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.7.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "reference": "c2270caaabe631b3b44c85f99e5a04bbb8060d16", + "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.39 || ^9.6.20" + }, + "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.7.1" + }, + "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-03-27T12:30:47+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/30e286560c137526eccd4ce21b2de477ab0676d2", + "reference": "30e286560c137526eccd4ce21b2de477ab0676d2", + "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.36 || ^9.6.15", + "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.4" + }, + "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-02-03T10:55:03+00:00" + }, + { + "name": "http-interop/http-factory-guzzle", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/http-interop/http-factory-guzzle.git", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/http-interop/http-factory-guzzle/zipball/8f06e92b95405216b237521cc64c804dd44c4a81", + "reference": "8f06e92b95405216b237521cc64c804dd44c4a81", + "shasum": "" + }, + "require": { + "guzzlehttp/psr7": "^1.7||^2.0", + "php": ">=7.3", + "psr/http-factory": "^1.0" + }, + "provide": { + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^9.5" + }, + "suggest": { + "guzzlehttp/psr7": "Includes an HTTP factory starting in version 2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Factory\\Guzzle\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "An HTTP Factory using Guzzle PSR7", + "keywords": [ + "factory", + "http", + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/http-interop/http-factory-guzzle/issues", + "source": "https://github.com/http-interop/http-factory-guzzle/tree/1.2.0" + }, + "time": "2021-07-21T13:50:14+00:00" + }, + { + "name": "inertiajs/inertia-laravel", + "version": "v2.0.3", + "source": { + "type": "git", + "url": "https://github.com/inertiajs/inertia-laravel.git", + "reference": "b732a5cc33423b2c2366fea38b17dc637d2a0b4f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/inertiajs/inertia-laravel/zipball/b732a5cc33423b2c2366fea38b17dc637d2a0b4f", + "reference": "b732a5cc33423b2c2366fea38b17dc637d2a0b4f", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^10.0|^11.0|^12.0", + "php": "^8.1.0", + "symfony/console": "^6.2|^7.0" + }, + "require-dev": { + "laravel/pint": "^1.16", + "mockery/mockery": "^1.3.3", + "orchestra/testbench": "^8.0|^9.2|^10.0", + "phpunit/phpunit": "^10.4|^11.5", + "roave/security-advisories": "dev-master" + }, + "suggest": { + "ext-pcntl": "Recommended when running the Inertia SSR server via the `inertia:start-ssr` artisan command." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Inertia\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "./helpers.php" + ], + "psr-4": { + "Inertia\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonathan Reinink", + "email": "jonathan@reinink.ca", + "homepage": "https://reinink.ca" + } + ], + "description": "The Laravel adapter for Inertia.js.", + "keywords": [ + "inertia", + "laravel" + ], + "support": { + "issues": "https://github.com/inertiajs/inertia-laravel/issues", + "source": "https://github.com/inertiajs/inertia-laravel/tree/v2.0.3" + }, + "time": "2025-06-20T07:38:21+00:00" + }, + { + "name": "iyzico/iyzipay-php", + "version": "v2.0.59", + "source": { + "type": "git", + "url": "https://github.com/iyzico/iyzipay-php.git", + "reference": "eccfa97465d78143dbe112886baeaf863619c074" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/iyzico/iyzipay-php/zipball/eccfa97465d78143dbe112886baeaf863619c074", + "reference": "eccfa97465d78143dbe112886baeaf863619c074", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=7.4.0" + }, + "require-dev": { + "phpunit/phpunit": "~9.6", + "satooshi/php-coveralls": "~0.6.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Iyzipay\\": "src/Iyzipay/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "iyzico and contributors", + "homepage": "https://github.com/iyzico/iyzipay-php/contributors" + } + ], + "description": "iyzipay api php client", + "homepage": "https://www.iyzico.com", + "keywords": [ + "iyzico", + "iyzico.com", + "iyzipay", + "iyzipay api", + "iyzipay api php", + "iyzipay api php client", + "iyzipay php", + "payment processing" + ], + "support": { + "issues": "https://github.com/iyzico/iyzipay-php/issues", + "source": "https://github.com/iyzico/iyzipay-php/tree/v2.0.59" + }, + "time": "2025-05-05T14:25:39+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+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": "larabug/larabug", + "version": "3.3", + "source": { + "type": "git", + "url": "https://github.com/LaraBug/LaraBug.git", + "reference": "e9dba4d38166372f3772b57f39296eb502c598d1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/LaraBug/LaraBug/zipball/e9dba4d38166372f3772b57f39296eb502c598d1", + "reference": "e9dba4d38166372f3772b57f39296eb502c598d1", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "^6.0.2 || ^7.0", + "illuminate/support": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0", + "nesbot/carbon": "^2.62.1 || ^3.0", + "php": "^7.4 || ^8.0 || ^8.2 || ^8.3 || ^8.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.4", + "mockery/mockery": "^1.3.3 || ^1.4.2", + "orchestra/testbench": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0", + "phpunit/phpunit": "^8.5.23 || ^9.5.12 || ^10.0.9 || ^11.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "LaraBug\\ServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "LaraBug\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nathan Geerinck", + "email": "nathan@intilli.be", + "role": "Owner" + } + ], + "description": "Laravel 6.x/7.x/8.x/9.x/10.x/11.x bug notifier", + "keywords": [ + "error", + "laravel", + "log" + ], + "support": { + "issues": "https://github.com/LaraBug/LaraBug/issues", + "source": "https://github.com/LaraBug/LaraBug/tree/3.3" + }, + "time": "2025-03-01T16:12:03+00:00" + }, + { + "name": "laravel/framework", + "version": "v12.20.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/1b9a00f8caf5503c92aa436279172beae1a484ff", + "reference": "1b9a00f8caf5503c92aa436279172beae1a484ff", + "shasum": "" + }, + "require": { + "brick/math": "^0.11|^0.12|^0.13", + "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", + "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.31", + "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/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", + "orchestra/testbench-core": "^10.0.0", + "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 use the eloquent factory builder (^1.9.1).", + "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-07-08T15:02:21+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/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.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "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" + }, + "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-05-05T12:20:28+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-aws-s3-v3", + "version": "3.30.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git", + "reference": "d286e896083bed3190574b8b088b557b59eb66f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/d286e896083bed3190574b8b088b557b59eb66f5", + "reference": "d286e896083bed3190574b8b088b557b59eb66f5", + "shasum": "" + }, + "require": { + "aws/aws-sdk-php": "^3.295.10", + "league/flysystem": "^3.10.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\AwsS3V3\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "AWS S3 filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "aws", + "file", + "files", + "filesystem", + "s3", + "storage" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/3.30.1" + }, + "time": "2025-10-20T15:27:33+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": "maennchen/zipstream-php", + "version": "3.1.2", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.2" + }, + "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": "^11.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.1.2" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-01-27T12:07:53+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": "mercadopago/dx-php", + "version": "2.6.2", + "source": { + "type": "git", + "url": "https://github.com/mercadopago/sdk-php.git", + "reference": "f5f97bd96dfcb3bafdfba634b3bc757025238caa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mercadopago/sdk-php/zipball/f5f97bd96dfcb3bafdfba634b3bc757025238caa", + "reference": "f5f97bd96dfcb3bafdfba634b3bc757025238caa", + "shasum": "" + }, + "require": { + "doctrine/annotations": "^1.8", + "doctrine/common": "^2.6 || ^3.0", + "php": ">=7.1.0" + }, + "require-dev": { + "doctrine/orm": "^2.3", + "phpmd/phpmd": "@stable", + "phpunit/phpunit": "^7", + "sebastian/phpcpd": "*", + "squizlabs/php_codesniffer": "2.8.1", + "symfony/yaml": "~2.5", + "vlucas/phpdotenv": "^2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "MercadoPago\\": [ + "src/MercadoPago/", + "tests/", + "src/MercadoPago/Generic/", + "src/MercadoPago/Entities/", + "src/MercadoPago/Entities/Shared/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Mercado Pago PHP SDK", + "homepage": "https://github.com/mercadopago/sdk-php", + "support": { + "source": "https://github.com/mercadopago/sdk-php/tree/2.6.2" + }, + "time": "2024-02-05T19:41:32+00:00" + }, + { + "name": "mollie/mollie-api-php", + "version": "v3.1.4", + "source": { + "type": "git", + "url": "https://github.com/mollie/mollie-api-php.git", + "reference": "5c5fe183fae988f49947ad9d6c6d019a9d3f7831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mollie/mollie-api-php/zipball/5c5fe183fae988f49947ad9d6c6d019a9d3f7831", + "reference": "5c5fe183fae988f49947ad9d6c6d019a9d3f7831", + "shasum": "" + }, + "require": { + "composer/ca-bundle": "^1.4", + "ext-curl": "*", + "ext-json": "*", + "ext-openssl": "*", + "nyholm/psr7": "^1.8", + "php": "^7.4|^8.0", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.1", + "psr/http-message": "^1.1|^2.0" + }, + "require-dev": { + "brianium/paratest": "^6.11", + "guzzlehttp/guzzle": "^7.6", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/var-dumper": "^5.4|^6.4|^7.2" + }, + "suggest": { + "mollie/oauth2-mollie-php": "Use OAuth to authenticate with the Mollie API. This is needed for some endpoints. Visit https://docs.mollie.com/ for more information." + }, + "type": "library", + "autoload": { + "psr-4": { + "Mollie\\Api\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Mollie B.V.", + "email": "info@mollie.com" + } + ], + "description": "Mollie API client library for PHP. Mollie is a European Payment Service provider and offers international payment methods such as Mastercard, VISA, American Express and PayPal, and local payment methods such as iDEAL, Bancontact, SOFORT Banking, SEPA direct debit, Belfius Direct Net, KBC Payment Button and various gift cards such as Podiumcadeaukaart and fashioncheque.", + "homepage": "https://www.mollie.com/en/developers", + "keywords": [ + "Apple Pay", + "CBC", + "Przelewy24", + "api", + "bancontact", + "banktransfer", + "belfius", + "belfius direct net", + "charges", + "creditcard", + "direct debit", + "fashioncheque", + "gateway", + "gift cards", + "ideal", + "inghomepay", + "intersolve", + "kbc", + "klarna", + "mistercash", + "mollie", + "paylater", + "payment", + "payments", + "paypal", + "paysafecard", + "podiumcadeaukaart", + "recurring", + "refunds", + "sepa", + "service", + "sliceit", + "sofort", + "sofortbanking", + "subscriptions" + ], + "support": { + "issues": "https://github.com/mollie/mollie-api-php/issues", + "source": "https://github.com/mollie/mollie-api-php/tree/v3.1.4" + }, + "time": "2025-06-11T13:14:29+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": "mtdowling/jmespath.php", + "version": "2.8.0", + "source": { + "type": "git", + "url": "https://github.com/jmespath/jmespath.php.git", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-mbstring": "^1.17" + }, + "require-dev": { + "composer/xdebug-handler": "^3.0.3", + "phpunit/phpunit": "^8.5.33" + }, + "bin": [ + "bin/jp.php" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "files": [ + "src/JmesPath.php" + ], + "psr-4": { + "JmesPath\\": "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" + } + ], + "description": "Declaratively specify how to extract elements from a JSON document", + "keywords": [ + "json", + "jsonpath" + ], + "support": { + "issues": "https://github.com/jmespath/jmespath.php/issues", + "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0" + }, + "time": "2024-09-04T18:46:31+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.10.1", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "reference": "1fd1935b2d90aef2f093c5e35f7ae1257c448d00", + "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": "^3.75.0", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "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-06-21T15:19:35+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.7", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "shasum": "" + }, + "require": { + "php": "8.0 - 8.4" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "dev-master", + "nette/tester": "^2.5", + "phpstan/phpstan": "^1.0", + "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": { + "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.7" + }, + "time": "2025-06-03T04:55:08+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.5.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "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.0-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.5.0" + }, + "time": "2025-05-31T08:24:38+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": "nyholm/psr7", + "version": "1.8.2", + "source": { + "type": "git", + "url": "https://github.com/Nyholm/psr7.git", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Nyholm/psr7/zipball/a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "reference": "a71f2b11690f4b24d099d6b16690a90ae14fc6f3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0", + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "http-interop/http-factory-tests": "^0.9", + "php-http/message-factory": "^1.0", + "php-http/psr7-integration-tests": "^1.0", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.4", + "symfony/error-handler": "^4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.8-dev" + } + }, + "autoload": { + "psr-4": { + "Nyholm\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + }, + { + "name": "Martijn van der Ven", + "email": "martijn@vanderven.se" + } + ], + "description": "A fast PHP7 implementation of PSR-7", + "homepage": "https://tnyholm.se", + "keywords": [ + "psr-17", + "psr-7" + ], + "support": { + "issues": "https://github.com/Nyholm/psr7/issues", + "source": "https://github.com/Nyholm/psr7/tree/1.8.2" + }, + "funding": [ + { + "url": "https://github.com/Zegnat", + "type": "github" + }, + { + "url": "https://github.com/nyholm", + "type": "github" + } + ], + "time": "2024-09-09T07:06:30+00:00" + }, + { + "name": "openai-php/client", + "version": "v0.14.0", + "source": { + "type": "git", + "url": "https://github.com/openai-php/client.git", + "reference": "c176c964902272649c10f092e2513bc12179161f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/openai-php/client/zipball/c176c964902272649c10f092e2513bc12179161f", + "reference": "c176c964902272649c10f092e2513bc12179161f", + "shasum": "" + }, + "require": { + "php": "^8.2.0", + "php-http/discovery": "^1.20.0", + "php-http/multipart-stream-builder": "^1.4.2", + "psr/http-client": "^1.0.3", + "psr/http-client-implementation": "^1.0.1", + "psr/http-factory-implementation": "*", + "psr/http-message": "^1.1.0|^2.0.0" + }, + "require-dev": { + "guzzlehttp/guzzle": "^7.9.3", + "guzzlehttp/psr7": "^2.7.1", + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "nunomaduro/collision": "^8.8.0", + "pestphp/pest": "^3.8.2|^4.0.0", + "pestphp/pest-plugin-arch": "^3.1.1|^4.0.0", + "pestphp/pest-plugin-type-coverage": "^3.5.1|^4.0.0", + "phpstan/phpstan": "^1.12.25", + "symfony/var-dumper": "^7.2.6" + }, + "type": "library", + "autoload": { + "files": [ + "src/OpenAI.php" + ], + "psr-4": { + "OpenAI\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + }, + { + "name": "Sandro Gehri" + } + ], + "description": "OpenAI PHP is a supercharged PHP API client that allows you to interact with the Open AI API", + "keywords": [ + "GPT-3", + "api", + "client", + "codex", + "dall-e", + "language", + "natural", + "openai", + "php", + "processing", + "sdk" + ], + "support": { + "issues": "https://github.com/openai-php/client/issues", + "source": "https://github.com/openai-php/client/tree/v0.14.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-06-24T10:49:48+00:00" + }, + { + "name": "paytabscom/laravel_paytabs", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/paytabscom/paytabs-php-laravel-package.git", + "reference": "a3b7c8a21e47fd5d529051c21c2048424552e184" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paytabscom/paytabs-php-laravel-package/zipball/a3b7c8a21e47fd5d529051c21c2048424552e184", + "reference": "a3b7c8a21e47fd5d529051c21c2048424552e184", + "shasum": "" + }, + "require": { + "php": "^7.0|^8.0" + }, + "type": "composer-package", + "autoload": { + "psr-4": { + "Paytabscom\\Laravel_paytabs\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Walaa Elsaeed", + "email": "w.elsaeed@paytabs.com" + } + ], + "description": "Official laravel package to implement PayTabs integration with laravel apps", + "homepage": "https://site.paytabs.com/en/", + "keywords": [ + "E-comerce", + "laravel", + "payments", + "paytabs" + ], + "support": { + "issues": "https://github.com/paytabscom/paytabs-php-laravel-package/issues", + "source": "https://github.com/paytabscom/paytabs-php-laravel-package/tree/V1.9.0" + }, + "time": "2025-02-26T12:00:07+00:00" + }, + { + "name": "php-ds/php-ds", + "version": "v1.7.0", + "source": { + "type": "git", + "url": "https://github.com/php-ds/polyfill.git", + "reference": "017fb5cdfa52a1f13126c94987b04b884c44f9cd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-ds/polyfill/zipball/017fb5cdfa52a1f13126c94987b04b884c44f9cd", + "reference": "017fb5cdfa52a1f13126c94987b04b884c44f9cd", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.4" + }, + "provide": { + "ext-ds": "1.5.0" + }, + "require-dev": { + "php-ds/tests": "^1.5" + }, + "suggest": { + "ext-ds": "to improve performance and reduce memory usage" + }, + "type": "library", + "autoload": { + "psr-4": { + "Ds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rudi Theunissen", + "email": "rudolf.theunissen@gmail.com" + } + ], + "description": "Specialized data structures as alternatives to the PHP array", + "keywords": [ + "data structures", + "ds", + "php", + "polyfill" + ], + "support": { + "issues": "https://github.com/php-ds/polyfill/issues", + "source": "https://github.com/php-ds/polyfill/tree/v1.7.0" + }, + "time": "2025-05-18T04:50:53+00:00" + }, + { + "name": "php-http/client-common", + "version": "2.7.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/client-common.git", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/client-common/zipball/0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "reference": "0cfe9858ab9d3b213041b947c881d5b19ceeca46", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/httplug": "^2.0", + "php-http/message": "^1.6", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.0 || ^2.0", + "symfony/options-resolver": "~4.0.15 || ~4.1.9 || ^4.2.1 || ^5.0 || ^6.0 || ^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "require-dev": { + "doctrine/instantiator": "^1.1", + "guzzlehttp/psr7": "^1.4", + "nyholm/psr7": "^1.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "phpspec/prophecy": "^1.10.2", + "phpunit/phpunit": "^7.5.20 || ^8.5.33 || ^9.6.7" + }, + "suggest": { + "ext-json": "To detect JSON responses with the ContentTypePlugin", + "ext-libxml": "To detect XML responses with the ContentTypePlugin", + "php-http/cache-plugin": "PSR-6 Cache plugin", + "php-http/logger-plugin": "PSR-3 Logger plugin", + "php-http/stopwatch-plugin": "Symfony Stopwatch plugin" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\Common\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Common HTTP Client implementations and tools for HTTPlug", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "common", + "http", + "httplug" + ], + "support": { + "issues": "https://github.com/php-http/client-common/issues", + "source": "https://github.com/php-http/client-common/tree/2.7.2" + }, + "time": "2024-09-24T06:21:48+00:00" + }, + { + "name": "php-http/discovery", + "version": "1.20.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/discovery.git", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/discovery/zipball/82fe4c73ef3363caed49ff8dd1539ba06044910d", + "reference": "82fe4c73ef3363caed49ff8dd1539ba06044910d", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^1.0|^2.0", + "php": "^7.1 || ^8.0" + }, + "conflict": { + "nyholm/psr7": "<1.0", + "zendframework/zend-diactoros": "*" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "*", + "psr/http-factory-implementation": "*", + "psr/http-message-implementation": "*" + }, + "require-dev": { + "composer/composer": "^1.0.2|^2.0", + "graham-campbell/phpspec-skip-example-extension": "^5.0", + "php-http/httplug": "^1.0 || ^2.0", + "php-http/message-factory": "^1.0", + "phpspec/phpspec": "^5.1 || ^6.1 || ^7.3", + "sebastian/comparator": "^3.0.5 || ^4.0.8", + "symfony/phpunit-bridge": "^6.4.4 || ^7.0.1" + }, + "type": "composer-plugin", + "extra": { + "class": "Http\\Discovery\\Composer\\Plugin", + "plugin-optional": true + }, + "autoload": { + "psr-4": { + "Http\\Discovery\\": "src/" + }, + "exclude-from-classmap": [ + "src/Composer/Plugin.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Finds and installs PSR-7, PSR-17, PSR-18 and HTTPlug implementations", + "homepage": "http://php-http.org", + "keywords": [ + "adapter", + "client", + "discovery", + "factory", + "http", + "message", + "psr17", + "psr7" + ], + "support": { + "issues": "https://github.com/php-http/discovery/issues", + "source": "https://github.com/php-http/discovery/tree/1.20.0" + }, + "time": "2024-10-02T11:20:13+00:00" + }, + { + "name": "php-http/httplug", + "version": "2.4.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/httplug.git", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/httplug/zipball/5cad731844891a4c282f3f3e1b582c46839d22f4", + "reference": "5cad731844891a4c282f3f3e1b582c46839d22f4", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/promise": "^1.1", + "psr/http-client": "^1.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.1 || ^5.0 || ^6.0", + "phpspec/phpspec": "^5.1 || ^6.0 || ^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eric GELOEN", + "email": "geloen.eric@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "HTTPlug, the HTTP client abstraction for PHP", + "homepage": "http://httplug.io", + "keywords": [ + "client", + "http" + ], + "support": { + "issues": "https://github.com/php-http/httplug/issues", + "source": "https://github.com/php-http/httplug/tree/2.4.1" + }, + "time": "2024-09-23T11:39:58+00:00" + }, + { + "name": "php-http/message", + "version": "1.16.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/message.git", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "shasum": "" + }, + "require": { + "clue/stream-filter": "^1.5", + "php": "^7.2 || ^8.0", + "psr/http-message": "^1.1 || ^2.0" + }, + "provide": { + "php-http/message-factory-implementation": "1.0" + }, + "require-dev": { + "ergebnis/composer-normalize": "^2.6", + "ext-zlib": "*", + "guzzlehttp/psr7": "^1.0 || ^2.0", + "laminas/laminas-diactoros": "^2.0 || ^3.0", + "php-http/message-factory": "^1.0.2", + "phpspec/phpspec": "^5.1 || ^6.3 || ^7.1", + "slim/slim": "^3.0" + }, + "suggest": { + "ext-zlib": "Used with compressor/decompressor streams", + "guzzlehttp/psr7": "Used with Guzzle PSR-7 Factories", + "laminas/laminas-diactoros": "Used with Diactoros Factories", + "slim/slim": "Used with Slim Framework PSR-7 implementation" + }, + "type": "library", + "autoload": { + "files": [ + "src/filters.php" + ], + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "HTTP Message related tools", + "homepage": "http://php-http.org", + "keywords": [ + "http", + "message", + "psr-7" + ], + "support": { + "issues": "https://github.com/php-http/message/issues", + "source": "https://github.com/php-http/message/tree/1.16.2" + }, + "time": "2024-10-02T11:34:13+00:00" + }, + { + "name": "php-http/message-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-http/message-factory.git", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/message-factory/zipball/4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "reference": "4d8778e1c7d405cbb471574821c1ff5b68cc8f57", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Factory interfaces for PSR-7 HTTP Message", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "stream", + "uri" + ], + "support": { + "issues": "https://github.com/php-http/message-factory/issues", + "source": "https://github.com/php-http/message-factory/tree/1.1.0" + }, + "abandoned": "psr/http-factory", + "time": "2023-04-14T14:16:17+00:00" + }, + { + "name": "php-http/multipart-stream-builder", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/php-http/multipart-stream-builder.git", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "php-http/discovery": "^1.15", + "psr/http-factory-implementation": "^1.0" + }, + "require-dev": { + "nyholm/psr7": "^1.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.0.2", + "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Message\\MultipartStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com" + } + ], + "description": "A builder class that help you create a multipart stream", + "homepage": "http://php-http.org", + "keywords": [ + "factory", + "http", + "message", + "multipart stream", + "stream" + ], + "support": { + "issues": "https://github.com/php-http/multipart-stream-builder/issues", + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2" + }, + "time": "2024-09-04T13:22:54+00:00" + }, + { + "name": "php-http/promise", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/php-http/promise.git", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-http/promise/zipball/fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "reference": "fc85b1fba37c169a69a07ef0d5a8075770cc1f83", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friends-of-phpspec/phpspec-code-coverage": "^4.3.2 || ^6.3", + "phpspec/phpspec": "^5.1.2 || ^6.2 || ^7.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Http\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Joel Wurtz", + "email": "joel.wurtz@gmail.com" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com" + } + ], + "description": "Promise used for asynchronous HTTP requests", + "homepage": "http://httplug.io", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/php-http/promise/issues", + "source": "https://github.com/php-http/promise/tree/1.3.1" + }, + "time": "2024-03-15T13:55:21+00:00" + }, + { + "name": "phpoffice/math", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/Math.git", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.1|^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpunit/phpunit": "^7.0 || ^9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\Math\\": "src/Math/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Progi1984", + "homepage": "https://lefevre.dev" + } + ], + "description": "Math - Manipulate Math Formula", + "homepage": "https://phpoffice.github.io/Math/", + "keywords": [ + "MathML", + "officemathml", + "php" + ], + "support": { + "issues": "https://github.com/PHPOffice/Math/issues", + "source": "https://github.com/PHPOffice/Math/tree/0.3.0" + }, + "time": "2025-05-29T08:31:49+00:00" + }, + { + "name": "phpoffice/phpspreadsheet", + "version": "5.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/eecd31b885a1c8192f12738130f85bbc6e8906ba", + "reference": "eecd31b885a1c8192f12738130f85bbc6e8906ba", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-filter": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.1", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-intl": "*", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.5", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()", + "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" + }, + { + "name": "Owen Leibman" + } + ], + "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/5.5.0" + }, + "time": "2026-03-01T00:58:56+00:00" + }, + { + "name": "phpoffice/phpword", + "version": "1.4.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PHPWord.git", + "reference": "6d75328229bc93790b37e93741adf70646cea958" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", + "reference": "6d75328229bc93790b37e93741adf70646cea958", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-gd": "*", + "ext-json": "*", + "ext-xml": "*", + "ext-zip": "*", + "php": "^7.1|^8.0", + "phpoffice/math": "^0.3" + }, + "require-dev": { + "dompdf/dompdf": "^2.0 || ^3.0", + "ext-libxml": "*", + "friendsofphp/php-cs-fixer": "^3.3", + "mpdf/mpdf": "^7.0 || ^8.0", + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": ">=7.0", + "symfony/process": "^4.4 || ^5.0", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Allows writing PDF", + "ext-xmlwriter": "Allows writing OOXML and ODF", + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpWord\\": "src/PhpWord" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-only" + ], + "authors": [ + { + "name": "Mark Baker" + }, + { + "name": "Gabriel Bull", + "email": "me@gabrielbull.com", + "homepage": "http://gabrielbull.com/" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net/blog/" + }, + { + "name": "Ivan Lanin", + "homepage": "http://ivan.lanin.org" + }, + { + "name": "Roman Syroeshko", + "homepage": "http://ru.linkedin.com/pub/roman-syroeshko/34/a53/994/" + }, + { + "name": "Antoine de Troostembergh" + } + ], + "description": "PHPWord - A pure PHP library for reading and writing word processing documents (OOXML, ODF, RTF, HTML, PDF)", + "homepage": "https://phpoffice.github.io/PHPWord/", + "keywords": [ + "ISO IEC 29500", + "OOXML", + "Office Open XML", + "OpenDocument", + "OpenXML", + "PhpOffice", + "PhpWord", + "Rich Text Format", + "WordprocessingML", + "doc", + "docx", + "html", + "odf", + "odt", + "office", + "pdf", + "php", + "reader", + "rtf", + "template", + "template processor", + "word", + "writer" + ], + "support": { + "issues": "https://github.com/PHPOffice/PHPWord/issues", + "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" + }, + "time": "2025-06-05T10:32:36+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.3", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54", + "reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28" + }, + "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.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2024-07-20T21:41:07+00:00" + }, + { + "name": "psr/cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/3.0.0" + }, + "time": "2021-02-03T23:26:27+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.9", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "1b801844becfe648985372cb4b12ad6840245ace" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/1b801844becfe648985372cb4b12ad6840245ace", + "reference": "1b801844becfe648985372cb4b12ad6840245ace", + "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", + "homepage": "http://justinhileman.com" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "http://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.9" + }, + "time": "2025-06-23T02:35:06+00:00" + }, + { + "name": "rachidlaasri/laravel-installer", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/rashidlaasri/LaravelInstaller.git", + "reference": "b751b4c23dba893e9a4a12f881a6fd8fa921d228" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rashidlaasri/LaravelInstaller/zipball/b751b4c23dba893e9a4a12f881a6fd8fa921d228", + "reference": "b751b4c23dba893e9a4a12f881a6fd8fa921d228", + "shasum": "" + }, + "require": { + "php": ">=7.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "RachidLaasri\\LaravelInstaller\\Providers\\LaravelInstallerServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Helpers/functions.php" + ], + "psr-4": { + "RachidLaasri\\LaravelInstaller\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rachid Laasri", + "email": "rashidlaasri@gmail.com" + }, + { + "name": "Jeremy Kenedy", + "email": "jeremykenedy@gmail.com" + } + ], + "description": "Laravel web installer", + "support": { + "issues": "https://github.com/rashidlaasri/LaravelInstaller/issues", + "source": "https://github.com/rashidlaasri/LaravelInstaller/tree/4.1.0" + }, + "funding": [ + { + "url": "https://rachidlaasri.com", + "type": "custom" + }, + { + "url": "https://twitter.com/rashidlaasri", + "type": "custom" + } + ], + "abandoned": true, + "time": "2020-05-19T13:19:45+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.0", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "reference": "4e0e23cc785f0724a0e838279a9eb03f28b092a0", + "shasum": "" + }, + "require": { + "brick/math": "^0.8.8 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13", + "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.0" + }, + "time": "2025-06-25T14:20:11+00:00" + }, + { + "name": "razorpay/razorpay", + "version": "2.9.1", + "source": { + "type": "git", + "url": "https://github.com/razorpay/razorpay-php.git", + "reference": "d5e49ef12c4862f6bc8003f87f79b942a9dd3a8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/razorpay/razorpay-php/zipball/d5e49ef12c4862f6bc8003f87f79b942a9dd3a8b", + "reference": "d5e49ef12c4862f6bc8003f87f79b942a9dd3a8b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=7.3", + "rmccue/requests": "^2.0" + }, + "require-dev": { + "phpunit/phpunit": "^9", + "raveren/kint": "1.*" + }, + "type": "library", + "autoload": { + "files": [ + "Deprecated.php" + ], + "psr-4": { + "Razorpay\\Api\\": "src/", + "Razorpay\\Tests\\": "tests/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Abhay Rana", + "email": "nemo@razorpay.com", + "homepage": "https://captnemo.in", + "role": "Developer" + }, + { + "name": "Shashank Kumar", + "email": "shashank@razorpay.com", + "role": "Developer" + } + ], + "description": "Razorpay PHP Client Library", + "homepage": "https://docs.razorpay.com", + "keywords": [ + "api", + "client", + "php", + "razorpay" + ], + "support": { + "email": "contact@razorpay.com", + "issues": "https://github.com/Razorpay/razorpay-php/issues", + "source": "https://github.com/Razorpay/razorpay-php" + }, + "time": "2025-03-20T12:51:47+00:00" + }, + { + "name": "rmccue/requests", + "version": "v2.0.15", + "source": { + "type": "git", + "url": "https://github.com/WordPress/Requests.git", + "reference": "877cd66169755899682f1595e057334b40d9d149" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/Requests/zipball/877cd66169755899682f1595e057334b40d9d149", + "reference": "877cd66169755899682f1595e057334b40d9d149", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.6" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.7", + "php-parallel-lint/php-console-highlighter": "^0.5.0", + "php-parallel-lint/php-parallel-lint": "^1.3.1", + "phpcompatibility/php-compatibility": "^9.0", + "requests/test-server": "dev-main", + "roave/security-advisories": "dev-latest", + "squizlabs/php_codesniffer": "^3.6", + "wp-coding-standards/wpcs": "^2.0", + "yoast/phpunit-polyfills": "^1.0.0" + }, + "suggest": { + "art4/requests-psr18-adapter": "For using Requests as a PSR-18 HTTP Client", + "ext-curl": "For improved performance", + "ext-openssl": "For secure transport support", + "ext-zlib": "For improved performance when decompressing encoded streams" + }, + "type": "library", + "autoload": { + "files": [ + "library/Deprecated.php" + ], + "psr-4": { + "WpOrg\\Requests\\": "src/" + }, + "classmap": [ + "library/Requests.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Ryan McCue", + "homepage": "https://rmccue.io/" + }, + { + "name": "Alain Schlesser", + "homepage": "https://github.com/schlessera" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl" + }, + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/Requests/graphs/contributors" + } + ], + "description": "A HTTP library written in PHP, for human beings.", + "homepage": "https://requests.ryanmccue.info/", + "keywords": [ + "curl", + "fsockopen", + "http", + "idna", + "ipv6", + "iri", + "sockets" + ], + "support": { + "docs": "https://requests.ryanmccue.info/", + "issues": "https://github.com/WordPress/Requests/issues", + "source": "https://github.com/WordPress/Requests" + }, + "time": "2025-01-21T10:13:31+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": "sentry/sdk", + "version": "3.6.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php-sdk.git", + "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php-sdk/zipball/24c235ff2027401cbea099bf88689e1a1f197c7a", + "reference": "24c235ff2027401cbea099bf88689e1a1f197c7a", + "shasum": "" + }, + "require": { + "http-interop/http-factory-guzzle": "^1.0", + "sentry/sentry": "^3.22", + "symfony/http-client": "^4.3|^5.0|^6.0|^7.0" + }, + "type": "metapackage", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "This is a metapackage shipping sentry/sentry with a recommended HTTP client.", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php-sdk/issues", + "source": "https://github.com/getsentry/sentry-php-sdk/tree/3.6.0" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2023-12-04T10:49:33+00:00" + }, + { + "name": "sentry/sentry", + "version": "3.22.1", + "source": { + "type": "git", + "url": "https://github.com/getsentry/sentry-php.git", + "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", + "reference": "8859631ba5ab15bc1af420b0eeed19ecc6c9d81d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "guzzlehttp/promises": "^1.5.3|^2.0", + "jean85/pretty-package-versions": "^1.5|^2.0.4", + "php": "^7.2|^8.0", + "php-http/async-client-implementation": "^1.0", + "php-http/client-common": "^1.5|^2.0", + "php-http/discovery": "^1.15", + "php-http/httplug": "^1.1|^2.0", + "php-http/message": "^1.5", + "php-http/message-factory": "^1.1", + "psr/http-factory": "^1.0", + "psr/http-factory-implementation": "^1.0", + "psr/log": "^1.0|^2.0|^3.0", + "symfony/options-resolver": "^3.4.43|^4.4.30|^5.0.11|^6.0|^7.0", + "symfony/polyfill-php80": "^1.17" + }, + "conflict": { + "php-http/client-common": "1.8.0", + "raven/raven": "*" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.19|3.4.*", + "guzzlehttp/psr7": "^1.8.4|^2.1.1", + "http-interop/http-factory-guzzle": "^1.0", + "monolog/monolog": "^1.6|^2.0|^3.0", + "nikic/php-parser": "^4.10.3", + "php-http/mock-client": "^1.3", + "phpbench/phpbench": "^1.0", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.3", + "phpstan/phpstan-phpunit": "^1.0", + "phpunit/phpunit": "^8.5.14|^9.4", + "symfony/phpunit-bridge": "^5.2|^6.0", + "vimeo/psalm": "^4.17" + }, + "suggest": { + "monolog/monolog": "Allow sending log messages to Sentry by using the included Monolog handler." + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Sentry\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sentry", + "email": "accounts@sentry.io" + } + ], + "description": "A PHP SDK for Sentry (http://sentry.io)", + "homepage": "http://sentry.io", + "keywords": [ + "crash-reporting", + "crash-reports", + "error-handler", + "error-monitoring", + "log", + "logging", + "sentry" + ], + "support": { + "issues": "https://github.com/getsentry/sentry-php/issues", + "source": "https://github.com/getsentry/sentry-php/tree/3.22.1" + }, + "funding": [ + { + "url": "https://sentry.io/", + "type": "custom" + }, + { + "url": "https://sentry.io/pricing/", + "type": "custom" + } + ], + "time": "2023-11-13T11:47:28+00:00" + }, + { + "name": "simplesoftwareio/simple-qrcode", + "version": "4.2.0", + "source": { + "type": "git", + "url": "https://github.com/SimpleSoftwareIO/simple-qrcode.git", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/SimpleSoftwareIO/simple-qrcode/zipball/916db7948ca6772d54bb617259c768c9cdc8d537", + "reference": "916db7948ca6772d54bb617259c768c9cdc8d537", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^2.0", + "ext-gd": "*", + "php": ">=7.2|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1", + "phpunit/phpunit": "~9" + }, + "suggest": { + "ext-imagick": "Allows the generation of PNG QrCodes.", + "illuminate/support": "Allows for use within Laravel." + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "QrCode": "SimpleSoftwareIO\\QrCode\\Facades\\QrCode" + }, + "providers": [ + "SimpleSoftwareIO\\QrCode\\QrCodeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "SimpleSoftwareIO\\QrCode\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Simple Software LLC", + "email": "support@simplesoftware.io" + } + ], + "description": "Simple QrCode is a QR code generator made for Laravel.", + "homepage": "https://www.simplesoftware.io/#/docs/simple-qrcode", + "keywords": [ + "Simple", + "generator", + "laravel", + "qrcode", + "wrapper" + ], + "support": { + "issues": "https://github.com/SimpleSoftwareIO/simple-qrcode/issues", + "source": "https://github.com/SimpleSoftwareIO/simple-qrcode/tree/4.2.0" + }, + "time": "2021-02-08T20:43:55+00:00" + }, + { + "name": "spatie/image", + "version": "3.8.5", + "source": { + "type": "git", + "url": "https://github.com/spatie/image.git", + "reference": "a63f60b7387ebeacab463e79a95deb7ffed75430" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/image/zipball/a63f60b7387ebeacab463e79a95deb7ffed75430", + "reference": "a63f60b7387ebeacab463e79a95deb7ffed75430", + "shasum": "" + }, + "require": { + "ext-exif": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.2", + "spatie/image-optimizer": "^1.7.5", + "spatie/temporary-directory": "^2.2", + "symfony/process": "^6.4|^7.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-imagick": "*", + "laravel/sail": "^1.34", + "pestphp/pest": "^2.28", + "phpstan/phpstan": "^1.10.50", + "spatie/pest-plugin-snapshots": "^2.1", + "spatie/pixelmatch-php": "^1.0", + "spatie/ray": "^1.40.1", + "symfony/var-dumper": "^6.4|7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Image\\": "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": "Manipulate images with an expressive API", + "homepage": "https://github.com/spatie/image", + "keywords": [ + "image", + "spatie" + ], + "support": { + "source": "https://github.com/spatie/image/tree/3.8.5" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-06-27T12:44:55+00:00" + }, + { + "name": "spatie/image-optimizer", + "version": "1.8.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/image-optimizer.git", + "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/4fd22035e81d98fffced65a8c20d9ec4daa9671c", + "reference": "4fd22035e81d98fffced65a8c20d9ec4daa9671c", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.3|^8.0", + "psr/log": "^1.0 | ^2.0 | ^3.0", + "symfony/process": "^4.2|^5.0|^6.0|^7.0" + }, + "require-dev": { + "pestphp/pest": "^1.21", + "phpunit/phpunit": "^8.5.21|^9.4.4", + "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\ImageOptimizer\\": "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": "Easily optimize images using PHP", + "homepage": "https://github.com/spatie/image-optimizer", + "keywords": [ + "image-optimizer", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/image-optimizer/issues", + "source": "https://github.com/spatie/image-optimizer/tree/1.8.0" + }, + "time": "2024-11-04T08:24:54+00:00" + }, + { + "name": "spatie/laravel-medialibrary", + "version": "11.13.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-medialibrary.git", + "reference": "e2324b2f138ec41181089a7dcf28489be93ede53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/e2324b2f138ec41181089a7dcf28489be93ede53", + "reference": "e2324b2f138ec41181089a7dcf28489be93ede53", + "shasum": "" + }, + "require": { + "composer/semver": "^3.4", + "ext-exif": "*", + "ext-fileinfo": "*", + "ext-json": "*", + "illuminate/bus": "^10.2|^11.0|^12.0", + "illuminate/conditionable": "^10.2|^11.0|^12.0", + "illuminate/console": "^10.2|^11.0|^12.0", + "illuminate/database": "^10.2|^11.0|^12.0", + "illuminate/pipeline": "^10.2|^11.0|^12.0", + "illuminate/support": "^10.2|^11.0|^12.0", + "maennchen/zipstream-php": "^3.1", + "php": "^8.2", + "spatie/image": "^3.3.2", + "spatie/laravel-package-tools": "^1.16.1", + "spatie/temporary-directory": "^2.2", + "symfony/console": "^6.4.1|^7.0" + }, + "conflict": { + "php-ffmpeg/php-ffmpeg": "<0.6.1" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.293.10", + "ext-imagick": "*", + "ext-pdo_sqlite": "*", + "ext-zip": "*", + "guzzlehttp/guzzle": "^7.8.1", + "larastan/larastan": "^2.7|^3.0", + "league/flysystem-aws-s3-v3": "^3.22", + "mockery/mockery": "^1.6.7", + "orchestra/testbench": "^7.0|^8.17|^9.0|^10.0", + "pestphp/pest": "^2.28|^3.5", + "phpstan/extension-installer": "^1.3.1", + "spatie/laravel-ray": "^1.33", + "spatie/pdf-to-image": "^2.2|^3.0", + "spatie/pest-plugin-snapshots": "^2.1" + }, + "suggest": { + "league/flysystem-aws-s3-v3": "Required to use AWS S3 file storage", + "php-ffmpeg/php-ffmpeg": "Required for generating video thumbnails", + "spatie/pdf-to-image": "Required for generating thumbnails of PDFs and SVGs" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\MediaLibrary\\MediaLibraryServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\MediaLibrary\\": "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": "Associate files with Eloquent models", + "homepage": "https://github.com/spatie/laravel-medialibrary", + "keywords": [ + "cms", + "conversion", + "downloads", + "images", + "laravel", + "laravel-medialibrary", + "media", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-medialibrary/issues", + "source": "https://github.com/spatie/laravel-medialibrary/tree/11.13.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-05-22T12:25:27+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.92.4", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "d20b1969f836d210459b78683d85c9cd5c5f508c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d20b1969f836d210459b78683d85c9cd5c5f508c", + "reference": "d20b1969f836d210459b78683d85c9cd5c5f508c", + "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.4" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-04-11T15:27:14+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "6.20.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "31c05679102c73f3b0d05790d2400182745a5615" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/31c05679102c73f3b0d05790d2400182745a5615", + "reference": "31c05679102c73f3b0d05790d2400182745a5615", + "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.20.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-06-05T07:33:07+00:00" + }, + { + "name": "spatie/temporary-directory", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", + "keywords": [ + "php", + "spatie", + "temporary-directory" + ], + "support": { + "issues": "https://github.com/spatie/temporary-directory/issues", + "source": "https://github.com/spatie/temporary-directory/tree/2.3.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-01-13T13:04:43+00:00" + }, + { + "name": "stripe/stripe-php", + "version": "v17.4.0", + "source": { + "type": "git", + "url": "https://github.com/stripe/stripe-php.git", + "reference": "893946057e43b145826b0dfd7f398673e381e2ae" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/stripe/stripe-php/zipball/893946057e43b145826b0dfd7f398673e381e2ae", + "reference": "893946057e43b145826b0dfd7f398673e381e2ae", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=5.6.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "3.72.0", + "phpstan/phpstan": "^1.2", + "phpunit/phpunit": "^5.7 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0-dev" + } + }, + "autoload": { + "psr-4": { + "Stripe\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Stripe and contributors", + "homepage": "https://github.com/stripe/stripe-php/contributors" + } + ], + "description": "Stripe PHP Library", + "homepage": "https://stripe.com/", + "keywords": [ + "api", + "payment processing", + "stripe" + ], + "support": { + "issues": "https://github.com/stripe/stripe-php/issues", + "source": "https://github.com/stripe/stripe-php/tree/v17.4.0" + }, + "time": "2025-07-01T20:23:15+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.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", + "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", + "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.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/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.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/35b55b166f6752d6aaf21aa042fc5ed280fce235", + "reference": "35b55b166f6752d6aaf21aa042fc5ed280fce235", + "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.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-13T07:48:40+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", + "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", + "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.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-22T09:11:45+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/filesystem", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d551b38811096d0be9c4691d406991b47c0c630a", + "reference": "d551b38811096d0be9c4691d406991b47c0c630a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-mbstring": "~1.8" + }, + "require-dev": { + "symfony/process": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "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 basic utilities for the filesystem", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/filesystem/tree/v7.4.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-11-27T13:27:24+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "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.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-12-30T19:00:26+00:00" + }, + { + "name": "symfony/http-client", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client.git", + "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client/zipball/4403d87a2c16f33345dca93407a8714ee8c05a64", + "reference": "4403d87a2c16f33345dca93407a8714ee8c05a64", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-client-contracts": "~3.4.4|^3.5.2", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "amphp/amp": "<2.5", + "amphp/socket": "<1.1", + "php-http/discovery": "<1.15", + "symfony/http-foundation": "<6.4" + }, + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/http-client-implementation": "1.0", + "symfony/http-client-implementation": "3.0" + }, + "require-dev": { + "amphp/http-client": "^4.2.1|^5.0", + "amphp/http-tunnel": "^1.0|^2.0", + "guzzlehttp/promises": "^1.4|^2.0", + "nyholm/psr7": "^1.0", + "php-http/httplug": "^1.0|^2.0", + "psr/http-client": "^1.0", + "symfony/amphp-http-client-meta": "^1.0|^2.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/http-kernel": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/rate-limiter": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpClient\\": "" + }, + "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 powerful methods to fetch HTTP resources synchronously or asynchronously", + "homepage": "https://symfony.com", + "keywords": [ + "http" + ], + "support": { + "source": "https://github.com/symfony/http-client/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-28T07:58:39+00:00" + }, + { + "name": "symfony/http-client-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-client-contracts.git", + "reference": "75d7043853a42837e68111812f4d964b01e5101c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/75d7043853a42837e68111812f4d964b01e5101c", + "reference": "75d7043853a42837e68111812f4d964b01e5101c", + "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\\HttpClient\\": "" + }, + "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 HTTP clients", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/http-client-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-29T11:18:49+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v7.4.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/bd1af1e425811d6f077db240c3a588bdb405cd27", + "reference": "bd1af1e425811d6f077db240c3a588bdb405cd27", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "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|^8.0", + "symfony/clock": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/mime": "^6.4|^7.0|^8.0", + "symfony/rate-limiter": "^6.4|^7.0|^8.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.4.1" + }, + "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-12-07T11:13:10+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "reference": "1644879a66e4aa29c36fe33dfa6c54b450ce1831", + "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.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-28T08:24:55+00:00" + }, + { + "name": "symfony/mailer", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "reference": "b5db5105b290bdbea5ab27b89c69effcf1cb3368", + "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.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/mime", + "version": "v7.4.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/bdb02729471be5d047a3ac4a69068748f1a6be7a", + "reference": "bdb02729471be5d047a3ac4a69068748f1a6be7a", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "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|^8.0", + "symfony/process": "^6.4|^7.0|^8.0", + "symfony/property-access": "^6.4|^7.0|^8.0", + "symfony/property-info": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4.3|^7.0.3|^8.0" + }, + "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.4.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-11-16T10:14:42+00:00" + }, + { + "name": "symfony/options-resolver", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/options-resolver.git", + "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/afb9a8038025e5dbc657378bfab9198d75f10fca", + "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "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 an improved replacement for the array_replace PHP function", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "support": { + "source": "https://github.com/symfony/options-resolver/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": "2025-04-04T13:12:05+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.32.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.32.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-09T11:45:10+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.32.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "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.32.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-09T11:45:10+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-uuid", + "version": "v1.32.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.32.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-09T11:45:10+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "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.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-17T09:11:12+00:00" + }, + { + "name": "symfony/routing", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "8e213820c5fea844ecea29203d2a308019007c15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/8e213820c5fea844ecea29203d2a308019007c15", + "reference": "8e213820c5fea844ecea29203d2a308019007c15", + "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.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-05-24T20:43:28+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.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "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.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-20T20:19:01+00:00" + }, + { + "name": "symfony/translation", + "version": "v7.3.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/241d5ac4910d256660238a7ecf250deba4c73063", + "reference": "241d5ac4910d256660238a7ecf250deba4c73063", + "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.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/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.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "reference": "6e209fbe5f5a7b6043baba46fe5735a4b85d0d42", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/console": "<6.4" + }, + "require-dev": { + "ext-iconv": "*", + "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.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": "tightenco/ziggy", + "version": "v2.5.3", + "source": { + "type": "git", + "url": "https://github.com/tighten/ziggy.git", + "reference": "0b3b521d2c55fbdb04b6721532f7f5f49d32f52b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tighten/ziggy/zipball/0b3b521d2c55fbdb04b6721532f7f5f49d32f52b", + "reference": "0b3b521d2c55fbdb04b6721532f7f5f49d32f52b", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": ">=9.0", + "php": ">=8.1" + }, + "require-dev": { + "laravel/folio": "^1.1", + "orchestra/testbench": "^7.0 || ^8.0 || ^9.0 || ^10.0", + "pestphp/pest": "^2.26|^3.0", + "pestphp/pest-plugin-laravel": "^2.4|^3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Tighten\\Ziggy\\ZiggyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Tighten\\Ziggy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Daniel Coulbourne", + "email": "daniel@tighten.co" + }, + { + "name": "Jake Bathman", + "email": "jake@tighten.co" + }, + { + "name": "Jacob Baker-Kretzmar", + "email": "jacob@tighten.co" + } + ], + "description": "Use your Laravel named routes in JavaScript.", + "homepage": "https://github.com/tighten/ziggy", + "keywords": [ + "Ziggy", + "javascript", + "laravel", + "routes" + ], + "support": { + "issues": "https://github.com/tighten/ziggy/issues", + "source": "https://github.com/tighten/ziggy/tree/v2.5.3" + }, + "time": "2025-05-17T18:15:19+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": "whichbrowser/parser", + "version": "v2.1.8", + "source": { + "type": "git", + "url": "https://github.com/WhichBrowser/Parser-PHP.git", + "reference": "581d614d686bfbec3529ad60562a5213ac5d8d72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WhichBrowser/Parser-PHP/zipball/581d614d686bfbec3529ad60562a5213ac5d8d72", + "reference": "581d614d686bfbec3529ad60562a5213ac5d8d72", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "cache/array-adapter": "^1.1", + "icomefromthenet/reverse-regex": "0.0.6.3", + "php-coveralls/php-coveralls": "^2.0", + "phpunit/php-code-coverage": "^5.0 || ^7.0", + "phpunit/phpunit": "^6.0 || ^8.0", + "squizlabs/php_codesniffer": "^3.5", + "symfony/yaml": "~3.4 || ~4.0" + }, + "suggest": { + "cache/array-adapter": "Allows testing of the caching functionality" + }, + "type": "library", + "autoload": { + "psr-4": { + "WhichBrowser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niels Leenheer", + "email": "niels@leenheer.nl", + "role": "Developer" + } + ], + "description": "Useragent sniffing library for PHP", + "homepage": "http://whichbrowser.net", + "keywords": [ + "browser", + "sniffing", + "ua", + "useragent" + ], + "support": { + "issues": "https://github.com/WhichBrowser/Parser-PHP/issues", + "source": "https://github.com/WhichBrowser/Parser-PHP/tree/v2.1.8" + }, + "time": "2024-04-17T12:47:41+00:00" + }, + { + "name": "yoomoney/yookassa-sdk-php", + "version": "3.8.1", + "source": { + "type": "git", + "url": "https://git.yoomoney.ru/scm/sdk/yookassa-sdk-php.git", + "reference": "27c2aef05a6d30e508fdb99dab49cd5e205bb5ee" + }, + "dist": { + "type": "zip", + "url": "https://git.yoomoney.ru/rest/api/latest/projects/SDK/repos/yookassa-sdk-php/archive?at=refs%2Ftags%2F3.8.1&format=zip", + "reference": "27c2aef05a6d30e508fdb99dab49cd5e205bb5ee" + }, + "require": { + "ext-curl": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": ">=8.0", + "php-ds/php-ds": "^1.4", + "psr/log": "^2.0 || ^3.0", + "yoomoney/yookassa-sdk-validator": "^1.0" + }, + "require-dev": { + "ext-xml": "*", + "friendsofphp/php-cs-fixer": "^3.15", + "mockery/mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.3", + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.6", + "yoomoney/yookassa-fakerphp": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "YooKassa\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "YooMoney", + "email": "cms@yoomoney.ru" + } + ], + "description": "This is a developer tool for integration with YooMoney.", + "homepage": "https://yookassa.ru/developers/api", + "keywords": [ + "api", + "payments", + "sdk", + "yookassa", + "yoomoney" + ], + "time": "2025-07-01T11:52:30+00:00" + }, + { + "name": "yoomoney/yookassa-sdk-validator", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://git.yoomoney.ru/scm/sdk/yookassa-sdk-validator-php.git", + "reference": "1068864a5179dcbb93b244bff43c0e6bc531b62a" + }, + "dist": { + "type": "zip", + "url": "https://git.yoomoney.ru/rest/api/latest/projects/SDK/repos/yookassa-sdk-validator-php/archive?at=refs%2Ftags%2F1.0.3&format=zip" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.0.0" + }, + "require-dev": { + "ext-xml": "*", + "phpunit/phpunit": "^9.6" + }, + "type": "library", + "autoload": { + "psr-4": { + "YooKassa\\Validator\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "YooMoney", + "email": "cms@yoomoney.ru" + } + ], + "description": "This is a developer tool for validating with YooMoney.", + "time": "2024-12-16T16:29:49+00:00" + } + ], + "packages-dev": [ + { + "name": "barryvdh/laravel-debugbar", + "version": "v3.16.0", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-debugbar.git", + "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/f265cf5e38577d42311f1a90d619bcd3740bea23", + "reference": "f265cf5e38577d42311f1a90d619bcd3740bea23", + "shasum": "" + }, + "require": { + "illuminate/routing": "^9|^10|^11|^12", + "illuminate/session": "^9|^10|^11|^12", + "illuminate/support": "^9|^10|^11|^12", + "php": "^8.1", + "php-debugbar/php-debugbar": "~2.2.0", + "symfony/finder": "^6|^7" + }, + "require-dev": { + "mockery/mockery": "^1.3.3", + "orchestra/testbench-dusk": "^7|^8|^9|^10", + "phpunit/phpunit": "^9.5.10|^10|^11", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facades\\Debugbar" + }, + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.16-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Barryvdh\\Debugbar\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "PHP Debugbar integration for Laravel", + "keywords": [ + "debug", + "debugbar", + "dev", + "laravel", + "profiler", + "webprofiler" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-debugbar/issues", + "source": "https://github.com/barryvdh/laravel-debugbar/tree/v3.16.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-07-14T11:56:43+00:00" + }, + { + "name": "brianium/paratest", + "version": "v7.8.3", + "source": { + "type": "git", + "url": "https://github.com/paratestphp/paratest.git", + "reference": "a585c346ddf1bec22e51e20b5387607905604a71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/a585c346ddf1bec22e51e20b5387607905604a71", + "reference": "a585c346ddf1bec22e51e20b5387607905604a71", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.2.0", + "jean85/pretty-package-versions": "^2.1.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "phpunit/php-code-coverage": "^11.0.9 || ^12.0.4", + "phpunit/php-file-iterator": "^5.1.0 || ^6", + "phpunit/php-timer": "^7.0.1 || ^8", + "phpunit/phpunit": "^11.5.11 || ^12.0.6", + "sebastian/environment": "^7.2.0 || ^8", + "symfony/console": "^6.4.17 || ^7.2.1", + "symfony/process": "^6.4.19 || ^7.2.4" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.6", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2.0.3", + "squizlabs/php_codesniffer": "^3.11.3", + "symfony/filesystem": "^6.4.13 || ^7.2.0" + }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], + "type": "library", + "autoload": { + "psr-4": { + "ParaTest\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" + } + ], + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", + "keywords": [ + "concurrent", + "parallel", + "phpunit", + "testing" + ], + "support": { + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.8.3" + }, + "funding": [ + { + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" + }, + { + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2025-03-05T08:29:11+00:00" + }, + { + "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": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" + } + ], + "time": "2024-08-06T10:04:20+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.3", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "59a123a3d459c5a23055802237cb317f609867e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", + "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.3" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-06-16T00:02:10+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.23.0", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "9ab851dba4faa51a3c3223dd3d07044129021024" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/9ab851dba4faa51a3c3223dd3d07044129021024", + "reference": "9ab851dba4faa51a3c3223dd3d07044129021024", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.76.0", + "illuminate/view": "^11.45.1", + "larastan/larastan": "^3.5.0", + "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": { + "files": [ + "overrides/Runner/Parallel/ProcessFactory.php" + ], + "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-07-03T10:37:47+00:00" + }, + { + "name": "laravel/sail", + "version": "v1.43.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/sail.git", + "reference": "3e7d899232a8c5e3ea4fc6dee7525ad583887e72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/sail/zipball/3e7d899232a8c5e3ea4fc6dee7525ad583887e72", + "reference": "3e7d899232a8c5e3ea4fc6dee7525ad583887e72", + "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-05-19T13:19:21+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.3", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "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.3" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-07-05T12:25:42+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": "pestphp/pest", + "version": "v3.8.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "c6244a8712968dbac88eb998e7ff3b5caa556b0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/c6244a8712968dbac88eb998e7ff3b5caa556b0d", + "reference": "c6244a8712968dbac88eb998e7ff3b5caa556b0d", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.8.3", + "nunomaduro/collision": "^8.8.0", + "nunomaduro/termwind": "^2.3.0", + "pestphp/pest-plugin": "^3.0.0", + "pestphp/pest-plugin-arch": "^3.1.0", + "pestphp/pest-plugin-mutate": "^3.0.5", + "php": "^8.2.0", + "phpunit/phpunit": "^11.5.15" + }, + "conflict": { + "filp/whoops": "<2.16.0", + "phpunit/phpunit": ">11.5.15", + "sebastian/exporter": "<6.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^3.4.0", + "pestphp/pest-plugin-type-coverage": "^3.5.0", + "symfony/process": "^7.2.5" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v3.8.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-04-17T10:53:02+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e79b26c65bc11c41093b10150c1341cc5cdbea83", + "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.2" + }, + "conflict": { + "pestphp/pest": "<3.0.0" + }, + "require-dev": { + "composer/composer": "^2.7.9", + "pestphp/pest": "^3.0.0", + "pestphp/pest-dev-tools": "^3.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", + "keywords": [ + "framework", + "manager", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin/tree/v3.0.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2024-09-08T23:21:41+00:00" + }, + { + "name": "pestphp/pest-plugin-arch", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/db7bd9cb1612b223e16618d85475c6f63b9c8daa", + "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa", + "shasum": "" + }, + "require": { + "pestphp/pest-plugin": "^3.0.0", + "php": "^8.2", + "ta-tikoma/phpunit-architecture-test": "^0.8.4" + }, + "require-dev": { + "pestphp/pest": "^3.8.1", + "pestphp/pest-dev-tools": "^3.4.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Arch\\Plugin" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Arch\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Arch plugin for Pest PHP.", + "keywords": [ + "arch", + "architecture", + "framework", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-04-16T22:59:48+00:00" + }, + { + "name": "pestphp/pest-plugin-laravel", + "version": "v3.2.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-laravel.git", + "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/6801be82fd92b96e82dd72e563e5674b1ce365fc", + "reference": "6801be82fd92b96e82dd72e563e5674b1ce365fc", + "shasum": "" + }, + "require": { + "laravel/framework": "^11.39.1|^12.9.2", + "pestphp/pest": "^3.8.2", + "php": "^8.2.0" + }, + "require-dev": { + "laravel/dusk": "^8.2.13|dev-develop", + "orchestra/testbench": "^9.9.0|^10.2.1", + "pestphp/pest-dev-tools": "^3.4.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Laravel\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Pest\\Laravel\\PestServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Pest\\Laravel\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest Laravel Plugin", + "keywords": [ + "framework", + "laravel", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v3.2.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-04-21T07:40:53+00:00" + }, + { + "name": "pestphp/pest-plugin-mutate", + "version": "v3.0.5", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.2.0", + "pestphp/pest-plugin": "^3.0.0", + "php": "^8.2", + "psr/simple-cache": "^3.0.0" + }, + "require-dev": { + "pestphp/pest": "^3.0.8", + "pestphp/pest-dev-tools": "^3.0.0", + "pestphp/pest-plugin-type-coverage": "^3.0.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Pest\\Mutate\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", + "keywords": [ + "framework", + "mutate", + "mutation", + "pest", + "php", + "plugin", + "test", + "testing", + "unit" + ], + "support": { + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v3.0.5" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/gehrisandro", + "type": "github" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2024-09-22T07:54:40+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": "php-debugbar/php-debugbar", + "version": "v2.2.4", + "source": { + "type": "git", + "url": "https://github.com/php-debugbar/php-debugbar.git", + "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/3146d04671f51f69ffec2a4207ac3bdcf13a9f35", + "reference": "3146d04671f51f69ffec2a4207ac3bdcf13a9f35", + "shasum": "" + }, + "require": { + "php": "^8", + "psr/log": "^1|^2|^3", + "symfony/var-dumper": "^4|^5|^6|^7" + }, + "replace": { + "maximebf/debugbar": "self.version" + }, + "require-dev": { + "dbrekelmans/bdi": "^1", + "phpunit/phpunit": "^8|^9", + "symfony/panther": "^1|^2.1", + "twig/twig": "^1.38|^2.7|^3.0" + }, + "suggest": { + "kriswallsmith/assetic": "The best way to manage assets", + "monolog/monolog": "Log using Monolog", + "predis/predis": "Redis storage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "psr-4": { + "DebugBar\\": "src/DebugBar/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maxime Bouroumeau-Fuseau", + "email": "maxime.bouroumeau@gmail.com", + "homepage": "http://maximebf.com" + }, + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "Debug bar in the browser for php application", + "homepage": "https://github.com/php-debugbar/php-debugbar", + "keywords": [ + "debug", + "debug bar", + "debugbar", + "dev" + ], + "support": { + "issues": "https://github.com/php-debugbar/php-debugbar/issues", + "source": "https://github.com/php-debugbar/php-debugbar/tree/v2.2.4" + }, + "time": "2025-07-22T14:01:30+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + }, + "time": "2025-04-13T19:20:35+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + }, + "time": "2025-02-19T13:28:12+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "11.0.10", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", + "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/show" + }, + "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-06-18T08:56:18+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.15", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", + "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.0", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.9", + "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.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.2", + "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.15" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", + "type": "tidelift" + } + ], + "time": "2025-03-23T16:02:11+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.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "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.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-07T06:57:01+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.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "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" + }, + { + "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.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-07-03T05:10:34+00:00" + }, + { + "name": "sebastian/type", + "version": "5.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "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.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-03-18T13:35:50+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.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/0c3555045a46ab3cd4cc5a69d161225195230edb", + "reference": "0c3555045a46ab3cd4cc5a69d161225195230edb", + "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.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-03T06:57:57+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.5", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "cf6fb197b676ba716837c886baca842e4db29005" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005", + "reference": "cf6fb197b676ba716837c886baca842e4db29005", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5" + }, + "time": "2025-04-20T20:23:40+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/config/app.php b/config/app.php new file mode 100644 index 000000000..fa607ccc5 --- /dev/null +++ b/config/app.php @@ -0,0 +1,140 @@ + env('APP_NAME', isSaas() ? 'HRM SaaS' : 'HRM'), + + /* + |-------------------------------------------------------------------------- + | 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), + + /* + |-------------------------------------------------------------------------- + | Demo Mode + |-------------------------------------------------------------------------- + | + | This value determines if the application is running in demo mode. + | When enabled, certain destructive operations will be restricted. + | + */ + + 'is_demo' => (bool) env('IS_DEMO', false), + + 'is_saas' => (bool) env('IS_SAAS', 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(',', 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/config/auth.php b/config/auth.php new file mode 100644 index 000000000..0ba5d5d8f --- /dev/null +++ b/config/auth.php @@ -0,0 +1,115 @@ + [ + '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', + ], + ], + + /* + |-------------------------------------------------------------------------- + | 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', App\Models\User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | 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 amount 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), + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 000000000..d13a63120 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,108 @@ + env('CACHE_STORE', 'file'), + + /* + |-------------------------------------------------------------------------- + | 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(env('APP_NAME', 'laravel'), '_').'_cache_'), + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 000000000..8910562d6 --- /dev/null +++ b/config/database.php @@ -0,0 +1,174 @@ + 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, + ], + + '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(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'), + ], + + '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'), + ], + + ], + +]; diff --git a/config/dateformat.php b/config/dateformat.php new file mode 100644 index 000000000..24103e6e1 --- /dev/null +++ b/config/dateformat.php @@ -0,0 +1,32 @@ + 'Jan 1, 2025', + 'd-m-Y' => '01-01-2025', + 'm-d-Y' => '01-01-2025', + 'Y-m-d' => '2025-01-01', + 'd M, Y' => '01 Jan, 2025', + 'd F, Y' => '01 January, 2025', + 'F j, Y' => 'January 1, 2025', + 'j F Y' => '1 January 2025', + 'D, M j, Y' => 'Thu, Jan 1, 2025', + 'l, F j, Y' => 'Thursday, January 1, 2025', + 'd/m/Y' => '01/01/2025', + 'm/d/Y' => '01/01/2025', + 'Y/m/d' => '2025/01/01', + 'd.m.Y' => '01.01.2025', + 'j M Y' => '1 Jan 2025', + 'D M j Y' => 'Thu Jan 1 2025', + 'd-M-Y' => '01-Jan-2025', + 'jS M Y' => '1st Jan 2025', + 'jS F Y' => '1st January 2025', + 'Ymd' => '20250101', + 'd M Y' => '01 Jan 2025', + 'M d, Y' => 'Jan 01, 2025', + 'M d Y' => 'Jan 01 2025', + 'd F Y' => '01 January 2025', + 'l, j F Y' => 'Thursday, 1 January 2025', + 'D, d M Y' => 'Thu, 01 Jan 2025', + 'Y.m.d' => '2025.01.01', + 'l jS \of F Y' => 'Thursday 1st of January 2025', +]; diff --git a/config/debugbar.php b/config/debugbar.php new file mode 100644 index 000000000..8ee60a600 --- /dev/null +++ b/config/debugbar.php @@ -0,0 +1,338 @@ + env('DEBUGBAR_ENABLED', null), + 'hide_empty_tabs' => env('DEBUGBAR_HIDE_EMPTY_TABS', true), // Hide tabs until they have content + 'except' => [ + 'telescope*', + 'horizon*', + ], + + /* + |-------------------------------------------------------------------------- + | Storage settings + |-------------------------------------------------------------------------- + | + | Debugbar stores data for session/ajax requests. + | You can disable this, so the debugbar stores data in headers/session, + | but this can cause problems with large data collectors. + | By default, file storage (in the storage folder) is used. Redis and PDO + | can also be used. For PDO, run the package migrations first. + | + | Warning: Enabling storage.open will allow everyone to access previous + | request, do not enable open storage in publicly available environments! + | Specify a callback if you want to limit based on IP or authentication. + | Leaving it to null will allow localhost only. + */ + 'storage' => [ + 'enabled' => env('DEBUGBAR_STORAGE_ENABLED', true), + 'open' => env('DEBUGBAR_OPEN_STORAGE'), // bool/callback. + 'driver' => env('DEBUGBAR_STORAGE_DRIVER', 'file'), // redis, file, pdo, socket, custom + 'path' => env('DEBUGBAR_STORAGE_PATH', storage_path('debugbar')), // For file driver + 'connection' => env('DEBUGBAR_STORAGE_CONNECTION', null), // Leave null for default connection (Redis/PDO) + 'provider' => env('DEBUGBAR_STORAGE_PROVIDER', ''), // Instance of StorageInterface for custom driver + 'hostname' => env('DEBUGBAR_STORAGE_HOSTNAME', '127.0.0.1'), // Hostname to use with the "socket" driver + 'port' => env('DEBUGBAR_STORAGE_PORT', 2304), // Port to use with the "socket" driver + ], + + /* + |-------------------------------------------------------------------------- + | Editor + |-------------------------------------------------------------------------- + | + | Choose your preferred editor to use when clicking file name. + | + | Supported: "phpstorm", "vscode", "vscode-insiders", "vscode-remote", + | "vscode-insiders-remote", "vscodium", "textmate", "emacs", + | "sublime", "atom", "nova", "macvim", "idea", "netbeans", + | "xdebug", "espresso" + | + */ + + 'editor' => env('DEBUGBAR_EDITOR') ?: env('IGNITION_EDITOR', 'phpstorm'), + + /* + |-------------------------------------------------------------------------- + | Remote Path Mapping + |-------------------------------------------------------------------------- + | + | If you are using a remote dev server, like Laravel Homestead, Docker, or + | even a remote VPS, it will be necessary to specify your path mapping. + | + | Leaving one, or both of these, empty or null will not trigger the remote + | URL changes and Debugbar will treat your editor links as local files. + | + | "remote_sites_path" is an absolute base path for your sites or projects + | in Homestead, Vagrant, Docker, or another remote development server. + | + | Example value: "/home/vagrant/Code" + | + | "local_sites_path" is an absolute base path for your sites or projects + | on your local computer where your IDE or code editor is running on. + | + | Example values: "/Users//Code", "C:\Users\\Documents\Code" + | + */ + + 'remote_sites_path' => env('DEBUGBAR_REMOTE_SITES_PATH'), + 'local_sites_path' => env('DEBUGBAR_LOCAL_SITES_PATH', env('IGNITION_LOCAL_SITES_PATH')), + + /* + |-------------------------------------------------------------------------- + | Vendors + |-------------------------------------------------------------------------- + | + | Vendor files are included by default, but can be set to false. + | This can also be set to 'js' or 'css', to only include javascript or css vendor files. + | Vendor files are for css: font-awesome (including fonts) and highlight.js (css files) + | and for js: jquery and highlight.js + | So if you want syntax highlighting, set it to true. + | jQuery is set to not conflict with existing jQuery scripts. + | + */ + + 'include_vendors' => env('DEBUGBAR_INCLUDE_VENDORS', true), + + /* + |-------------------------------------------------------------------------- + | Capture Ajax Requests + |-------------------------------------------------------------------------- + | + | The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors), + | you can use this option to disable sending the data through the headers. + | + | Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools. + | + | Note for your request to be identified as ajax requests they must either send the header + | X-Requested-With with the value XMLHttpRequest (most JS libraries send this), or have application/json as a Accept header. + | + | By default `ajax_handler_auto_show` is set to true allowing ajax requests to be shown automatically in the Debugbar. + | Changing `ajax_handler_auto_show` to false will prevent the Debugbar from reloading. + | + | You can defer loading the dataset, so it will be loaded with ajax after the request is done. (Experimental) + */ + + 'capture_ajax' => env('DEBUGBAR_CAPTURE_AJAX', true), + 'add_ajax_timing' => env('DEBUGBAR_ADD_AJAX_TIMING', false), + 'ajax_handler_auto_show' => env('DEBUGBAR_AJAX_HANDLER_AUTO_SHOW', true), + 'ajax_handler_enable_tab' => env('DEBUGBAR_AJAX_HANDLER_ENABLE_TAB', true), + 'defer_datasets' => env('DEBUGBAR_DEFER_DATASETS', false), + /* + |-------------------------------------------------------------------------- + | Custom Error Handler for Deprecated warnings + |-------------------------------------------------------------------------- + | + | When enabled, the Debugbar shows deprecated warnings for Symfony components + | in the Messages tab. + | + */ + 'error_handler' => env('DEBUGBAR_ERROR_HANDLER', false), + + /* + |-------------------------------------------------------------------------- + | Clockwork integration + |-------------------------------------------------------------------------- + | + | The Debugbar can emulate the Clockwork headers, so you can use the Chrome + | Extension, without the server-side code. It uses Debugbar collectors instead. + | + */ + 'clockwork' => env('DEBUGBAR_CLOCKWORK', false), + + /* + |-------------------------------------------------------------------------- + | DataCollectors + |-------------------------------------------------------------------------- + | + | Enable/disable DataCollectors + | + */ + + 'collectors' => [ + 'phpinfo' => env('DEBUGBAR_COLLECTORS_PHPINFO', false), // Php version + 'messages' => env('DEBUGBAR_COLLECTORS_MESSAGES', true), // Messages + 'time' => env('DEBUGBAR_COLLECTORS_TIME', true), // Time Datalogger + 'memory' => env('DEBUGBAR_COLLECTORS_MEMORY', true), // Memory usage + 'exceptions' => env('DEBUGBAR_COLLECTORS_EXCEPTIONS', true), // Exception displayer + 'log' => env('DEBUGBAR_COLLECTORS_LOG', true), // Logs from Monolog (merged in messages if enabled) + 'db' => env('DEBUGBAR_COLLECTORS_DB', true), // Show database (PDO) queries and bindings + 'views' => env('DEBUGBAR_COLLECTORS_VIEWS', true), // Views with their data + 'route' => env('DEBUGBAR_COLLECTORS_ROUTE', false), // Current route information + 'auth' => env('DEBUGBAR_COLLECTORS_AUTH', false), // Display Laravel authentication status + 'gate' => env('DEBUGBAR_COLLECTORS_GATE', true), // Display Laravel Gate checks + 'session' => env('DEBUGBAR_COLLECTORS_SESSION', false), // Display session data + 'symfony_request' => env('DEBUGBAR_COLLECTORS_SYMFONY_REQUEST', true), // Only one can be enabled.. + 'mail' => env('DEBUGBAR_COLLECTORS_MAIL', true), // Catch mail messages + 'laravel' => env('DEBUGBAR_COLLECTORS_LARAVEL', true), // Laravel version and environment + 'events' => env('DEBUGBAR_COLLECTORS_EVENTS', false), // All events fired + 'default_request' => env('DEBUGBAR_COLLECTORS_DEFAULT_REQUEST', false), // Regular or special Symfony request logger + 'logs' => env('DEBUGBAR_COLLECTORS_LOGS', false), // Add the latest log messages + 'files' => env('DEBUGBAR_COLLECTORS_FILES', false), // Show the included files + 'config' => env('DEBUGBAR_COLLECTORS_CONFIG', false), // Display config settings + 'cache' => env('DEBUGBAR_COLLECTORS_CACHE', false), // Display cache events + 'models' => env('DEBUGBAR_COLLECTORS_MODELS', true), // Display models + 'livewire' => env('DEBUGBAR_COLLECTORS_LIVEWIRE', true), // Display Livewire (when available) + 'jobs' => env('DEBUGBAR_COLLECTORS_JOBS', false), // Display dispatched jobs + 'pennant' => env('DEBUGBAR_COLLECTORS_PENNANT', false), // Display Pennant feature flags + ], + + /* + |-------------------------------------------------------------------------- + | Extra options + |-------------------------------------------------------------------------- + | + | Configure some DataCollectors + | + */ + + 'options' => [ + 'time' => [ + 'memory_usage' => env('DEBUGBAR_OPTIONS_TIME_MEMORY_USAGE', false), // Calculated by subtracting memory start and end, it may be inaccurate + ], + 'messages' => [ + 'trace' => env('DEBUGBAR_OPTIONS_MESSAGES_TRACE', true), // Trace the origin of the debug message + 'capture_dumps' => env('DEBUGBAR_OPTIONS_MESSAGES_CAPTURE_DUMPS', false), // Capture laravel `dump();` as message + ], + 'memory' => [ + 'reset_peak' => env('DEBUGBAR_OPTIONS_MEMORY_RESET_PEAK', false), // run memory_reset_peak_usage before collecting + 'with_baseline' => env('DEBUGBAR_OPTIONS_MEMORY_WITH_BASELINE', false), // Set boot memory usage as memory peak baseline + 'precision' => (int) env('DEBUGBAR_OPTIONS_MEMORY_PRECISION', 0), // Memory rounding precision + ], + 'auth' => [ + 'show_name' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_NAME', true), // Also show the users name/email in the debugbar + 'show_guards' => env('DEBUGBAR_OPTIONS_AUTH_SHOW_GUARDS', true), // Show the guards that are used + ], + 'gate' => [ + 'trace' => false, // Trace the origin of the Gate checks + ], + 'db' => [ + 'with_params' => env('DEBUGBAR_OPTIONS_WITH_PARAMS', true), // Render SQL with the parameters substituted + 'exclude_paths' => [ // Paths to exclude entirely from the collector + //'vendor/laravel/framework/src/Illuminate/Session', // Exclude sessions queries + ], + 'backtrace' => env('DEBUGBAR_OPTIONS_DB_BACKTRACE', true), // Use a backtrace to find the origin of the query in your files. + 'backtrace_exclude_paths' => [], // Paths to exclude from backtrace. (in addition to defaults) + 'timeline' => env('DEBUGBAR_OPTIONS_DB_TIMELINE', false), // Add the queries to the timeline + 'duration_background' => env('DEBUGBAR_OPTIONS_DB_DURATION_BACKGROUND', true), // Show shaded background on each query relative to how long it took to execute. + 'explain' => [ // Show EXPLAIN output on queries + 'enabled' => env('DEBUGBAR_OPTIONS_DB_EXPLAIN_ENABLED', false), + ], + 'hints' => env('DEBUGBAR_OPTIONS_DB_HINTS', false), // Show hints for common mistakes + 'show_copy' => env('DEBUGBAR_OPTIONS_DB_SHOW_COPY', true), // Show copy button next to the query, + 'slow_threshold' => env('DEBUGBAR_OPTIONS_DB_SLOW_THRESHOLD', false), // Only track queries that last longer than this time in ms + 'memory_usage' => env('DEBUGBAR_OPTIONS_DB_MEMORY_USAGE', false), // Show queries memory usage + 'soft_limit' => (int) env('DEBUGBAR_OPTIONS_DB_SOFT_LIMIT', 100), // After the soft limit, no parameters/backtrace are captured + 'hard_limit' => (int) env('DEBUGBAR_OPTIONS_DB_HARD_LIMIT', 500), // After the hard limit, queries are ignored + ], + 'mail' => [ + 'timeline' => env('DEBUGBAR_OPTIONS_MAIL_TIMELINE', true), // Add mails to the timeline + 'show_body' => env('DEBUGBAR_OPTIONS_MAIL_SHOW_BODY', true), + ], + 'views' => [ + 'timeline' => env('DEBUGBAR_OPTIONS_VIEWS_TIMELINE', true), // Add the views to the timeline + 'data' => env('DEBUGBAR_OPTIONS_VIEWS_DATA', false), // True for all data, 'keys' for only names, false for no parameters. + 'group' => (int) env('DEBUGBAR_OPTIONS_VIEWS_GROUP', 50), // Group duplicate views. Pass value to auto-group, or true/false to force + 'inertia_pages' => env('DEBUGBAR_OPTIONS_VIEWS_INERTIA_PAGES', 'js/Pages'), // Path for Inertia views + 'exclude_paths' => [ // Add the paths which you don't want to appear in the views + 'vendor/filament' // Exclude Filament components by default + ], + ], + 'route' => [ + 'label' => env('DEBUGBAR_OPTIONS_ROUTE_LABEL', true), // Show complete route on bar + ], + 'session' => [ + 'hiddens' => [], // Hides sensitive values using array paths + ], + 'symfony_request' => [ + 'label' => env('DEBUGBAR_OPTIONS_SYMFONY_REQUEST_LABEL', true), // Show route on bar + 'hiddens' => [], // Hides sensitive values using array paths, example: request_request.password + ], + 'events' => [ + 'data' => env('DEBUGBAR_OPTIONS_EVENTS_DATA', false), // Collect events data, listeners + 'excluded' => [], // Example: ['eloquent.*', 'composing', Illuminate\Cache\Events\CacheHit::class] + ], + 'logs' => [ + 'file' => env('DEBUGBAR_OPTIONS_LOGS_FILE', null), + ], + 'cache' => [ + 'values' => env('DEBUGBAR_OPTIONS_CACHE_VALUES', true), // Collect cache values + ], + ], + + /* + |-------------------------------------------------------------------------- + | Inject Debugbar in Response + |-------------------------------------------------------------------------- + | + | Usually, the debugbar is added just before , by listening to the + | Response after the App is done. If you disable this, you have to add them + | in your template yourself. See http://phpdebugbar.com/docs/rendering.html + | + */ + + 'inject' => env('DEBUGBAR_INJECT', true), + + /* + |-------------------------------------------------------------------------- + | Debugbar route prefix + |-------------------------------------------------------------------------- + | + | Sometimes you want to set route prefix to be used by Debugbar to load + | its resources from. Usually the need comes from misconfigured web server or + | from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97 + | + */ + 'route_prefix' => env('DEBUGBAR_ROUTE_PREFIX', '_debugbar'), + + /* + |-------------------------------------------------------------------------- + | Debugbar route middleware + |-------------------------------------------------------------------------- + | + | Additional middleware to run on the Debugbar routes + */ + 'route_middleware' => [], + + /* + |-------------------------------------------------------------------------- + | Debugbar route domain + |-------------------------------------------------------------------------- + | + | By default Debugbar route served from the same domain that request served. + | To override default domain, specify it as a non-empty value. + */ + 'route_domain' => env('DEBUGBAR_ROUTE_DOMAIN', null), + + /* + |-------------------------------------------------------------------------- + | Debugbar theme + |-------------------------------------------------------------------------- + | + | Switches between light and dark theme. If set to auto it will respect system preferences + | Possible values: auto, light, dark + */ + 'theme' => env('DEBUGBAR_THEME', 'auto'), + + /* + |-------------------------------------------------------------------------- + | Backtrace stack limit + |-------------------------------------------------------------------------- + | + | By default, the Debugbar limits the number of frames returned by the 'debug_backtrace()' function. + | If you need larger stacktraces, you can increase this number. Setting it to 0 will result in no limit. + */ + 'debug_backtrace_limit' => (int) env('DEBUGBAR_DEBUG_BACKTRACE_LIMIT', 50), +]; diff --git a/config/dompdf.php b/config/dompdf.php new file mode 100644 index 000000000..35eef8ff6 --- /dev/null +++ b/config/dompdf.php @@ -0,0 +1,301 @@ + false, // Throw an Exception on warnings from dompdf + + 'public_path' => null, // Override the public path if needed + + /* + * Dejavu Sans font is missing glyphs for converted entities, turn it off if you need to show € and £. + */ + 'convert_entities' => true, + + 'options' => [ + /** + * The location of the DOMPDF font directory + * + * The location of the directory where DOMPDF will store fonts and font metrics + * Note: This directory must exist and be writable by the webserver process. + * *Please note the trailing slash.* + * + * Notes regarding fonts: + * Additional .afm font metrics can be added by executing load_font.php from command line. + * + * Only the original "Base 14 fonts" are present on all pdf viewers. Additional fonts must + * be embedded in the pdf file or the PDF may not display correctly. This can significantly + * increase file size unless font subsetting is enabled. Before embedding a font please + * review your rights under the font license. + * + * Any font specification in the source HTML is translated to the closest font available + * in the font directory. + * + * The pdf standard "Base 14 fonts" are: + * Courier, Courier-Bold, Courier-BoldOblique, Courier-Oblique, + * Helvetica, Helvetica-Bold, Helvetica-BoldOblique, Helvetica-Oblique, + * Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic, + * Symbol, ZapfDingbats. + */ + 'font_dir' => storage_path('fonts'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782) + + /** + * The location of the DOMPDF font cache directory + * + * This directory contains the cached font metrics for the fonts used by DOMPDF. + * This directory can be the same as DOMPDF_FONT_DIR + * + * Note: This directory must exist and be writable by the webserver process. + */ + 'font_cache' => storage_path('fonts'), + + /** + * The location of a temporary directory. + * + * The directory specified must be writeable by the webserver process. + * The temporary directory is required to download remote images and when + * using the PDFLib back end. + */ + 'temp_dir' => sys_get_temp_dir(), + + /** + * ==== IMPORTANT ==== + * + * dompdf's "chroot": Prevents dompdf from accessing system files or other + * files on the webserver. All local files opened by dompdf must be in a + * subdirectory of this directory. DO NOT set it to '/' since this could + * allow an attacker to use dompdf to read any files on the server. This + * should be an absolute path. + * This is only checked on command line call by dompdf.php, but not by + * direct class use like: + * $dompdf = new DOMPDF(); $dompdf->load_html($htmldata); $dompdf->render(); $pdfdata = $dompdf->output(); + */ + 'chroot' => realpath(base_path()), + + /** + * Protocol whitelist + * + * Protocols and PHP wrappers allowed in URIs, and the validation rules + * that determine if a resouce may be loaded. Full support is not guaranteed + * for the protocols/wrappers specified + * by this array. + * + * @var array + */ + 'allowed_protocols' => [ + 'data://' => ['rules' => []], + 'file://' => ['rules' => []], + 'http://' => ['rules' => []], + 'https://' => ['rules' => []], + ], + + /** + * Operational artifact (log files, temporary files) path validation + */ + 'artifactPathValidation' => null, + + /** + * @var string + */ + 'log_output_file' => null, + + /** + * Whether to enable font subsetting or not. + */ + 'enable_font_subsetting' => false, + + /** + * The PDF rendering backend to use + * + * Valid settings are 'PDFLib', 'CPDF' (the bundled R&OS PDF class), 'GD' and + * 'auto'. 'auto' will look for PDFLib and use it if found, or if not it will + * fall back on CPDF. 'GD' renders PDFs to graphic files. + * {@link * Canvas_Factory} ultimately determines which rendering class to + * instantiate based on this setting. + * + * Both PDFLib & CPDF rendering backends provide sufficient rendering + * capabilities for dompdf, however additional features (e.g. object, + * image and font support, etc.) differ between backends. Please see + * {@link PDFLib_Adapter} for more information on the PDFLib backend + * and {@link CPDF_Adapter} and lib/class.pdf.php for more information + * on CPDF. Also see the documentation for each backend at the links + * below. + * + * The GD rendering backend is a little different than PDFLib and + * CPDF. Several features of CPDF and PDFLib are not supported or do + * not make any sense when creating image files. For example, + * multiple pages are not supported, nor are PDF 'objects'. Have a + * look at {@link GD_Adapter} for more information. GD support is + * experimental, so use it at your own risk. + * + * @link http://www.pdflib.com + * @link http://www.ros.co.nz/pdf + * @link http://www.php.net/image + */ + 'pdf_backend' => 'CPDF', + + /** + * html target media view which should be rendered into pdf. + * List of types and parsing rules for future extensions: + * http://www.w3.org/TR/REC-html40/types.html + * screen, tty, tv, projection, handheld, print, braille, aural, all + * Note: aural is deprecated in CSS 2.1 because it is replaced by speech in CSS 3. + * Note, even though the generated pdf file is intended for print output, + * the desired content might be different (e.g. screen or projection view of html file). + * Therefore allow specification of content here. + */ + 'default_media_type' => 'screen', + + /** + * The default paper size. + * + * North America standard is "letter"; other countries generally "a4" + * + * @see CPDF_Adapter::PAPER_SIZES for valid sizes ('letter', 'legal', 'A4', etc.) + */ + 'default_paper_size' => 'a4', + + /** + * The default paper orientation. + * + * The orientation of the page (portrait or landscape). + * + * @var string + */ + 'default_paper_orientation' => 'portrait', + + /** + * The default font family + * + * Used if no suitable fonts can be found. This must exist in the font folder. + * + * @var string + */ + 'default_font' => 'serif', + + /** + * Image DPI setting + * + * This setting determines the default DPI setting for images and fonts. The + * DPI may be overridden for inline images by explictly setting the + * image's width & height style attributes (i.e. if the image's native + * width is 600 pixels and you specify the image's width as 72 points, + * the image will have a DPI of 600 in the rendered PDF. The DPI of + * background images can not be overridden and is controlled entirely + * via this parameter. + * + * For the purposes of DOMPDF, pixels per inch (PPI) = dots per inch (DPI). + * If a size in html is given as px (or without unit as image size), + * this tells the corresponding size in pt. + * This adjusts the relative sizes to be similar to the rendering of the + * html page in a reference browser. + * + * In pdf, always 1 pt = 1/72 inch + * + * Rendering resolution of various browsers in px per inch: + * Windows Firefox and Internet Explorer: + * SystemControl->Display properties->FontResolution: Default:96, largefonts:120, custom:? + * Linux Firefox: + * about:config *resolution: Default:96 + * (xorg screen dimension in mm and Desktop font dpi settings are ignored) + * + * Take care about extra font/image zoom factor of browser. + * + * In images, size in pixel attribute, img css style, are overriding + * the real image dimension in px for rendering. + * + * @var int + */ + 'dpi' => 96, + + /** + * Enable embedded PHP + * + * If this setting is set to true then DOMPDF will automatically evaluate embedded PHP contained + * within tags. + * + * ==== IMPORTANT ==== Enabling this for documents you do not trust (e.g. arbitrary remote html pages) + * is a security risk. + * Embedded scripts are run with the same level of system access available to dompdf. + * Set this option to false (recommended) if you wish to process untrusted documents. + * This setting may increase the risk of system exploit. + * Do not change this settings without understanding the consequences. + * Additional documentation is available on the dompdf wiki at: + * https://github.com/dompdf/dompdf/wiki + * + * @var bool + */ + 'enable_php' => false, + + /** + * Rnable inline JavaScript + * + * If this setting is set to true then DOMPDF will automatically insert JavaScript code contained + * within tags as written into the PDF. + * NOTE: This is PDF-based JavaScript to be executed by the PDF viewer, + * not browser-based JavaScript executed by Dompdf. + * + * @var bool + */ + 'enable_javascript' => true, + + /** + * Enable remote file access + * + * If this setting is set to true, DOMPDF will access remote sites for + * images and CSS files as required. + * + * ==== IMPORTANT ==== + * This can be a security risk, in particular in combination with isPhpEnabled and + * allowing remote html code to be passed to $dompdf = new DOMPDF(); $dompdf->load_html(...); + * This allows anonymous users to download legally doubtful internet content which on + * tracing back appears to being downloaded by your server, or allows malicious php code + * in remote html pages to be executed by your server with your account privileges. + * + * This setting may increase the risk of system exploit. Do not change + * this settings without understanding the consequences. Additional + * documentation is available on the dompdf wiki at: + * https://github.com/dompdf/dompdf/wiki + * + * @var bool + */ + 'enable_remote' => false, + + /** + * List of allowed remote hosts + * + * Each value of the array must be a valid hostname. + * + * This will be used to filter which resources can be loaded in combination with + * isRemoteEnabled. If enable_remote is FALSE, then this will have no effect. + * + * Leave to NULL to allow any remote host. + * + * @var array|null + */ + 'allowed_remote_hosts' => null, + + /** + * A ratio applied to the fonts height to be more like browsers' line height + */ + 'font_height_ratio' => 1.1, + + /** + * Use the HTML5 Lib parser + * + * @deprecated This feature is now always on in dompdf 2.x + * + * @var bool + */ + 'enable_html5_parser' => true, + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 000000000..54e16b325 --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'public'), + + /* + |-------------------------------------------------------------------------- + | 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/config/inertia.php b/config/inertia.php new file mode 100644 index 000000000..f75e7b886 --- /dev/null +++ b/config/inertia.php @@ -0,0 +1,55 @@ + [ + 'enabled' => true, + 'url' => 'http://127.0.0.1:13714', + // 'bundle' => base_path('bootstrap/ssr/ssr.mjs'), + + ], + + /* + |-------------------------------------------------------------------------- + | Testing + |-------------------------------------------------------------------------- + | + | The values described here are used to locate Inertia components on the + | filesystem. For instance, when using `assertInertia`, the assertion + | attempts to locate the component as a file relative to the paths. + | + */ + + 'testing' => [ + + 'ensure_pages_exist' => true, + + 'page_paths' => [ + resource_path('js/pages'), + ], + + 'page_extensions' => [ + 'js', + 'jsx', + 'svelte', + 'ts', + 'tsx', + 'vue', + ], + + ], + +]; diff --git a/config/installer.php b/config/installer.php new file mode 100644 index 000000000..8a63e5cc5 --- /dev/null +++ b/config/installer.php @@ -0,0 +1,150 @@ + [ + 'minPhpVersion' => '8.2.0', + ], + 'final' => [ + 'key' => true, + 'publish' => true, + ], + 'requirements' => [ + 'php' => [ + 'openssl', + 'pdo', + 'mbstring', + 'tokenizer', + 'JSON', + 'cURL', + 'fileinfo', + 'gd', + 'zip', + 'xml', + ], + 'apache' => [ + 'mod_rewrite', + ], + ], + + /* + |-------------------------------------------------------------------------- + | Folders Permissions + |-------------------------------------------------------------------------- + | + | This is the default Laravel folders permissions, if your application + | requires more permissions just add them to the array list bellow. + | + */ + 'permissions' => [ + 'storage/framework/' => '775', + 'storage/logs/' => '775', + 'storage/app/' => '775', + 'storage/app/public/' => '775', + 'bootstrap/cache/' => '775', + 'public/storage/' => '775', + 'storage/uploads/' => '775', + 'resources/lang/' => '775', + ], + + /* + |-------------------------------------------------------------------------- + | Environment Form Wizard Validation Rules & Messages + |-------------------------------------------------------------------------- + | + | This are the default form field validation rules. Available Rules: + | https://laravel.com/docs/5.4/validation#available-validation-rules + | + */ + 'environment' => [ + 'form' => [ + 'rules' => [ + 'app_name' => 'required|string|max:50', + 'environment' => 'required|string|max:50', + 'environment_custom' => 'required_if:environment,other|max:50', + 'app_debug' => 'required|string', + 'app_log_level' => 'required|string|max:50', + 'app_url' => 'required|url', + 'database_connection' => 'required|string|max:50', + 'database_hostname' => 'required|string|max:50', + 'database_port' => 'required|numeric', + 'database_name' => 'required|string|max:50', + 'database_username' => 'required|string|max:50', + 'database_password' => 'nullable|string|max:50', + 'broadcast_driver' => 'required|string|max:50', + 'cache_driver' => 'required|string|max:50', + 'session_driver' => 'required|string|max:50', + 'queue_driver' => 'required|string|max:50', + 'redis_hostname' => 'required|string|max:50', + 'redis_password' => 'required|string|max:50', + 'redis_port' => 'required|numeric', + 'mail_driver' => 'required|string|max:50', + 'mail_host' => 'required|string|max:50', + 'mail_port' => 'required|string|max:50', + 'mail_username' => 'required|string|max:50', + 'mail_password' => 'required|string|max:50', + 'mail_encryption' => 'required|string|max:50', + 'pusher_app_id' => 'max:50', + 'pusher_app_key' => 'max:50', + 'pusher_app_secret' => 'max:50', + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Installed Middleware Options + |-------------------------------------------------------------------------- + | Different available status switch configuration for the + | canInstall middleware located in `canInstall.php`. + | + */ + 'installed' => [ + 'redirectOptions' => [ + 'route' => [ + 'name' => 'dashboard', + 'data' => [], + ], + 'abort' => [ + 'type' => '404', + ], + 'dump' => [ + 'data' => 'Dumping a not found message.', + ], + ], + ], + + /* + |-------------------------------------------------------------------------- + | Selected Installed Middleware Option + |-------------------------------------------------------------------------- + | The selected option fo what happens when an installer instance has been + | Default output is to `/resources/views/error/404.blade.php` if none. + | The available middleware options include: + | route, abort, dump, 404, default, '' + | + */ + 'installedAlreadyAction' => 'route', + + /* + |-------------------------------------------------------------------------- + | Updater Enabled + |-------------------------------------------------------------------------- + | Can the application run the '/update' route with the migrations. + | The default option is set to False if none is present. + | Boolean value + | + */ + 'updaterEnabled' => 'true', + +]; diff --git a/config/larabug.php b/config/larabug.php new file mode 100644 index 000000000..adfb36f6e --- /dev/null +++ b/config/larabug.php @@ -0,0 +1,145 @@ + env('LB_KEY', ''), + + /* + |-------------------------------------------------------------------------- + | Project key + |-------------------------------------------------------------------------- + | + | This is your project key which you receive when creating a project + | Retrieve your key from https://www.larabug.com + | + */ + + 'project_key' => env('LB_PROJECT_KEY', ''), + + /* + |-------------------------------------------------------------------------- + | Environment setting + |-------------------------------------------------------------------------- + | + | This setting determines if the exception should be send over or not. + | + */ + + 'environments' => [ + 'production', + ], + + /* + |-------------------------------------------------------------------------- + | Project version + |-------------------------------------------------------------------------- + | + | Set the project version, default: null. + | For git repository: shell_exec("git log -1 --pretty=format:'%h' --abbrev-commit") + | + */ + 'project_version' => null, + + /* + |-------------------------------------------------------------------------- + | Lines near exception + |-------------------------------------------------------------------------- + | + | How many lines to show near exception line. The more you specify the bigger + | the displayed code will be. Max value can be 50, will be defaulted to + | 12 if higher than 50 automatically. + | + */ + + 'lines_count' => 12, + + /* + |-------------------------------------------------------------------------- + | Prevent duplicates + |-------------------------------------------------------------------------- + | + | Set the sleep time between duplicate exceptions. This value is in seconds, default: 60 seconds (1 minute) + | + */ + + 'sleep' => 60, + + /* + |-------------------------------------------------------------------------- + | Skip exceptions + |-------------------------------------------------------------------------- + | + | List of exceptions to skip sending. + | + */ + + 'except' => [ + 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException', + ], + + /* + |-------------------------------------------------------------------------- + | Key filtering + |-------------------------------------------------------------------------- + | + | Filter out these variables before sending them to LaraBug + | + */ + + 'blacklist' => [ + '*authorization*', + '*password*', + '*token*', + '*auth*', + '*verification*', + '*credit_card*', + 'cardToken', // mollie card token + '*cvv*', + '*iban*', + '*name*', + '*email*' + ], + + /* + |-------------------------------------------------------------------------- + | Release git hash + |-------------------------------------------------------------------------- + | + | + */ + + // 'release' => trim(exec('git --git-dir ' . base_path('.git') . ' log --pretty="%h" -n1 HEAD')), + + /* + |-------------------------------------------------------------------------- + | Server setting + |-------------------------------------------------------------------------- + | + | This setting allows you to change the server. + | + */ + + 'server' => env('LB_SERVER', 'https://www.larabug.com/api/log'), + + /* + |-------------------------------------------------------------------------- + | Verify SSL setting + |-------------------------------------------------------------------------- + | + | Enables / disables the SSL verification when sending exceptions to LaraBug + | Never turn SSL verification off on production instances + | + */ + 'verify_ssl' => env('LB_VERIFY_SSL', true), + +]; diff --git a/config/laravel-impersonate.php b/config/laravel-impersonate.php new file mode 100644 index 000000000..8e64fb2cc --- /dev/null +++ b/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' => 'web', + + /** + * 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' => '/dashboard', + + /** + * 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' => '/companies', + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 000000000..84ebeeb0e --- /dev/null +++ b/config/logging.php @@ -0,0 +1,141 @@ + 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' => ['single', 'larabug'], + '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, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + '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'), + ], + + 'PayTabs' => [ + 'driver' => 'single', + 'path' => storage_path('logs/paytabs.log'), + 'level' => 'info', + ], + 'larabug' => [ + 'driver' => 'larabug', + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 000000000..756305b3c --- /dev/null +++ b/config/mail.php @@ -0,0 +1,116 @@ + 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(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', + ], + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | 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/config/media-library.php b/config/media-library.php new file mode 100644 index 000000000..954b891b1 --- /dev/null +++ b/config/media-library.php @@ -0,0 +1,168 @@ + env('MEDIA_DISK', 'public'), + + /* + * The maximum file size of an item in bytes. + * Adding a larger file will result in an exception. + */ + 'max_file_size' => 1024 * 1024 * 10, // 10MB + + /* + * This is the class that is responsible for naming generated files. + */ + 'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, + + /* + * This is the class that is responsible for determining the path structure of generated files. + */ + 'path_generator' => App\PathGenerators\MediaPathGenerator::class, + + /* + * Here you can specify which path generator should be used for the given class. + */ + 'custom_path_generators' => [ + // Model::class => PathGenerator::class + // or + // 'model_morph_alias' => PathGenerator::class + ], + + /* + * When urls to files get generated, this class will be called. Use the default + * if your files are stored locally above the site root or on s3. + */ + 'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class, + + /* + * Whether to activate versioning when urls to files get generated. + * When activated, this attaches a ?v=xx query string to the URL. + */ + 'version_urls' => false, + + /* + * The class that contains the strategy for determining a media file's path. + */ + 'path_generator' => App\PathGenerators\MediaPathGenerator::class, + + /* + * Medialibrary will try to optimize all converted images by removing + * metadata and applying a little bit of compression. These are + * the optimizers that will be used by default. + */ + 'image_optimizers' => [ + Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [ + '-m85', // set maximum quality to 85% + '--strip-all', // this strips out all text information such as comments and EXIF data + '--all-progressive', // this will make sure the resulting image is a progressive one + ], + + Spatie\ImageOptimizer\Optimizers\Pngquant::class => [ + '--force', // required parameter for this package + ], + + Spatie\ImageOptimizer\Optimizers\Optipng::class => [ + '-i0', // this will result in a non-interlaced, progressive scanned image + '-o2', // this set the optimization level to two (multiple IDAT compression trials) + '-quiet', // required parameter for this package + ], + + Spatie\ImageOptimizer\Optimizers\Svgo::class => [ + '--disable=cleanupIDs', // disabling because it is known to cause troubles + ], + + Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [ + '-b', // required parameter for this package + '-O3', // this produces the slowest but best results + ], + + Spatie\ImageOptimizer\Optimizers\Cwebp::class => [ + '-m', '6', // for the slowest compression method in order to get the best compression. + '-pass', '10', // for maximizing the amount of analysis pass. + '-mt', // multithreading for some speed improvements. + '-q', '90', //quality factor that brings the least noticeable changes. + ], + ], + + /* + * These generators will be used to create an image of media files. + */ + 'image_generators' => [ + Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class, + ], + + /* + * The engine that should perform the image conversions. + * Should be either `gd` or `imagick`. + */ + 'image_driver' => env('IMAGE_DRIVER', 'gd'), + + /* + * FFMPEG & FFProbe binaries paths, only used if you try to generate video + * thumbnails and have installed the php-ffmpeg/php-ffmpeg composer package. + */ + 'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'), + 'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'), + + /* + * The path where to store temporary files while performing image conversions. + * If set to null, storage_path('medialibrary/temp') will be used. + */ + 'temporary_directory_path' => null, + + /* + * Here you can override the class names of the jobs used by this package. Make sure + * your custom jobs extend the ones provided by the package. + */ + 'jobs' => [ + 'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class, + ], + + /* + * When using the addMediaFromUrl method, this class will be used + * to download the file from the given URL. + */ + 'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class, + + /* + * The class that contains the strategy for determining how to remove responsive images. + */ + 'responsive_images' => [ + + /* + * This class is responsible for naming the files of the conversions. + */ + 'file_namer' => Spatie\MediaLibrary\ResponsiveImages\DefaultResponsiveImageFileNamer::class, + + /* + * This class can modify the performance of generating responsive images. + */ + 'performance_optimizer' => null, + + /* + * This class is responsible for determining the urls of generated responsive images. + */ + 'url_generator' => null, + ], + + /* + * When converting Media instances to response the media library will add + * a `loading` attribute to the `img` tag. Here you can set the default value + * of that attribute. + */ + 'default_loading_attribute_value' => null, + + /* + * You can specify a prefix that is used for storing all media. + * If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory. + */ + 'prefix' => env('MEDIA_PATH_PREFIX', ''), +]; \ No newline at end of file diff --git a/config/openai.php b/config/openai.php new file mode 100644 index 000000000..3ed7e2b1c --- /dev/null +++ b/config/openai.php @@ -0,0 +1,52 @@ + env('OPENAI_API_KEY'), + + 'default_model' => env('OPENAI_DEFAULT_MODEL', 'gpt-3.5-turbo'), + + 'models' => [ + 'gpt-3.5-turbo' => [ + 'name' => 'GPT-3.5 Turbo', + 'max_tokens' => 4096, + 'cost_per_1k_tokens' => 0.002, + ], + 'gpt-4' => [ + 'name' => 'GPT-4', + 'max_tokens' => 8192, + 'cost_per_1k_tokens' => 0.03, + ], + 'gpt-4-turbo' => [ + 'name' => 'GPT-4 Turbo', + 'max_tokens' => 128000, + 'cost_per_1k_tokens' => 0.01, + ], + ], + + 'timeout' => env('OPENAI_TIMEOUT', 30), + + 'max_retries' => env('OPENAI_MAX_RETRIES', 3), + + 'temperature' => [ + 'low' => 0.3, + 'medium' => 0.7, + 'high' => 0.9, + ], + + 'supported_languages' => [ + 'en' => 'English', + 'es' => 'Spanish', + 'fr' => 'French', + 'de' => 'German', + 'it' => 'Italian', + ], +]; \ No newline at end of file diff --git a/config/paytabs.php b/config/paytabs.php new file mode 100644 index 000000000..aa54d231b --- /dev/null +++ b/config/paytabs.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + + /* + |-------------------------------------------------------------------------- + | Merchant profile id + |-------------------------------------------------------------------------- + | + | Your merchant profile id , you can find the profile id on your PayTabs Merchant’s Dashboard- profile. + | + */ + + 'profile_id' => env('paytabs_profile_id', null), + + /* + |-------------------------------------------------------------------------- + | Server Key + |-------------------------------------------------------------------------- + | + | You can find the Server key on your PayTabs Merchant’s Dashboard - Developers - Key management. + | + */ + + 'server_key' => env('paytabs_server_key', null), + + /* + |-------------------------------------------------------------------------- + | Currency + |-------------------------------------------------------------------------- + | + | The currency you registered in with PayTabs account + you must pass value from this array ['AED','EGP','SAR','OMR','JOD','US'] + | + */ + + 'currency' => env('paytabs_currency', null), + + + /* + |-------------------------------------------------------------------------- + | Region + |-------------------------------------------------------------------------- + | + | The region you registered in with PayTabs + you must pass value from this array ['ARE','EGY','SAU','OMN','JOR','GLOBAL'] + | + */ + + 'region' => env('paytabs_region', null), + +]; diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 000000000..a5b992df5 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,204 @@ + [ + + /* + * 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' => Spatie\Permission\Models\Permission::class, + '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' => Spatie\Permission\Models\Role::class, + 'role' => App\Models\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/config/queue.php b/config/queue.php new file mode 100644 index 000000000..116bd8d00 --- /dev/null +++ b/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/config/role-permissions.php b/config/role-permissions.php new file mode 100644 index 000000000..21f64b5b8 --- /dev/null +++ b/config/role-permissions.php @@ -0,0 +1,118 @@ + [ + 'dashboard', + 'users', + 'roles', + 'permissions', + 'companies', + 'plans', + 'plan_requests', + 'plan_orders', + 'domain_requests', + 'currencies', + 'referral', + 'coupons', + 'appointments', + 'businesses', + 'settings' + ], + + 'company' => [ + 'dashboard', + 'users', + 'roles', + 'media', + 'media_directories', + // 'plans', + // 'referral', + // 'settings', + 'webhooks', + + // 'landing_page', + 'branches', + 'departments', + 'designations', + 'document_types', + 'employees', + 'award_types', + 'awards', + 'promotions', + 'resignations', + 'terminations', + 'warnings', + 'trips', + 'complaints', + 'transfers', + 'holidays', + 'announcements', + 'asset-types', + 'assets', + 'training-types', + 'training-programs', + 'training-sessions', + 'employee-trainings', + 'performance_indicator_categories', + 'performance_indicators', + 'goal_types', + 'employee_goals', + 'review_cycles', + 'employee_reviews', + 'job_categories', + 'job_requisitions', + 'job_types', + 'job_locations', + 'job_postings', + 'candidate_sources', + 'candidates', + 'interview_types', + 'interview_rounds', + 'interviews', + 'interview_feedback', + 'candidate_assessments', + 'offer_templates', + 'offers', + 'onboarding_checklists', + 'checklist_items', + 'candidate_onboarding', + 'meeting_types', + 'meeting_rooms', + 'meetings', + 'meeting_attendees', + 'meeting_minutes', + 'action_items', + 'contract_types', + 'employee_contracts', + // 'contract_amendments', + // 'contract_renewals', + 'contract_templates', + 'document_categories', + 'hr_documents', + 'document_acknowledgments', + 'document_templates', + 'leave_types', + 'shifts', + 'projects', + 'salary_components', + 'leave_policies', + 'attendance_policies', + 'employee_salaries', + 'attendance_records', + 'time_entries', + 'leave_applications', + 'timesheet_submissions', + 'timesheet_approvals', + 'attendance_regularizations', + 'payroll_runs', + 'leave_balances', + 'payslips', + 'payroll_adjustments', + 'working_days', + 'biometric_attendance', + 'ip_restriction', + 'calendar', + 'custom_questions', + 'career' + ] +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 000000000..27a36175f --- /dev/null +++ b/config/services.php @@ -0,0 +1,38 @@ + [ + 'token' => env('POSTMARK_TOKEN'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'resend' => [ + 'key' => env('RESEND_KEY'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 000000000..062192fa7 --- /dev/null +++ b/config/session.php @@ -0,0 +1,217 @@ + env('SESSION_DRIVER', 'file'), + + /* + |-------------------------------------------------------------------------- + | 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', 120), + + '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: "apc", "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/config/timeformat.php b/config/timeformat.php new file mode 100644 index 000000000..c04d0e2e1 --- /dev/null +++ b/config/timeformat.php @@ -0,0 +1,17 @@ + '1:30 PM', + 'g:i a' => '1:30 pm', + 'h:i A' => '01:30 PM', + 'h:i a' => '01:30 pm', + 'G:i' => '13:30', + 'H:i' => '13:30', + 'H:i:s' => '13:30:00', + 'g:i:s A' => '1:30:00 PM', + 'g:i:s a' => '1:30:00 pm', + 'h:i:s A' => '01:30:00 PM', + 'h:i:s a' => '01:30:00 pm', + 'H:i:s T' => '13:30:00 UTC', + 'H:i:s e' => '13:30:00 UTC', +]; diff --git a/config/timezones.php b/config/timezones.php new file mode 100755 index 000000000..251e14157 --- /dev/null +++ b/config/timezones.php @@ -0,0 +1,405 @@ + 'UTC', + 'America/Adak' => '(GMT-10:00) America/Adak (Hawaii-Aleutian Standard Time)', + 'America/Atka' => '(GMT-10:00) America/Atka (Hawaii-Aleutian Standard Time)', + 'America/Anchorage' => '(GMT-9:00) America/Anchorage (Alaska Standard Time)', + 'America/Juneau' => '(GMT-9:00) America/Juneau (Alaska Standard Time)', + 'America/Nome' => '(GMT-9:00) America/Nome (Alaska Standard Time)', + 'America/Yakutat' => '(GMT-9:00) America/Yakutat (Alaska Standard Time)', + 'America/Dawson' => '(GMT-8:00) America/Dawson (Pacific Standard Time)', + 'America/Ensenada' => '(GMT-8:00) America/Ensenada (Pacific Standard Time)', + 'America/Los_Angeles' => '(GMT-8:00) America/Los_Angeles (Pacific Standard Time)', + 'America/Tijuana' => '(GMT-8:00) America/Tijuana (Pacific Standard Time)', + 'America/Vancouver' => '(GMT-8:00) America/Vancouver (Pacific Standard Time)', + 'America/Whitehorse' => '(GMT-8:00) America/Whitehorse (Pacific Standard Time)', + 'Canada/Pacific' => '(GMT-8:00) Canada/Pacific (Pacific Standard Time)', + 'Canada/Yukon' => '(GMT-8:00) Canada/Yukon (Pacific Standard Time)', + 'Mexico/BajaNorte' => '(GMT-8:00) Mexico/BajaNorte (Pacific Standard Time)', + 'America/Boise' => '(GMT-7:00) America/Boise (Mountain Standard Time)', + 'America/Cambridge_Bay' => '(GMT-7:00) America/Cambridge_Bay (Mountain Standard Time)', + 'America/Chihuahua' => '(GMT-7:00) America/Chihuahua (Mountain Standard Time)', + 'America/Dawson_Creek' => '(GMT-7:00) America/Dawson_Creek (Mountain Standard Time)', + 'America/Denver' => '(GMT-7:00) America/Denver (Mountain Standard Time)', + 'America/Edmonton' => '(GMT-7:00) America/Edmonton (Mountain Standard Time)', + 'America/Hermosillo' => '(GMT-7:00) America/Hermosillo (Mountain Standard Time)', + 'America/Inuvik' => '(GMT-7:00) America/Inuvik (Mountain Standard Time)', + 'America/Mazatlan' => '(GMT-7:00) America/Mazatlan (Mountain Standard Time)', + 'America/Phoenix' => '(GMT-7:00) America/Phoenix (Mountain Standard Time)', + 'America/Shiprock' => '(GMT-7:00) America/Shiprock (Mountain Standard Time)', + 'America/Yellowknife' => '(GMT-7:00) America/Yellowknife (Mountain Standard Time)', + 'Canada/Mountain' => '(GMT-7:00) Canada/Mountain (Mountain Standard Time)', + 'Mexico/BajaSur' => '(GMT-7:00) Mexico/BajaSur (Mountain Standard Time)', + 'America/Belize' => '(GMT-6:00) America/Belize (Central Standard Time)', + 'America/Cancun' => '(GMT-6:00) America/Cancun (Central Standard Time)', + 'America/Chicago' => '(GMT-6:00) America/Chicago (Central Standard Time)', + 'America/Costa_Rica' => '(GMT-6:00) America/Costa_Rica (Central Standard Time)', + 'America/El_Salvador' => '(GMT-6:00) America/El_Salvador (Central Standard Time)', + 'America/Guatemala' => '(GMT-6:00) America/Guatemala (Central Standard Time)', + 'America/Knox_IN' => '(GMT-6:00) America/Knox_IN (Central Standard Time)', + 'America/Managua' => '(GMT-6:00) America/Managua (Central Standard Time)', + 'America/Menominee' => '(GMT-6:00) America/Menominee (Central Standard Time)', + 'America/Merida' => '(GMT-6:00) America/Merida (Central Standard Time)', + 'America/Mexico_City' => '(GMT-6:00) America/Mexico_City (Central Standard Time)', + 'America/Monterrey' => '(GMT-6:00) America/Monterrey (Central Standard Time)', + 'America/Rainy_River' => '(GMT-6:00) America/Rainy_River (Central Standard Time)', + 'America/Rankin_Inlet' => '(GMT-6:00) America/Rankin_Inlet (Central Standard Time)', + 'America/Regina' => '(GMT-6:00) America/Regina (Central Standard Time)', + 'America/Swift_Current' => '(GMT-6:00) America/Swift_Current (Central Standard Time)', + 'America/Tegucigalpa' => '(GMT-6:00) America/Tegucigalpa (Central Standard Time)', + 'America/Winnipeg' => '(GMT-6:00) America/Winnipeg (Central Standard Time)', + 'Canada/Central' => '(GMT-6:00) Canada/Central (Central Standard Time)', + 'Canada/East-Saskatchewan' => '(GMT-6:00) Canada/East-Saskatchewan (Central Standard Time)', + 'Canada/Saskatchewan' => '(GMT-6:00) Canada/Saskatchewan (Central Standard Time)', + 'Chile/EasterIsland' => '(GMT-6:00) Chile/EasterIsland (Easter Is. Time)', + 'Mexico/General' => '(GMT-6:00) Mexico/General (Central Standard Time)', + 'America/Atikokan' => '(GMT-5:00) America/Atikokan (Eastern Standard Time)', + 'America/Bogota' => '(GMT-5:00) America/Bogota (Colombia Time)', + 'America/Cayman' => '(GMT-5:00) America/Cayman (Eastern Standard Time)', + 'America/Coral_Harbour' => '(GMT-5:00) America/Coral_Harbour (Eastern Standard Time)', + 'America/Detroit' => '(GMT-5:00) America/Detroit (Eastern Standard Time)', + 'America/Fort_Wayne' => '(GMT-5:00) America/Fort_Wayne (Eastern Standard Time)', + 'America/Grand_Turk' => '(GMT-5:00) America/Grand_Turk (Eastern Standard Time)', + 'America/Guayaquil' => '(GMT-5:00) America/Guayaquil (Ecuador Time)', + 'America/Havana' => '(GMT-5:00) America/Havana (Cuba Standard Time)', + 'America/Indianapolis' => '(GMT-5:00) America/Indianapolis (Eastern Standard Time)', + 'America/Iqaluit' => '(GMT-5:00) America/Iqaluit (Eastern Standard Time)', + 'America/Jamaica' => '(GMT-5:00) America/Jamaica (Eastern Standard Time)', + 'America/Lima' => '(GMT-5:00) America/Lima (Peru Time)', + 'America/Louisville' => '(GMT-5:00) America/Louisville (Eastern Standard Time)', + 'America/Montreal' => '(GMT-5:00) America/Montreal (Eastern Standard Time)', + 'America/Nassau' => '(GMT-5:00) America/Nassau (Eastern Standard Time)', + 'America/New_York' => '(GMT-5:00) America/New_York (Eastern Standard Time)', + 'America/Nipigon' => '(GMT-5:00) America/Nipigon (Eastern Standard Time)', + 'America/Panama' => '(GMT-5:00) America/Panama (Eastern Standard Time)', + 'America/Pangnirtung' => '(GMT-5:00) America/Pangnirtung (Eastern Standard Time)', + 'America/Port-au-Prince' => '(GMT-5:00) America/Port-au-Prince (Eastern Standard Time)', + 'America/Resolute' => '(GMT-5:00) America/Resolute (Eastern Standard Time)', + 'America/Thunder_Bay' => '(GMT-5:00) America/Thunder_Bay (Eastern Standard Time)', + 'America/Toronto' => '(GMT-5:00) America/Toronto (Eastern Standard Time)', + 'Canada/Eastern' => '(GMT-5:00) Canada/Eastern (Eastern Standard Time)', + 'America/Caracas' => '(GMT-4:-30) America/Caracas (Venezuela Time)', + 'America/Anguilla' => '(GMT-4:00) America/Anguilla (Atlantic Standard Time)', + 'America/Antigua' => '(GMT-4:00) America/Antigua (Atlantic Standard Time)', + 'America/Aruba' => '(GMT-4:00) America/Aruba (Atlantic Standard Time)', + 'America/Asuncion' => '(GMT-4:00) America/Asuncion (Paraguay Time)', + 'America/Barbados' => '(GMT-4:00) America/Barbados (Atlantic Standard Time)', + 'America/Blanc-Sablon' => '(GMT-4:00) America/Blanc-Sablon (Atlantic Standard Time)', + 'America/Boa_Vista' => '(GMT-4:00) America/Boa_Vista (Amazon Time)', + 'America/Campo_Grande' => '(GMT-4:00) America/Campo_Grande (Amazon Time)', + 'America/Cuiaba' => '(GMT-4:00) America/Cuiaba (Amazon Time)', + 'America/Curacao' => '(GMT-4:00) America/Curacao (Atlantic Standard Time)', + 'America/Dominica' => '(GMT-4:00) America/Dominica (Atlantic Standard Time)', + 'America/Eirunepe' => '(GMT-4:00) America/Eirunepe (Amazon Time)', + 'America/Glace_Bay' => '(GMT-4:00) America/Glace_Bay (Atlantic Standard Time)', + 'America/Goose_Bay' => '(GMT-4:00) America/Goose_Bay (Atlantic Standard Time)', + 'America/Grenada' => '(GMT-4:00) America/Grenada (Atlantic Standard Time)', + 'America/Guadeloupe' => '(GMT-4:00) America/Guadeloupe (Atlantic Standard Time)', + 'America/Guyana' => '(GMT-4:00) America/Guyana (Guyana Time)', + 'America/Halifax' => '(GMT-4:00) America/Halifax (Atlantic Standard Time)', + 'America/La_Paz' => '(GMT-4:00) America/La_Paz (Bolivia Time)', + 'America/Manaus' => '(GMT-4:00) America/Manaus (Amazon Time)', + 'America/Marigot' => '(GMT-4:00) America/Marigot (Atlantic Standard Time)', + 'America/Martinique' => '(GMT-4:00) America/Martinique (Atlantic Standard Time)', + 'America/Moncton' => '(GMT-4:00) America/Moncton (Atlantic Standard Time)', + 'America/Montserrat' => '(GMT-4:00) America/Montserrat (Atlantic Standard Time)', + 'America/Port_of_Spain' => '(GMT-4:00) America/Port_of_Spain (Atlantic Standard Time)', + 'America/Porto_Acre' => '(GMT-4:00) America/Porto_Acre (Amazon Time)', + 'America/Porto_Velho' => '(GMT-4:00) America/Porto_Velho (Amazon Time)', + 'America/Puerto_Rico' => '(GMT-4:00) America/Puerto_Rico (Atlantic Standard Time)', + 'America/Rio_Branco' => '(GMT-4:00) America/Rio_Branco (Amazon Time)', + 'America/Santiago' => '(GMT-4:00) America/Santiago (Chile Time)', + 'America/Santo_Domingo' => '(GMT-4:00) America/Santo_Domingo (Atlantic Standard Time)', + 'America/St_Barthelemy' => '(GMT-4:00) America/St_Barthelemy (Atlantic Standard Time)', + 'America/St_Kitts' => '(GMT-4:00) America/St_Kitts (Atlantic Standard Time)', + 'America/St_Lucia' => '(GMT-4:00) America/St_Lucia (Atlantic Standard Time)', + 'America/St_Thomas' => '(GMT-4:00) America/St_Thomas (Atlantic Standard Time)', + 'America/St_Vincent' => '(GMT-4:00) America/St_Vincent (Atlantic Standard Time)', + 'America/Thule' => '(GMT-4:00) America/Thule (Atlantic Standard Time)', + 'America/Tortola' => '(GMT-4:00) America/Tortola (Atlantic Standard Time)', + 'America/Virgin' => '(GMT-4:00) America/Virgin (Atlantic Standard Time)', + 'Antarctica/Palmer' => '(GMT-4:00) Antarctica/Palmer (Chile Time)', + 'Atlantic/Bermuda' => '(GMT-4:00) Atlantic/Bermuda (Atlantic Standard Time)', + 'Atlantic/Stanley' => '(GMT-4:00) Atlantic/Stanley (Falkland Is. Time)', + 'Brazil/Acre' => '(GMT-4:00) Brazil/Acre (Amazon Time)', + 'Brazil/West' => '(GMT-4:00) Brazil/West (Amazon Time)', + 'Canada/Atlantic' => '(GMT-4:00) Canada/Atlantic (Atlantic Standard Time)', + 'Chile/Continental' => '(GMT-4:00) Chile/Continental (Chile Time)', + 'America/St_Johns' => '(GMT-3:-30) America/St_Johns (Newfoundland Standard Time)', + 'Canada/Newfoundland' => '(GMT-3:-30) Canada/Newfoundland (Newfoundland Standard Time)', + 'America/Araguaina' => '(GMT-3:00) America/Araguaina (Brasilia Time)', + 'America/Bahia' => '(GMT-3:00) America/Bahia (Brasilia Time)', + 'America/Belem' => '(GMT-3:00) America/Belem (Brasilia Time)', + 'America/Buenos_Aires' => '(GMT-3:00) America/Buenos_Aires (Argentine Time)', + 'America/Catamarca' => '(GMT-3:00) America/Catamarca (Argentine Time)', + 'America/Cayenne' => '(GMT-3:00) America/Cayenne (French Guiana Time)', + 'America/Cordoba' => '(GMT-3:00) America/Cordoba (Argentine Time)', + 'America/Fortaleza' => '(GMT-3:00) America/Fortaleza (Brasilia Time)', + 'America/Godthab' => '(GMT-3:00) America/Godthab (Western Greenland Time)', + 'America/Jujuy' => '(GMT-3:00) America/Jujuy (Argentine Time)', + 'America/Maceio' => '(GMT-3:00) America/Maceio (Brasilia Time)', + 'America/Mendoza' => '(GMT-3:00) America/Mendoza (Argentine Time)', + 'America/Miquelon' => '(GMT-3:00) America/Miquelon (Pierre & Miquelon Standard Time)', + 'America/Montevideo' => '(GMT-3:00) America/Montevideo (Uruguay Time)', + 'America/Paramaribo' => '(GMT-3:00) America/Paramaribo (Suriname Time)', + 'America/Recife' => '(GMT-3:00) America/Recife (Brasilia Time)', + 'America/Rosario' => '(GMT-3:00) America/Rosario (Argentine Time)', + 'America/Santarem' => '(GMT-3:00) America/Santarem (Brasilia Time)', + 'America/Sao_Paulo' => '(GMT-3:00) America/Sao_Paulo (Brasilia Time)', + 'Antarctica/Rothera' => '(GMT-3:00) Antarctica/Rothera (Rothera Time)', + 'Brazil/East' => '(GMT-3:00) Brazil/East (Brasilia Time)', + 'America/Noronha' => '(GMT-2:00) America/Noronha (Fernando de Noronha Time)', + 'Atlantic/South_Georgia' => '(GMT-2:00) Atlantic/South_Georgia (South Georgia Standard Time)', + 'Brazil/DeNoronha' => '(GMT-2:00) Brazil/DeNoronha (Fernando de Noronha Time)', + 'America/Scoresbysund' => '(GMT-1:00) America/Scoresbysund (Eastern Greenland Time)', + 'Atlantic/Azores' => '(GMT-1:00) Atlantic/Azores (Azores Time)', + 'Atlantic/Cape_Verde' => '(GMT-1:00) Atlantic/Cape_Verde (Cape Verde Time)', + 'Africa/Abidjan' => '(GMT+0:00) Africa/Abidjan (Greenwich Mean Time)', + 'Africa/Accra' => '(GMT+0:00) Africa/Accra (Ghana Mean Time)', + 'Africa/Bamako' => '(GMT+0:00) Africa/Bamako (Greenwich Mean Time)', + 'Africa/Banjul' => '(GMT+0:00) Africa/Banjul (Greenwich Mean Time)', + 'Africa/Bissau' => '(GMT+0:00) Africa/Bissau (Greenwich Mean Time)', + 'Africa/Casablanca' => '(GMT+0:00) Africa/Casablanca (Western European Time)', + 'Africa/Conakry' => '(GMT+0:00) Africa/Conakry (Greenwich Mean Time)', + 'Africa/Dakar' => '(GMT+0:00) Africa/Dakar (Greenwich Mean Time)', + 'Africa/El_Aaiun' => '(GMT+0:00) Africa/El_Aaiun (Western European Time)', + 'Africa/Freetown' => '(GMT+0:00) Africa/Freetown (Greenwich Mean Time)', + 'Africa/Lome' => '(GMT+0:00) Africa/Lome (Greenwich Mean Time)', + 'Africa/Monrovia' => '(GMT+0:00) Africa/Monrovia (Greenwich Mean Time)', + 'Africa/Nouakchott' => '(GMT+0:00) Africa/Nouakchott (Greenwich Mean Time)', + 'Africa/Ouagadougou' => '(GMT+0:00) Africa/Ouagadougou (Greenwich Mean Time)', + 'Africa/Sao_Tome' => '(GMT+0:00) Africa/Sao_Tome (Greenwich Mean Time)', + 'Africa/Timbuktu' => '(GMT+0:00) Africa/Timbuktu (Greenwich Mean Time)', + 'America/Danmarkshavn' => '(GMT+0:00) America/Danmarkshavn (Greenwich Mean Time)', + 'Atlantic/Canary' => '(GMT+0:00) Atlantic/Canary (Western European Time)', + 'Atlantic/Faeroe' => '(GMT+0:00) Atlantic/Faeroe (Western European Time)', + 'Atlantic/Faroe' => '(GMT+0:00) Atlantic/Faroe (Western European Time)', + 'Atlantic/Madeira' => '(GMT+0:00) Atlantic/Madeira (Western European Time)', + 'Atlantic/Reykjavik' => '(GMT+0:00) Atlantic/Reykjavik (Greenwich Mean Time)', + 'Atlantic/St_Helena' => '(GMT+0:00) Atlantic/St_Helena (Greenwich Mean Time)', + 'Europe/Belfast' => '(GMT+0:00) Europe/Belfast (Greenwich Mean Time)', + 'Europe/Dublin' => '(GMT+0:00) Europe/Dublin (Greenwich Mean Time)', + 'Europe/Guernsey' => '(GMT+0:00) Europe/Guernsey (Greenwich Mean Time)', + 'Europe/Isle_of_Man' => '(GMT+0:00) Europe/Isle_of_Man (Greenwich Mean Time)', + 'Europe/Jersey' => '(GMT+0:00) Europe/Jersey (Greenwich Mean Time)', + 'Europe/Lisbon' => '(GMT+0:00) Europe/Lisbon (Western European Time)', + 'Europe/London' => '(GMT+0:00) Europe/London (Greenwich Mean Time)', + 'Africa/Algiers' => '(GMT+1:00) Africa/Algiers (Central European Time)', + 'Africa/Bangui' => '(GMT+1:00) Africa/Bangui (Western African Time)', + 'Africa/Brazzaville' => '(GMT+1:00) Africa/Brazzaville (Western African Time)', + 'Africa/Ceuta' => '(GMT+1:00) Africa/Ceuta (Central European Time)', + 'Africa/Douala' => '(GMT+1:00) Africa/Douala (Western African Time)', + 'Africa/Kinshasa' => '(GMT+1:00) Africa/Kinshasa (Western African Time)', + 'Africa/Lagos' => '(GMT+1:00) Africa/Lagos (Western African Time)', + 'Africa/Libreville' => '(GMT+1:00) Africa/Libreville (Western African Time)', + 'Africa/Luanda' => '(GMT+1:00) Africa/Luanda (Western African Time)', + 'Africa/Malabo' => '(GMT+1:00) Africa/Malabo (Western African Time)', + 'Africa/Ndjamena' => '(GMT+1:00) Africa/Ndjamena (Western African Time)', + 'Africa/Niamey' => '(GMT+1:00) Africa/Niamey (Western African Time)', + 'Africa/Porto-Novo' => '(GMT+1:00) Africa/Porto-Novo (Western African Time)', + 'Africa/Tunis' => '(GMT+1:00) Africa/Tunis (Central European Time)', + 'Africa/Windhoek' => '(GMT+1:00) Africa/Windhoek (Western African Time)', + 'Arctic/Longyearbyen' => '(GMT+1:00) Arctic/Longyearbyen (Central European Time)', + 'Atlantic/Jan_Mayen' => '(GMT+1:00) Atlantic/Jan_Mayen (Central European Time)', + 'Europe/Amsterdam' => '(GMT+1:00) Europe/Amsterdam (Central European Time)', + 'Europe/Andorra' => '(GMT+1:00) Europe/Andorra (Central European Time)', + 'Europe/Belgrade' => '(GMT+1:00) Europe/Belgrade (Central European Time)', + 'Europe/Berlin' => '(GMT+1:00) Europe/Berlin (Central European Time)', + 'Europe/Bratislava' => '(GMT+1:00) Europe/Bratislava (Central European Time)', + 'Europe/Brussels' => '(GMT+1:00) Europe/Brussels (Central European Time)', + 'Europe/Budapest' => '(GMT+1:00) Europe/Budapest (Central European Time)', + 'Europe/Copenhagen' => '(GMT+1:00) Europe/Copenhagen (Central European Time)', + 'Europe/Gibraltar' => '(GMT+1:00) Europe/Gibraltar (Central European Time)', + 'Europe/Ljubljana' => '(GMT+1:00) Europe/Ljubljana (Central European Time)', + 'Europe/Luxembourg' => '(GMT+1:00) Europe/Luxembourg (Central European Time)', + 'Europe/Madrid' => '(GMT+1:00) Europe/Madrid (Central European Time)', + 'Europe/Malta' => '(GMT+1:00) Europe/Malta (Central European Time)', + 'Europe/Monaco' => '(GMT+1:00) Europe/Monaco (Central European Time)', + 'Europe/Oslo' => '(GMT+1:00) Europe/Oslo (Central European Time)', + 'Europe/Paris' => '(GMT+1:00) Europe/Paris (Central European Time)', + 'Europe/Podgorica' => '(GMT+1:00) Europe/Podgorica (Central European Time)', + 'Europe/Prague' => '(GMT+1:00) Europe/Prague (Central European Time)', + 'Europe/Rome' => '(GMT+1:00) Europe/Rome (Central European Time)', + 'Europe/San_Marino' => '(GMT+1:00) Europe/San_Marino (Central European Time)', + 'Europe/Sarajevo' => '(GMT+1:00) Europe/Sarajevo (Central European Time)', + 'Europe/Skopje' => '(GMT+1:00) Europe/Skopje (Central European Time)', + 'Europe/Stockholm' => '(GMT+1:00) Europe/Stockholm (Central European Time)', + 'Europe/Tirane' => '(GMT+1:00) Europe/Tirane (Central European Time)', + 'Europe/Vaduz' => '(GMT+1:00) Europe/Vaduz (Central European Time)', + 'Europe/Vatican' => '(GMT+1:00) Europe/Vatican (Central European Time)', + 'Europe/Vienna' => '(GMT+1:00) Europe/Vienna (Central European Time)', + 'Europe/Warsaw' => '(GMT+1:00) Europe/Warsaw (Central European Time)', + 'Europe/Zagreb' => '(GMT+1:00) Europe/Zagreb (Central European Time)', + 'Europe/Zurich' => '(GMT+1:00) Europe/Zurich (Central European Time)', + 'Africa/Blantyre' => '(GMT+2:00) Africa/Blantyre (Central African Time)', + 'Africa/Bujumbura' => '(GMT+2:00) Africa/Bujumbura (Central African Time)', + 'Africa/Cairo' => '(GMT+2:00) Africa/Cairo (Eastern European Time)', + 'Africa/Gaborone' => '(GMT+2:00) Africa/Gaborone (Central African Time)', + 'Africa/Harare' => '(GMT+2:00) Africa/Harare (Central African Time)', + 'Africa/Johannesburg' => '(GMT+2:00) Africa/Johannesburg (South Africa Standard Time)', + 'Africa/Kigali' => '(GMT+2:00) Africa/Kigali (Central African Time)', + 'Africa/Lubumbashi' => '(GMT+2:00) Africa/Lubumbashi (Central African Time)', + 'Africa/Lusaka' => '(GMT+2:00) Africa/Lusaka (Central African Time)', + 'Africa/Maputo' => '(GMT+2:00) Africa/Maputo (Central African Time)', + 'Africa/Maseru' => '(GMT+2:00) Africa/Maseru (South Africa Standard Time)', + 'Africa/Mbabane' => '(GMT+2:00) Africa/Mbabane (South Africa Standard Time)', + 'Africa/Tripoli' => '(GMT+2:00) Africa/Tripoli (Eastern European Time)', + 'Asia/Amman' => '(GMT+2:00) Asia/Amman (Eastern European Time)', + 'Asia/Beirut' => '(GMT+2:00) Asia/Beirut (Eastern European Time)', + 'Asia/Damascus' => '(GMT+2:00) Asia/Damascus (Eastern European Time)', + 'Asia/Gaza' => '(GMT+2:00) Asia/Gaza (Eastern European Time)', + 'Asia/Istanbul' => '(GMT+2:00) Asia/Istanbul (Eastern European Time)', + 'Asia/Jerusalem' => '(GMT+2:00) Asia/Jerusalem (Israel Standard Time)', + 'Asia/Nicosia' => '(GMT+2:00) Asia/Nicosia (Eastern European Time)', + 'Asia/Tel_Aviv' => '(GMT+2:00) Asia/Tel_Aviv (Israel Standard Time)', + 'Europe/Athens' => '(GMT+2:00) Europe/Athens (Eastern European Time)', + 'Europe/Bucharest' => '(GMT+2:00) Europe/Bucharest (Eastern European Time)', + 'Europe/Chisinau' => '(GMT+2:00) Europe/Chisinau (Eastern European Time)', + 'Europe/Helsinki' => '(GMT+2:00) Europe/Helsinki (Eastern European Time)', + 'Europe/Istanbul' => '(GMT+2:00) Europe/Istanbul (Eastern European Time)', + 'Europe/Kaliningrad' => '(GMT+2:00) Europe/Kaliningrad (Eastern European Time)', + 'Europe/Kiev' => '(GMT+2:00) Europe/Kiev (Eastern European Time)', + 'Europe/Mariehamn' => '(GMT+2:00) Europe/Mariehamn (Eastern European Time)', + 'Europe/Minsk' => '(GMT+2:00) Europe/Minsk (Eastern European Time)', + 'Europe/Nicosia' => '(GMT+2:00) Europe/Nicosia (Eastern European Time)', + 'Europe/Riga' => '(GMT+2:00) Europe/Riga (Eastern European Time)', + 'Europe/Simferopol' => '(GMT+2:00) Europe/Simferopol (Eastern European Time)', + 'Europe/Sofia' => '(GMT+2:00) Europe/Sofia (Eastern European Time)', + 'Europe/Tallinn' => '(GMT+2:00) Europe/Tallinn (Eastern European Time)', + 'Europe/Tiraspol' => '(GMT+2:00) Europe/Tiraspol (Eastern European Time)', + 'Europe/Uzhgorod' => '(GMT+2:00) Europe/Uzhgorod (Eastern European Time)', + 'Europe/Vilnius' => '(GMT+2:00) Europe/Vilnius (Eastern European Time)', + 'Europe/Zaporozhye' => '(GMT+2:00) Europe/Zaporozhye (Eastern European Time)', + 'Africa/Addis_Ababa' => '(GMT+3:00) Africa/Addis_Ababa (Eastern African Time)', + 'Africa/Asmara' => '(GMT+3:00) Africa/Asmara (Eastern African Time)', + 'Africa/Asmera' => '(GMT+3:00) Africa/Asmera (Eastern African Time)', + 'Africa/Dar_es_Salaam' => '(GMT+3:00) Africa/Dar_es_Salaam (Eastern African Time)', + 'Africa/Djibouti' => '(GMT+3:00) Africa/Djibouti (Eastern African Time)', + 'Africa/Kampala' => '(GMT+3:00) Africa/Kampala (Eastern African Time)', + 'Africa/Khartoum' => '(GMT+3:00) Africa/Khartoum (Eastern African Time)', + 'Africa/Mogadishu' => '(GMT+3:00) Africa/Mogadishu (Eastern African Time)', + 'Africa/Nairobi' => '(GMT+3:00) Africa/Nairobi (Eastern African Time)', + 'Antarctica/Syowa' => '(GMT+3:00) Antarctica/Syowa (Syowa Time)', + 'Asia/Aden' => '(GMT+3:00) Asia/Aden (Arabia Standard Time)', + 'Asia/Baghdad' => '(GMT+3:00) Asia/Baghdad (Arabia Standard Time)', + 'Asia/Bahrain' => '(GMT+3:00) Asia/Bahrain (Arabia Standard Time)', + 'Asia/Kuwait' => '(GMT+3:00) Asia/Kuwait (Arabia Standard Time)', + 'Asia/Qatar' => '(GMT+3:00) Asia/Qatar (Arabia Standard Time)', + 'Europe/Moscow' => '(GMT+3:00) Europe/Moscow (Moscow Standard Time)', + 'Europe/Volgograd' => '(GMT+3:00) Europe/Volgograd (Volgograd Time)', + 'Indian/Antananarivo' => '(GMT+3:00) Indian/Antananarivo (Eastern African Time)', + 'Indian/Comoro' => '(GMT+3:00) Indian/Comoro (Eastern African Time)', + 'Indian/Mayotte' => '(GMT+3:00) Indian/Mayotte (Eastern African Time)', + 'Asia/Tehran' => '(GMT+3:30) Asia/Tehran (Iran Standard Time)', + 'Asia/Baku' => '(GMT+4:00) Asia/Baku (Azerbaijan Time)', + 'Asia/Dubai' => '(GMT+4:00) Asia/Dubai (Gulf Standard Time)', + 'Asia/Muscat' => '(GMT+4:00) Asia/Muscat (Gulf Standard Time)', + 'Asia/Tbilisi' => '(GMT+4:00) Asia/Tbilisi (Georgia Time)', + 'Asia/Yerevan' => '(GMT+4:00) Asia/Yerevan (Armenia Time)', + 'Europe/Samara' => '(GMT+4:00) Europe/Samara (Samara Time)', + 'Indian/Mahe' => '(GMT+4:00) Indian/Mahe (Seychelles Time)', + 'Indian/Mauritius' => '(GMT+4:00) Indian/Mauritius (Mauritius Time)', + 'Indian/Reunion' => '(GMT+4:00) Indian/Reunion (Reunion Time)', + 'Asia/Kabul' => '(GMT+4:30) Asia/Kabul (Afghanistan Time)', + 'Asia/Aqtau' => '(GMT+5:00) Asia/Aqtau (Aqtau Time)', + 'Asia/Aqtobe' => '(GMT+5:00) Asia/Aqtobe (Aqtobe Time)', + 'Asia/Ashgabat' => '(GMT+5:00) Asia/Ashgabat (Turkmenistan Time)', + 'Asia/Ashkhabad' => '(GMT+5:00) Asia/Ashkhabad (Turkmenistan Time)', + 'Asia/Dushanbe' => '(GMT+5:00) Asia/Dushanbe (Tajikistan Time)', + 'Asia/Karachi' => '(GMT+5:00) Asia/Karachi (Pakistan Time)', + 'Asia/Oral' => '(GMT+5:00) Asia/Oral (Oral Time)', + 'Asia/Samarkand' => '(GMT+5:00) Asia/Samarkand (Uzbekistan Time)', + 'Asia/Tashkent' => '(GMT+5:00) Asia/Tashkent (Uzbekistan Time)', + 'Asia/Yekaterinburg' => '(GMT+5:00) Asia/Yekaterinburg (Yekaterinburg Time)', + 'Indian/Kerguelen' => '(GMT+5:00) Indian/Kerguelen (French Southern & Antarctic Lands Time)', + 'Indian/Maldives' => '(GMT+5:00) Indian/Maldives (Maldives Time)', + 'Asia/Calcutta' => '(GMT+5:30) Asia/Calcutta (India Standard Time)', + 'Asia/Colombo' => '(GMT+5:30) Asia/Colombo (India Standard Time)', + 'Asia/Kolkata' => '(GMT+5:30) Asia/Kolkata (India Standard Time)', + 'Asia/Katmandu' => '(GMT+5:45) Asia/Katmandu (Nepal Time)', + 'Antarctica/Mawson' => '(GMT+6:00) Antarctica/Mawson (Mawson Time)', + 'Antarctica/Vostok' => '(GMT+6:00) Antarctica/Vostok (Vostok Time)', + 'Asia/Almaty' => '(GMT+6:00) Asia/Almaty (Alma-Ata Time)', + 'Asia/Bishkek' => '(GMT+6:00) Asia/Bishkek (Kirgizstan Time)', + 'Asia/Dacca' => '(GMT+6:00) Asia/Dacca (Bangladesh Time)', + 'Asia/Dhaka' => '(GMT+6:00) Asia/Dhaka (Bangladesh Time)', + 'Asia/Novosibirsk' => '(GMT+6:00) Asia/Novosibirsk (Novosibirsk Time)', + 'Asia/Omsk' => '(GMT+6:00) Asia/Omsk (Omsk Time)', + 'Asia/Qyzylorda' => '(GMT+6:00) Asia/Qyzylorda (Qyzylorda Time)', + 'Asia/Thimbu' => '(GMT+6:00) Asia/Thimbu (Bhutan Time)', + 'Asia/Thimphu' => '(GMT+6:00) Asia/Thimphu (Bhutan Time)', + 'Indian/Chagos' => '(GMT+6:00) Indian/Chagos (Indian Ocean Territory Time)', + 'Asia/Rangoon' => '(GMT+6:30) Asia/Rangoon (Myanmar Time)', + 'Indian/Cocos' => '(GMT+6:30) Indian/Cocos (Cocos Islands Time)', + 'Antarctica/Davis' => '(GMT+7:00) Antarctica/Davis (Davis Time)', + 'Asia/Bangkok' => '(GMT+7:00) Asia/Bangkok (Indochina Time)', + 'Asia/Ho_Chi_Minh' => '(GMT+7:00) Asia/Ho_Chi_Minh (Indochina Time)', + 'Asia/Hovd' => '(GMT+7:00) Asia/Hovd (Hovd Time)', + 'Asia/Jakarta' => '(GMT+7:00) Asia/Jakarta (West Indonesia Time)', + 'Asia/Krasnoyarsk' => '(GMT+7:00) Asia/Krasnoyarsk (Krasnoyarsk Time)', + 'Asia/Phnom_Penh' => '(GMT+7:00) Asia/Phnom_Penh (Indochina Time)', + 'Asia/Pontianak' => '(GMT+7:00) Asia/Pontianak (West Indonesia Time)', + 'Asia/Saigon' => '(GMT+7:00) Asia/Saigon (Indochina Time)', + 'Asia/Vientiane' => '(GMT+7:00) Asia/Vientiane (Indochina Time)', + 'Indian/Christmas' => '(GMT+7:00) Indian/Christmas (Christmas Island Time)', + 'Antarctica/Casey' => '(GMT+8:00) Antarctica/Casey (Western Standard Time (Australia))', + 'Asia/Brunei' => '(GMT+8:00) Asia/Brunei (Brunei Time)', + 'Asia/Choibalsan' => '(GMT+8:00) Asia/Choibalsan (Choibalsan Time)', + 'Asia/Chongqing' => '(GMT+8:00) Asia/Chongqing (China Standard Time)', + 'Asia/Chungking' => '(GMT+8:00) Asia/Chungking (China Standard Time)', + 'Asia/Harbin' => '(GMT+8:00) Asia/Harbin (China Standard Time)', + 'Asia/Hong_Kong' => '(GMT+8:00) Asia/Hong_Kong (Hong Kong Time)', + 'Asia/Irkutsk' => '(GMT+8:00) Asia/Irkutsk (Irkutsk Time)', + 'Asia/Kashgar' => '(GMT+8:00) Asia/Kashgar (China Standard Time)', + 'Asia/Kuala_Lumpur' => '(GMT+8:00) Asia/Kuala_Lumpur (Malaysia Time)', + 'Asia/Kuching' => '(GMT+8:00) Asia/Kuching (Malaysia Time)', + 'Asia/Macao' => '(GMT+8:00) Asia/Macao (China Standard Time)', + 'Asia/Macau' => '(GMT+8:00) Asia/Macau (China Standard Time)', + 'Asia/Makassar' => '(GMT+8:00) Asia/Makassar (Central Indonesia Time)', + 'Asia/Manila' => '(GMT+8:00) Asia/Manila (Philippines Time)', + 'Asia/Shanghai' => '(GMT+8:00) Asia/Shanghai (China Standard Time)', + 'Asia/Singapore' => '(GMT+8:00) Asia/Singapore (Singapore Time)', + 'Asia/Taipei' => '(GMT+8:00) Asia/Taipei (China Standard Time)', + 'Asia/Ujung_Pandang' => '(GMT+8:00) Asia/Ujung_Pandang (Central Indonesia Time)', + 'Asia/Ulaanbaatar' => '(GMT+8:00) Asia/Ulaanbaatar (Ulaanbaatar Time)', + 'Asia/Ulan_Bator' => '(GMT+8:00) Asia/Ulan_Bator (Ulaanbaatar Time)', + 'Asia/Urumqi' => '(GMT+8:00) Asia/Urumqi (China Standard Time)', + 'Australia/Perth' => '(GMT+8:00) Australia/Perth (Western Standard Time (Australia))', + 'Australia/West' => '(GMT+8:00) Australia/West (Western Standard Time (Australia))', + 'Australia/Eucla' => '(GMT+8:45) Australia/Eucla (Central Western Standard Time (Australia))', + 'Asia/Dili' => '(GMT+9:00) Asia/Dili (Timor-Leste Time)', + 'Asia/Jayapura' => '(GMT+9:00) Asia/Jayapura (East Indonesia Time)', + 'Asia/Pyongyang' => '(GMT+9:00) Asia/Pyongyang (Korea Standard Time)', + 'Asia/Seoul' => '(GMT+9:00) Asia/Seoul (Korea Standard Time)', + 'Asia/Tokyo' => '(GMT+9:00) Asia/Tokyo (Japan Standard Time)', + 'Asia/Yakutsk' => '(GMT+9:00) Asia/Yakutsk (Yakutsk Time)', + 'Australia/Adelaide' => '(GMT+9:30) Australia/Adelaide (Central Standard Time (South Australia))', + 'Australia/Broken_Hill' => '(GMT+9:30) Australia/Broken_Hill (Central Standard Time (South Australia/New South Wales))', + 'Australia/Darwin' => '(GMT+9:30) Australia/Darwin (Central Standard Time (Northern Territory))', + 'Australia/North' => '(GMT+9:30) Australia/North (Central Standard Time (Northern Territory))', + 'Australia/South' => '(GMT+9:30) Australia/South (Central Standard Time (South Australia))', + 'Australia/Yancowinna' => '(GMT+9:30) Australia/Yancowinna (Central Standard Time (South Australia/New South Wales))', + 'Antarctica/DumontDUrville' => '(GMT+10:00) Antarctica/DumontDUrville (Dumont-d\'Urville Time)', + 'Asia/Sakhalin' => '(GMT+10:00) Asia/Sakhalin (Sakhalin Time)', + 'Asia/Vladivostok' => '(GMT+10:00) Asia/Vladivostok (Vladivostok Time)', + 'Australia/ACT' => '(GMT+10:00) Australia/ACT (Eastern Standard Time (New South Wales))', + 'Australia/Brisbane' => '(GMT+10:00) Australia/Brisbane (Eastern Standard Time (Queensland))', + 'Australia/Canberra' => '(GMT+10:00) Australia/Canberra (Eastern Standard Time (New South Wales))', + 'Australia/Currie' => '(GMT+10:00) Australia/Currie (Eastern Standard Time (New South Wales))', + 'Australia/Hobart' => '(GMT+10:00) Australia/Hobart (Eastern Standard Time (Tasmania))', + 'Australia/Lindeman' => '(GMT+10:00) Australia/Lindeman (Eastern Standard Time (Queensland))', + 'Australia/Melbourne' => '(GMT+10:00) Australia/Melbourne (Eastern Standard Time (Victoria))', + 'Australia/NSW' => '(GMT+10:00) Australia/NSW (Eastern Standard Time (New South Wales))', + 'Australia/Queensland' => '(GMT+10:00) Australia/Queensland (Eastern Standard Time (Queensland))', + 'Australia/Sydney' => '(GMT+10:00) Australia/Sydney (Eastern Standard Time (New South Wales))', + 'Australia/Tasmania' => '(GMT+10:00) Australia/Tasmania (Eastern Standard Time (Tasmania))', + 'Australia/Victoria' => '(GMT+10:00) Australia/Victoria (Eastern Standard Time (Victoria))', + 'Australia/LHI' => '(GMT+10:30) Australia/LHI (Lord Howe Standard Time)', + 'Australia/Lord_Howe' => '(GMT+10:30) Australia/Lord_Howe (Lord Howe Standard Time)', + 'Asia/Magadan' => '(GMT+11:00) Asia/Magadan (Magadan Time)', + 'Antarctica/McMurdo' => '(GMT+12:00) Antarctica/McMurdo (New Zealand Standard Time)', + 'Antarctica/South_Pole' => '(GMT+12:00) Antarctica/South_Pole (New Zealand Standard Time)', + 'Asia/Anadyr' => '(GMT+12:00) Asia/Anadyr (Anadyr Time)', + 'Asia/Kamchatka' => '(GMT+12:00) Asia/Kamchatka (Petropavlovsk-Kamchatski Time)', +]; diff --git a/config/verification.php b/config/verification.php new file mode 100644 index 000000000..3b7158805 --- /dev/null +++ b/config/verification.php @@ -0,0 +1,6 @@ + 'HRM', + 'system_version' => '8.6', +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 000000000..9b19b93c9 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php new file mode 100644 index 000000000..8ae7a5515 --- /dev/null +++ b/database/factories/CategoryFactory.php @@ -0,0 +1,24 @@ + $this->faker->unique()->word(), + 'description' => $this->faker->sentence(), + 'image' => 'categories/category-' . $this->faker->numberBetween(1, 5) . '.jpg', + 'created_at' => $this->faker->dateTimeBetween('-1 year', 'now'), + 'updated_at' => function (array $attributes) { + return $this->faker->dateTimeBetween($attributes['created_at'], 'now'); + }, + ]; + } +} \ No newline at end of file diff --git a/database/factories/ProductFactory.php b/database/factories/ProductFactory.php new file mode 100644 index 000000000..f5fa775e7 --- /dev/null +++ b/database/factories/ProductFactory.php @@ -0,0 +1,30 @@ + $this->faker->words(3, true), + 'description' => $this->faker->paragraph(), + 'price' => $this->faker->randomFloat(2, 10, 1000), + 'category_id' => function () { + return Category::inRandomOrder()->first()->id ?? null; + }, + 'featured_image' => 'products/product-' . $this->faker->numberBetween(1, 10) . '.jpg', + 'featured_image_original_name' => 'product-image.jpg', + 'created_at' => $this->faker->dateTimeBetween('-6 months', 'now'), + 'updated_at' => function (array $attributes) { + return $this->faker->dateTimeBetween($attributes['created_at'], 'now'); + }, + ]; + } +} \ No newline at end of file diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 000000000..584104c9c --- /dev/null +++ b/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/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 000000000..42ab1b3e6 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,87 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password')->nullable(); + $table->rememberToken(); + $table->string('lang')->default('en')->nullable(); + $table->string('avatar')->nullable(); + $table->string('type', 20)->default('company'); + + // Saas Product Field + if (isSaas()) { + $table->unsignedBigInteger('plan_id')->nullable(); + $table->date('plan_expire_date')->nullable(); + $table->integer('requested_plan')->default(0); + $table->integer('plan_is_active')->default(1); + $table->float('storage_limit', 15, 2)->default(0.00); + $table->string('is_trial')->nullable(); + $table->integer('trial_day')->default(0); + $table->date('trial_expire_date')->nullable(); + $table->text('active_module')->nullable(); + $table->integer('referral_code')->default(0); + $table->integer('used_referral_code')->default(0); + $table->integer('commission_amount')->default(0); + } + + $table->integer('created_by')->default(0); + $table->string('mode')->default('light'); + $table->integer('is_enable_login')->default(1); + $table->integer('google2fa_enable')->default(0); + $table->text('google2fa_secret')->nullable(); + $table->string('status')->default('active'); + $table->timestamps(); + // Foreign keys will be added in a separate migration after all tables are created + }); + + 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->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + + if (isSaas() && Schema::hasColumn('users', 'referral_code')) { + $users = DB::table('users')->where('type', 'company')->get(); + foreach ($users as $user) { + do { + $code = rand(100000, 999999); + } while (DB::table('users')->where('referral_code', $code)->exists()); + DB::table('users')->where('id', $user->id)->update(['referral_code' => $code]); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 000000000..b9c106be8 --- /dev/null +++ b/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/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 000000000..425e7058f --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,57 @@ +id(); + $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->string('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->id(); + $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/database/migrations/2024_01_15_000001_create_leave_types_table.php b/database/migrations/2024_01_15_000001_create_leave_types_table.php new file mode 100644 index 000000000..f9ededc7b --- /dev/null +++ b/database/migrations/2024_01_15_000001_create_leave_types_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->integer('max_days_per_year')->default(0); + $table->boolean('is_paid')->default(true); + $table->string('color', 7)->default('#3B82F6'); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('leave_types'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000002_create_leave_policies_table.php b/database/migrations/2024_01_15_000002_create_leave_policies_table.php new file mode 100644 index 000000000..b697b6524 --- /dev/null +++ b/database/migrations/2024_01_15_000002_create_leave_policies_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->foreignId('leave_type_id')->constrained('leave_types')->onDelete('cascade'); + $table->enum('accrual_type', ['monthly', 'yearly'])->default('yearly'); + $table->decimal('accrual_rate', 8, 2)->default(0); + $table->integer('carry_forward_limit')->default(0); + $table->integer('min_days_per_application')->default(1); + $table->integer('max_days_per_application')->default(30); + $table->boolean('requires_approval')->default(true); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('leave_policies'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000003_create_leave_applications_table.php b/database/migrations/2024_01_15_000003_create_leave_applications_table.php new file mode 100644 index 000000000..1c15c393e --- /dev/null +++ b/database/migrations/2024_01_15_000003_create_leave_applications_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('leave_type_id')->constrained('leave_types')->onDelete('cascade'); + $table->foreignId('leave_policy_id')->constrained('leave_policies')->onDelete('cascade'); + $table->date('start_date'); + $table->date('end_date'); + $table->integer('total_days'); + $table->text('reason'); + $table->string('attachment')->nullable(); + $table->enum('status', ['pending', 'approved', 'rejected'])->default('pending'); + $table->text('manager_comments')->nullable(); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('approved_at')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('leave_applications'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000004_create_leave_balances_table.php b/database/migrations/2024_01_15_000004_create_leave_balances_table.php new file mode 100644 index 000000000..8141635d1 --- /dev/null +++ b/database/migrations/2024_01_15_000004_create_leave_balances_table.php @@ -0,0 +1,35 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('leave_type_id')->constrained('leave_types')->onDelete('cascade'); + $table->foreignId('leave_policy_id')->constrained('leave_policies')->onDelete('cascade'); + $table->year('year'); + $table->decimal('allocated_days', 8, 2)->default(0); + $table->decimal('used_days', 8, 2)->default(0); + $table->decimal('remaining_days', 8, 2)->default(0); + $table->decimal('carried_forward', 8, 2)->default(0); + $table->decimal('manual_adjustment', 8, 2)->default(0); + $table->text('adjustment_reason')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + + // Unique constraint for employee, leave type, and year + $table->unique(['employee_id', 'leave_type_id', 'year']); + }); + } + + public function down(): void + { + Schema::dropIfExists('leave_balances'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000005_create_shifts_table.php b/database/migrations/2024_01_15_000005_create_shifts_table.php new file mode 100644 index 000000000..f6d515550 --- /dev/null +++ b/database/migrations/2024_01_15_000005_create_shifts_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->time('start_time'); + $table->time('end_time'); + $table->integer('break_duration')->default(0); // minutes + $table->time('break_start_time')->nullable(); // break start time + $table->time('break_end_time')->nullable(); // break end time + $table->integer('grace_period')->default(0); // minutes + $table->boolean('is_night_shift')->default(false); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('shifts'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000006_create_attendance_policies_table.php b/database/migrations/2024_01_15_000006_create_attendance_policies_table.php new file mode 100644 index 000000000..4ca7af04f --- /dev/null +++ b/database/migrations/2024_01_15_000006_create_attendance_policies_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->integer('late_arrival_grace')->default(15); // minutes + $table->integer('early_departure_grace')->default(15); // minutes + $table->decimal('half_day_threshold', 5, 2)->default(4.00); // hours + $table->decimal('overtime_rate_per_hour', 8, 2)->default(150.00); // ₹150 per hour + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('attendance_policies'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000007_create_attendance_records_table.php b/database/migrations/2024_01_15_000007_create_attendance_records_table.php new file mode 100644 index 000000000..b59038d4b --- /dev/null +++ b/database/migrations/2024_01_15_000007_create_attendance_records_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('shift_id')->nullable()->constrained('shifts')->onDelete('set null'); + $table->foreignId('attendance_policy_id')->nullable()->constrained('attendance_policies')->onDelete('set null'); + $table->date('date'); + $table->time('clock_in')->nullable(); + $table->time('clock_out')->nullable(); + $table->decimal('total_hours', 5, 2)->default(0); + $table->decimal('break_hours', 5, 2)->default(0); + $table->decimal('overtime_hours', 5, 2)->default(0); + $table->decimal('overtime_amount', 8, 2)->default(0); + $table->boolean('is_late')->default(false); + $table->boolean('is_early_departure')->default(false); + $table->boolean('is_absent')->default(false); + $table->boolean('is_holiday')->default(false); + $table->boolean('is_weekend')->default(false); + $table->enum('status', ['present', 'absent', 'half_day', 'on_leave', 'holiday'])->default('present'); + $table->text('notes')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + + // Unique constraint for employee and date + $table->unique(['employee_id', 'date']); + }); + } + + public function down(): void + { + Schema::dropIfExists('attendance_records'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000008_create_attendance_regularizations_table.php b/database/migrations/2024_01_15_000008_create_attendance_regularizations_table.php new file mode 100644 index 000000000..d312070c5 --- /dev/null +++ b/database/migrations/2024_01_15_000008_create_attendance_regularizations_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('attendance_record_id')->constrained('attendance_records')->onDelete('cascade'); + $table->date('date'); + $table->time('requested_clock_in')->nullable(); + $table->time('requested_clock_out')->nullable(); + $table->time('original_clock_in')->nullable(); + $table->time('original_clock_out')->nullable(); + $table->text('reason'); + $table->enum('status', ['pending', 'approved', 'rejected'])->default('pending'); + $table->text('manager_comments')->nullable(); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('approved_at')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('attendance_regularizations'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000009_create_time_entries_table.php b/database/migrations/2024_01_15_000009_create_time_entries_table.php new file mode 100644 index 000000000..b09948a92 --- /dev/null +++ b/database/migrations/2024_01_15_000009_create_time_entries_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->date('date'); + $table->decimal('hours', 5, 2); + $table->text('description'); + $table->string('project')->nullable(); + $table->enum('status', ['pending', 'approved', 'rejected'])->default('pending'); + $table->text('manager_comments')->nullable(); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('approved_at')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('time_entries'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000010_create_salary_components_table.php b/database/migrations/2024_01_15_000010_create_salary_components_table.php new file mode 100644 index 000000000..580e527bb --- /dev/null +++ b/database/migrations/2024_01_15_000010_create_salary_components_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->enum('type', ['earning', 'deduction'])->default('earning'); + $table->enum('calculation_type', ['fixed', 'percentage'])->default('fixed'); + $table->decimal('default_amount', 10, 2)->default(0); + $table->decimal('percentage_of_basic', 5, 2)->nullable(); + $table->boolean('is_taxable')->default(true); + $table->boolean('is_mandatory')->default(false); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('salary_components'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000011_create_employee_salaries_table.php b/database/migrations/2024_01_15_000011_create_employee_salaries_table.php new file mode 100644 index 000000000..8f747fbf3 --- /dev/null +++ b/database/migrations/2024_01_15_000011_create_employee_salaries_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->decimal('basic_salary', 10, 2)->nullable(); + $table->json('components')->nullable(); + $table->boolean('is_active')->default(true); + $table->enum('calculation_status', ['pending', 'calculated'])->default('pending'); + $table->text('notes')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + + // Unique constraint for employee + $table->unique(['employee_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('employee_salaries'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000012_create_payroll_runs_table.php b/database/migrations/2024_01_15_000012_create_payroll_runs_table.php new file mode 100644 index 000000000..75dba1b09 --- /dev/null +++ b/database/migrations/2024_01_15_000012_create_payroll_runs_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('title'); + $table->enum('payroll_frequency', ['weekly', 'biweekly', 'monthly'])->default('monthly'); + $table->date('pay_period_start'); + $table->date('pay_period_end'); + $table->date('pay_date'); + $table->decimal('total_gross_pay', 12, 2)->default(0); + $table->decimal('total_deductions', 12, 2)->default(0); + $table->decimal('total_net_pay', 12, 2)->default(0); + $table->integer('employee_count')->default(0); + $table->enum('status', ['draft', 'processing', 'completed', 'cancelled'])->default('draft'); + $table->text('notes')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('payroll_runs'); + } +}; \ No newline at end of file diff --git a/database/migrations/2024_01_15_000013_create_payroll_entries_table.php b/database/migrations/2024_01_15_000013_create_payroll_entries_table.php new file mode 100644 index 000000000..89bd0c5e8 --- /dev/null +++ b/database/migrations/2024_01_15_000013_create_payroll_entries_table.php @@ -0,0 +1,58 @@ +id(); + $table->foreignId('payroll_run_id')->constrained('payroll_runs')->onDelete('cascade'); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + + // Monetary values (always decimal for accuracy) + $table->decimal('basic_salary', 10, 2)->default(0); + $table->decimal('component_earnings', 10, 2)->default(0); + $table->decimal('total_earnings', 10, 2)->default(0); + $table->decimal('total_deductions', 10, 2)->default(0); + $table->decimal('gross_pay', 10, 2)->default(0); + $table->decimal('net_pay', 10, 2)->default(0); + $table->decimal('overtime_amount', 10, 2)->default(0); + $table->decimal('per_day_salary', 10, 2)->default(0); + $table->decimal('unpaid_leave_deduction', 10, 2)->default(0); + + // Days (use decimal where half-days are possible) + $table->integer('working_days')->default(0); // whole working days + $table->decimal('present_days', 5, 2)->default(0); // allow 22.5 etc. + $table->integer('full_present_days')->default(0); // whole days only + $table->decimal('half_days', 5, 2)->default(0); // fractional (0.5, 1.5 etc.) + $table->integer('holiday_days')->default(0); // whole days + $table->decimal('paid_leave_days', 5, 2)->default(0); // could be 1.5 + $table->decimal('unpaid_leave_days', 5, 2)->default(0); // could be 2.5 + $table->integer('absent_days')->default(0); // whole days only + + // Overtime + $table->decimal('overtime_hours', 5, 2)->default(0); // e.g., 2.75 hours + + // Breakdown JSONs + $table->json('earnings_breakdown')->nullable(); + $table->json('deductions_breakdown')->nullable(); + + $table->text('notes')->nullable(); + + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + + // Unique constraint for employee per payroll run + $table->unique(['payroll_run_id', 'employee_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('payroll_entries'); + } +}; diff --git a/database/migrations/2024_01_15_000014_create_payslips_table.php b/database/migrations/2024_01_15_000014_create_payslips_table.php new file mode 100644 index 000000000..71edb23e6 --- /dev/null +++ b/database/migrations/2024_01_15_000014_create_payslips_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('payroll_entry_id')->constrained('payroll_entries')->onDelete('cascade'); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->string('payslip_number')->unique(); + $table->date('pay_period_start'); + $table->date('pay_period_end'); + $table->date('pay_date'); + $table->string('file_path')->nullable(); + $table->enum('status', ['generated', 'sent', 'downloaded'])->default('generated'); + $table->timestamp('sent_at')->nullable(); + $table->timestamp('downloaded_at')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('payslips'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_01_27_084150_create_landing_page_settings_table.php b/database/migrations/2025_01_27_084150_create_landing_page_settings_table.php new file mode 100644 index 000000000..adb70cc12 --- /dev/null +++ b/database/migrations/2025_01_27_084150_create_landing_page_settings_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('company_name')->nullable()->default('HRM'); + $table->string('contact_email')->nullable()->default('support@hrm.com'); + $table->string('contact_phone')->nullable()->default('+1 (555) 123-4567'); + $table->string('contact_address')->nullable()->default('San Francisco, CA'); + $table->json('config_sections')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('landing_page_settings'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_01_28_000001_create_webhooks_table.php b/database/migrations/2025_01_28_000001_create_webhooks_table.php new file mode 100644 index 000000000..6711370b3 --- /dev/null +++ b/database/migrations/2025_01_28_000001_create_webhooks_table.php @@ -0,0 +1,27 @@ +id(); + $table->unsignedBigInteger('user_id'); + $table->enum('module', ['New User', 'New Appointment']); + $table->enum('method', ['GET', 'POST']); + $table->string('url'); + $table->timestamps(); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + public function down(): void + { + Schema::dropIfExists('webhooks'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_05_25_000000_create_permission_tables.php b/database/migrations/2025_05_25_000000_create_permission_tables.php new file mode 100644 index 000000000..7bfeb6b03 --- /dev/null +++ b/database/migrations/2025_05_25_000000_create_permission_tables.php @@ -0,0 +1,145 @@ +bigIncrements('id'); // permission id + $table->string('module')->nullable(); + $table->string('name'); // For MySQL 8.0 use string('name', 125); + $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); + $table->string('label')->nullable(); + $table->text('description')->nullable(); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) { + $table->bigIncrements('id'); // role id + if ($teams || config('permission.teams')) { + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); // For MySQL 8.0 use string('name', 125); + $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125); + $table->string('label')->nullable(); + $table->text('description')->nullable(); + $table->unsignedBigInteger('created_by')->nullable(); + $table->timestamps(); + if ($teams || config('permission.teams')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + //$table->unique(['guard_name']); + } + + $table->foreign('created_by')->references('id')->on('users')->onDelete('set null'); + }); + + Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($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'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($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'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($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']); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_06_18_000001_create_plans_table.php b/database/migrations/2025_06_18_000001_create_plans_table.php new file mode 100644 index 000000000..60c92e897 --- /dev/null +++ b/database/migrations/2025_06_18_000001_create_plans_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('name', 100)->unique(); + $table->float('price', 30, 2)->default(0); // Monthly price + $table->float('yearly_price', 30, 2)->nullable(); // Yearly price + $table->string('duration', 100); + $table->integer('max_users')->default(0); + $table->integer('max_employees')->default(0); + $table->text('description')->nullable(); + $table->string('enable_chatgpt', 255)->default('on'); + $table->float('storage_limit', 15, 2)->default('0.00'); + $table->string('is_trial')->nullable(); + $table->integer('trial_day')->default(0); + $table->string('is_plan_enable')->default('on'); + $table->boolean('is_default')->default(false); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (isSaas()) { + Schema::dropIfExists('plans'); + } + } +}; diff --git a/database/migrations/2025_06_18_105755_create_settings_table.php b/database/migrations/2025_06_18_105755_create_settings_table.php new file mode 100644 index 000000000..667670090 --- /dev/null +++ b/database/migrations/2025_06_18_105755_create_settings_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->string('key'); + $table->text('value')->nullable(); + $table->timestamps(); + + // Unique constraint to prevent duplicate settings for a user + $table->unique(['user_id', 'key']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('settings'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_06_19_051735_create_coupons_table.php b/database/migrations/2025_06_19_051735_create_coupons_table.php new file mode 100644 index 000000000..61d7c2051 --- /dev/null +++ b/database/migrations/2025_06_19_051735_create_coupons_table.php @@ -0,0 +1,45 @@ +id(); + $table->string('name'); + $table->enum('type', ['percentage', 'flat']); + $table->decimal('minimum_spend', 10, 2)->nullable(); + $table->decimal('maximum_spend', 10, 2)->nullable(); + $table->decimal('discount_amount', 10, 2); + $table->integer('use_limit_per_coupon')->nullable(); + $table->integer('use_limit_per_user')->nullable(); + $table->date('expiry_date')->nullable(); + $table->string('code')->unique(); + $table->enum('code_type', ['manual', 'auto'])->default('manual'); + $table->boolean('status')->default(true); + $table->unsignedBigInteger('created_by'); + $table->timestamps(); + + $table->foreign('created_by')->references('id')->on('users')->onDelete('cascade'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (isSaas()) { + Schema::dropIfExists('coupons'); + } + } +}; diff --git a/database/migrations/2025_06_19_084856_create_plan_requests_table.php b/database/migrations/2025_06_19_084856_create_plan_requests_table.php new file mode 100644 index 000000000..e2e37ac7f --- /dev/null +++ b/database/migrations/2025_06_19_084856_create_plan_requests_table.php @@ -0,0 +1,39 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('plan_id')->constrained()->onDelete('cascade'); + $table->string('status')->default('pending'); // pending, approved, rejected + $table->text('message')->nullable(); + $table->timestamp('approved_at')->nullable(); + $table->timestamp('rejected_at')->nullable(); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->foreignId('rejected_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (isSaas()) { + Schema::dropIfExists('plan_requests'); + } + } +}; diff --git a/database/migrations/2025_06_19_085023_create_plan_orders_table.php b/database/migrations/2025_06_19_085023_create_plan_orders_table.php new file mode 100644 index 000000000..1ef5d070f --- /dev/null +++ b/database/migrations/2025_06_19_085023_create_plan_orders_table.php @@ -0,0 +1,47 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('plan_id')->constrained()->onDelete('cascade'); + $table->foreignId('coupon_id')->nullable()->constrained()->onDelete('set null'); + $table->string('billing_cycle')->nullable(); + $table->string('order_number')->unique(); + $table->decimal('original_price', 10, 2); + $table->decimal('discount_amount', 10, 2)->default(0); + $table->decimal('final_price', 10, 2); + $table->string('coupon_code')->nullable(); + $table->string('payment_method')->nullable(); + $table->text('payment_id')->nullable(); + $table->enum('status', ['pending', 'approved', 'rejected', 'cancelled'])->default('pending'); + $table->timestamp('ordered_at'); + $table->timestamp('processed_at')->nullable(); + $table->foreignId('processed_by')->nullable()->constrained('users')->onDelete('set null'); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (isSaas()) { + Schema::dropIfExists('plan_orders'); + } + } +}; diff --git a/database/migrations/2025_06_20_044143_create_referral_settings_table.php b/database/migrations/2025_06_20_044143_create_referral_settings_table.php new file mode 100644 index 000000000..6e242ef67 --- /dev/null +++ b/database/migrations/2025_06_20_044143_create_referral_settings_table.php @@ -0,0 +1,29 @@ +id(); + $table->boolean('is_enabled')->default(true); + $table->decimal('commission_percentage', 5, 2)->default(10.00); + $table->decimal('threshold_amount', 10, 2)->default(50.00); + $table->text('guidelines')->nullable(); + $table->timestamps(); + }); + } + } + + public function down(): void + { + if (isSaas()) { + Schema::dropIfExists('referral_settings'); + } + } +}; \ No newline at end of file diff --git a/database/migrations/2025_06_20_044158_create_referrals_table.php b/database/migrations/2025_06_20_044158_create_referrals_table.php new file mode 100644 index 000000000..a0452ce7a --- /dev/null +++ b/database/migrations/2025_06_20_044158_create_referrals_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('user_id')->constrained()->onDelete('cascade'); + $table->foreignId('company_id')->constrained('users')->onDelete('cascade'); + $table->decimal('commission_percentage', 5, 2); + $table->decimal('amount', 10, 2); + $table->foreignId('plan_id')->nullable()->constrained()->onDelete('set null'); + $table->timestamps(); + }); + } + } + + public function down(): void + { + if (isSaas()) { + Schema::dropIfExists('referrals'); + } + } +}; diff --git a/database/migrations/2025_06_20_044206_create_payout_requests_table.php b/database/migrations/2025_06_20_044206_create_payout_requests_table.php new file mode 100644 index 000000000..e928f3556 --- /dev/null +++ b/database/migrations/2025_06_20_044206_create_payout_requests_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('company_id')->constrained('users')->onDelete('cascade'); + $table->decimal('amount', 10, 2); + $table->enum('status', ['pending', 'approved', 'rejected'])->default('pending'); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + } + + public function down(): void + { + if (isSaas()) { + Schema::dropIfExists('payout_requests'); + } + } +}; diff --git a/database/migrations/2025_06_24_044208_create_currencies_table.php b/database/migrations/2025_06_24_044208_create_currencies_table.php new file mode 100644 index 000000000..062eaddd9 --- /dev/null +++ b/database/migrations/2025_06_24_044208_create_currencies_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('code', 10)->unique(); + $table->string('symbol', 10)->nullable(); + $table->text('description')->nullable(); + $table->boolean('is_default')->default(false); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('currencies'); + } +}; diff --git a/database/migrations/2025_06_26_100501_create_payment_settings_table.php b/database/migrations/2025_06_26_100501_create_payment_settings_table.php new file mode 100644 index 000000000..095e0baff --- /dev/null +++ b/database/migrations/2025_06_26_100501_create_payment_settings_table.php @@ -0,0 +1,28 @@ +id(); + $table->unsignedBigInteger('user_id'); + $table->string('key'); + $table->text('value')->nullable(); + $table->timestamps(); + + $table->unique(['user_id', 'key']); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->index(['user_id', 'key']); + }); + } + + public function down(): void + { + Schema::dropIfExists('payment_settings'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_06_27_060535_create_media_items_table.php b/database/migrations/2025_06_27_060535_create_media_items_table.php new file mode 100644 index 000000000..49952feae --- /dev/null +++ b/database/migrations/2025_06_27_060535_create_media_items_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('media_items'); + } +}; diff --git a/database/migrations/2025_06_27_115807_create_email_templates_table.php b/database/migrations/2025_06_27_115807_create_email_templates_table.php new file mode 100644 index 000000000..7c4c6e3d2 --- /dev/null +++ b/database/migrations/2025_06_27_115807_create_email_templates_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('name'); + $table->string('from')->nullable(); + $table->unsignedBigInteger('user_id')->default(1); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('email_templates'); + } +}; diff --git a/database/migrations/2025_06_27_115820_create_email_template_langs_table.php b/database/migrations/2025_06_27_115820_create_email_template_langs_table.php new file mode 100644 index 000000000..a853dee2a --- /dev/null +++ b/database/migrations/2025_06_27_115820_create_email_template_langs_table.php @@ -0,0 +1,33 @@ +id(); + $table->unsignedBigInteger('parent_id'); + $table->string('lang'); + $table->string('subject'); + $table->longText('content'); + $table->timestamps(); + + $table->foreign('parent_id')->references('id')->on('email_templates')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('email_template_langs'); + } +}; diff --git a/database/migrations/2025_06_27_115828_create_user_email_templates_table.php b/database/migrations/2025_06_27_115828_create_user_email_templates_table.php new file mode 100644 index 000000000..a78048c66 --- /dev/null +++ b/database/migrations/2025_06_27_115828_create_user_email_templates_table.php @@ -0,0 +1,32 @@ +id(); + $table->unsignedBigInteger('template_id'); + $table->unsignedBigInteger('user_id'); + $table->boolean('is_active')->default(1); + $table->timestamps(); + + $table->foreign('template_id')->references('id')->on('email_templates')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('user_email_templates'); + } +}; diff --git a/database/migrations/2025_07_02_094334_create_landing_page_custom_pages_table.php b/database/migrations/2025_07_02_094334_create_landing_page_custom_pages_table.php new file mode 100644 index 000000000..1e3ca7eb9 --- /dev/null +++ b/database/migrations/2025_07_02_094334_create_landing_page_custom_pages_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('title'); + $table->string('slug')->unique(); + $table->longText('content'); + $table->string('meta_title')->nullable(); + $table->text('meta_description')->nullable(); + $table->boolean('is_active')->default(true); + $table->integer('sort_order')->default(0); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('landing_page_custom_pages'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_22_055549_create_contacts_table.php b/database/migrations/2025_07_22_055549_create_contacts_table.php new file mode 100644 index 000000000..196d47033 --- /dev/null +++ b/database/migrations/2025_07_22_055549_create_contacts_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name'); + $table->string('email'); + $table->string('subject'); + $table->text('message'); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('contacts'); + } +}; diff --git a/database/migrations/2025_07_23_054614_create_branches_table.php b/database/migrations/2025_07_23_054614_create_branches_table.php new file mode 100644 index 000000000..a6f533199 --- /dev/null +++ b/database/migrations/2025_07_23_054614_create_branches_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('name'); + $table->text('address')->nullable(); + $table->string('city')->nullable(); + $table->string('state')->nullable(); + $table->string('country')->nullable(); + $table->string('zip_code')->nullable(); + $table->string('phone')->nullable(); + $table->string('email')->nullable(); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + + public function down(): void + { + Schema::dropIfExists('branches'); + } +}; diff --git a/database/migrations/2025_07_23_054635_create_departments_table.php b/database/migrations/2025_07_23_054635_create_departments_table.php new file mode 100644 index 000000000..b09046670 --- /dev/null +++ b/database/migrations/2025_07_23_054635_create_departments_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->foreignId('branch_id')->constrained('branches')->onDelete('cascade'); + $table->text('description')->nullable(); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + + // Composite unique constraint for department name within a branch + $table->unique(['name', 'branch_id']); + }); + } + + + public function down(): void + { + Schema::dropIfExists('departments'); + } +}; diff --git a/database/migrations/2025_07_23_055231_create_designations_table.php b/database/migrations/2025_07_23_055231_create_designations_table.php new file mode 100644 index 000000000..19f0a60c1 --- /dev/null +++ b/database/migrations/2025_07_23_055231_create_designations_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->foreignId('department_id')->constrained('departments')->onDelete('cascade'); + $table->enum('status', ['active', 'inactive'])->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('designations'); + } +}; diff --git a/database/migrations/2025_07_23_062221_create_document_types_table.php b/database/migrations/2025_07_23_062221_create_document_types_table.php new file mode 100644 index 000000000..7ee700d7b --- /dev/null +++ b/database/migrations/2025_07_23_062221_create_document_types_table.php @@ -0,0 +1,24 @@ +id(); + $table->string('name'); + $table->boolean('is_required')->default(false); + $table->text('description')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + public function down(): void + { + Schema::dropIfExists('document_types'); + } +}; diff --git a/database/migrations/2025_07_23_095242_create_employees_table.php b/database/migrations/2025_07_23_095242_create_employees_table.php new file mode 100644 index 000000000..7491cc0d4 --- /dev/null +++ b/database/migrations/2025_07_23_095242_create_employees_table.php @@ -0,0 +1,65 @@ +id(); + + // Basic Information + $table->string('employee_id')->unique(); + $table->string('phone')->nullable(); + $table->date('date_of_birth')->nullable(); + $table->enum('gender', ['male', 'female', 'other'])->nullable(); + + // Employment Details + $table->foreignId('branch_id')->nullable()->constrained('branches')->onDelete('set null'); + $table->foreignId('department_id')->nullable()->constrained('departments')->onDelete('set null'); + $table->foreignId('designation_id')->nullable()->constrained('designations')->onDelete('set null'); + $table->foreignId('shift_id')->nullable()->constrained('shifts')->onDelete('set null'); + $table->foreignId('attendance_policy_id')->nullable()->constrained('attendance_policies')->onDelete('set null'); + $table->date('date_of_joining')->nullable(); + $table->string('employment_type')->nullable(); // Full-time, Part-time, Contract, etc. + + // Contact Information + $table->string('address_line_1')->nullable(); + $table->string('address_line_2')->nullable(); + $table->string('city')->nullable(); + $table->string('state')->nullable(); + $table->string('country')->nullable(); + $table->string('postal_code')->nullable(); + $table->string('emergency_contact_name')->nullable(); + $table->string('emergency_contact_relationship')->nullable(); + $table->string('emergency_contact_number')->nullable(); + + // Banking Information + $table->string('bank_name')->nullable(); + $table->string('account_holder_name')->nullable(); + $table->string('account_number')->nullable(); + $table->string('bank_identifier_code')->nullable(); // BIC/SWIFT + $table->string('bank_branch')->nullable(); + $table->string('tax_payer_id')->nullable(); + + // System fields + $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employees'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_23_095308_create_employee_documents_table.php b/database/migrations/2025_07_23_095308_create_employee_documents_table.php new file mode 100644 index 000000000..1fd698343 --- /dev/null +++ b/database/migrations/2025_07_23_095308_create_employee_documents_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('document_type_id')->constrained('document_types')->onDelete('cascade'); + $table->string('file_path'); + $table->date('expiry_date')->nullable(); + $table->enum('verification_status', ['pending', 'verified', 'rejected'])->default('pending'); + $table->text('notes')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employee_documents'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_23_104501_create_award_types_table.php b/database/migrations/2025_07_23_104501_create_award_types_table.php new file mode 100644 index 000000000..90a2ddc73 --- /dev/null +++ b/database/migrations/2025_07_23_104501_create_award_types_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('award_types'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_23_104518_create_awards_table.php b/database/migrations/2025_07_23_104518_create_awards_table.php new file mode 100644 index 000000000..7a5a07e8e --- /dev/null +++ b/database/migrations/2025_07_23_104518_create_awards_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('award_type_id')->constrained('award_types')->onDelete('cascade'); + $table->date('award_date'); + $table->string('gift')->nullable(); + $table->decimal('monetary_value', 15, 2)->nullable(); + $table->text('description')->nullable(); + $table->string('certificate')->nullable(); + $table->string('photo')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('awards'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_24_064317_create_performance_indicator_categories_table.php b/database/migrations/2025_07_24_064317_create_performance_indicator_categories_table.php new file mode 100644 index 000000000..8a73d0c01 --- /dev/null +++ b/database/migrations/2025_07_24_064317_create_performance_indicator_categories_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('performance_indicator_categories'); + } +}; diff --git a/database/migrations/2025_07_24_064510_create_performance_indicators_table.php b/database/migrations/2025_07_24_064510_create_performance_indicators_table.php new file mode 100644 index 000000000..3b09fb767 --- /dev/null +++ b/database/migrations/2025_07_24_064510_create_performance_indicators_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('category_id')->constrained('performance_indicator_categories')->onDelete('cascade'); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('measurement_unit')->nullable(); + $table->string('target_value')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('performance_indicators'); + } +}; diff --git a/database/migrations/2025_07_24_064733_create_goal_types_table.php b/database/migrations/2025_07_24_064733_create_goal_types_table.php new file mode 100644 index 000000000..13f18097e --- /dev/null +++ b/database/migrations/2025_07_24_064733_create_goal_types_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('goal_types'); + } +}; diff --git a/database/migrations/2025_07_24_064815_create_employee_goals_table.php b/database/migrations/2025_07_24_064815_create_employee_goals_table.php new file mode 100644 index 000000000..2a1225422 --- /dev/null +++ b/database/migrations/2025_07_24_064815_create_employee_goals_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('goal_type_id')->constrained('goal_types')->onDelete('cascade'); + $table->string('title'); + $table->text('description')->nullable(); + $table->date('start_date'); + $table->date('end_date'); + $table->string('target')->nullable(); + $table->integer('progress')->default(0); + $table->string('status')->default('not_started'); // not_started, in_progress, completed + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employee_goals'); + } +}; diff --git a/database/migrations/2025_07_24_064911_create_review_cycles_table.php b/database/migrations/2025_07_24_064911_create_review_cycles_table.php new file mode 100644 index 000000000..2ae49247c --- /dev/null +++ b/database/migrations/2025_07_24_064911_create_review_cycles_table.php @@ -0,0 +1,32 @@ +id(); + $table->string('name'); + $table->string('frequency'); // Monthly, Quarterly, Semi-Annual, Annual + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('review_cycles'); + } +}; diff --git a/database/migrations/2025_07_24_065125_create_employee_reviews_table.php b/database/migrations/2025_07_24_065125_create_employee_reviews_table.php new file mode 100644 index 000000000..d390bacb2 --- /dev/null +++ b/database/migrations/2025_07_24_065125_create_employee_reviews_table.php @@ -0,0 +1,47 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('reviewer_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('review_cycle_id')->constrained('review_cycles')->onDelete('cascade'); + $table->date('review_date'); + $table->date('completion_date')->nullable(); + $table->decimal('overall_rating', 3, 1)->nullable(); + $table->text('comments')->nullable(); + $table->string('status')->default('scheduled'); // scheduled, in_progress, completed + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Table for storing individual indicator ratings + Schema::create('employee_review_ratings', function (Blueprint $table) { + $table->id(); + $table->foreignId('employee_review_id')->constrained()->onDelete('cascade'); + $table->foreignId('performance_indicator_id')->constrained()->onDelete('cascade'); + $table->decimal('rating', 3, 1); + $table->text('comments')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employee_review_ratings'); + Schema::dropIfExists('employee_reviews'); + } +}; diff --git a/database/migrations/2025_07_24_104501_create_resignations_table.php b/database/migrations/2025_07_24_104501_create_resignations_table.php new file mode 100644 index 000000000..b33cf1ac2 --- /dev/null +++ b/database/migrations/2025_07_24_104501_create_resignations_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->date('resignation_date'); + $table->date('last_working_day'); + $table->string('notice_period')->nullable(); + $table->string('reason')->nullable(); + $table->text('description')->nullable(); + $table->string('status')->default('pending'); // pending, approved, rejected, completed + $table->string('documents')->nullable(); // For storing document paths + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('approved_at')->nullable(); + $table->text('exit_feedback')->nullable(); + $table->boolean('exit_interview_conducted')->default(false); + $table->date('exit_interview_date')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('resignations'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_24_104518_create_promotions_table.php b/database/migrations/2025_07_24_104518_create_promotions_table.php new file mode 100644 index 000000000..9d9c494f4 --- /dev/null +++ b/database/migrations/2025_07_24_104518_create_promotions_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->string('previous_designation')->nullable(); + $table->foreignId('designation_id')->constrained('designations')->onDelete('cascade'); + $table->date('promotion_date'); + $table->date('effective_date'); + $table->decimal('salary_adjustment', 15, 2)->nullable(); + $table->text('reason')->nullable(); + $table->string('document')->nullable(); + $table->enum('status', ['pending', 'approved', 'rejected'])->default('pending'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('promotions'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_25_104501_create_terminations_table.php b/database/migrations/2025_07_25_104501_create_terminations_table.php new file mode 100644 index 000000000..ac9f5ddfb --- /dev/null +++ b/database/migrations/2025_07_25_104501_create_terminations_table.php @@ -0,0 +1,42 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->string('termination_type'); // voluntary, involuntary, layoff, retirement, etc. + $table->date('termination_date'); + $table->date('notice_date'); + $table->string('notice_period')->nullable(); + $table->string('reason')->nullable(); + $table->text('description')->nullable(); + $table->string('status')->default('planned'); // planned, in progress, completed + $table->string('documents')->nullable(); // For storing document paths + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('approved_at')->nullable(); + $table->boolean('exit_interview_conducted')->default(false); + $table->date('exit_interview_date')->nullable(); + $table->text('exit_feedback')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('terminations'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_26_104501_create_warnings_table.php b/database/migrations/2025_07_26_104501_create_warnings_table.php new file mode 100644 index 000000000..2ced6a74b --- /dev/null +++ b/database/migrations/2025_07_26_104501_create_warnings_table.php @@ -0,0 +1,47 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('warning_by')->constrained('users')->onDelete('cascade'); + $table->string('warning_type'); // attendance, performance, conduct, etc. + $table->string('subject'); + $table->string('severity'); // verbal, written, final + $table->date('warning_date'); + $table->text('description')->nullable(); + $table->string('status')->default('draft'); // draft, issued, acknowledged, expired + $table->string('documents')->nullable(); // For storing document paths + $table->date('acknowledgment_date')->nullable(); + $table->text('employee_response')->nullable(); + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('approved_at')->nullable(); + $table->date('expiry_date')->nullable(); + $table->boolean('has_improvement_plan')->default(false); + $table->text('improvement_plan_goals')->nullable(); + $table->date('improvement_plan_start_date')->nullable(); + $table->date('improvement_plan_end_date')->nullable(); + $table->text('improvement_plan_progress')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('warnings'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_27_104501_create_trips_table.php b/database/migrations/2025_07_27_104501_create_trips_table.php new file mode 100644 index 000000000..23abfd9ab --- /dev/null +++ b/database/migrations/2025_07_27_104501_create_trips_table.php @@ -0,0 +1,61 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->string('purpose'); + $table->string('destination'); + $table->date('start_date'); + $table->date('end_date'); + $table->text('description')->nullable(); + $table->text('expected_outcomes')->nullable(); + $table->string('status')->default('planned'); // planned, ongoing, completed, cancelled + $table->string('documents')->nullable(); // For storing document paths + $table->decimal('advance_amount', 15, 2)->nullable(); + $table->string('advance_status')->nullable(); // requested, approved, paid, reconciled + $table->decimal('total_expenses', 15, 2)->nullable(); + $table->string('reimbursement_status')->nullable(); // pending, approved, paid + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('approved_at')->nullable(); + $table->text('trip_report')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create trip expenses table + Schema::create('trip_expenses', function (Blueprint $table) { + $table->id(); + $table->foreignId('trip_id')->constrained('trips')->onDelete('cascade'); + $table->string('expense_type'); // transportation, accommodation, meals, etc. + $table->date('expense_date'); + $table->decimal('amount', 15, 2); + $table->string('currency')->default('USD'); + $table->text('description')->nullable(); + $table->string('receipt')->nullable(); // For storing receipt file path + $table->boolean('is_reimbursable')->default(true); + $table->string('status')->default('pending'); // pending, approved, rejected + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('trip_expenses'); + Schema::dropIfExists('trips'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_071158_create_complaints_table.php b/database/migrations/2025_07_28_071158_create_complaints_table.php new file mode 100644 index 000000000..686355f4f --- /dev/null +++ b/database/migrations/2025_07_28_071158_create_complaints_table.php @@ -0,0 +1,45 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); // Complainant + $table->foreignId('against_employee_id')->nullable()->constrained('users')->nullOnDelete(); // Against whom + $table->string('complaint_type'); // harassment, discrimination, workplace conditions, etc. + $table->string('subject'); + $table->date('complaint_date'); + $table->text('description')->nullable(); + $table->string('status')->default('submitted'); // submitted, under investigation, resolved, dismissed + $table->string('documents')->nullable(); // For storing document paths + $table->boolean('is_anonymous')->default(false); + $table->foreignId('assigned_to')->nullable()->constrained('users')->nullOnDelete(); // HR personnel assigned + $table->date('resolution_deadline')->nullable(); + $table->text('investigation_notes')->nullable(); + $table->text('resolution_action')->nullable(); + $table->date('resolution_date')->nullable(); + $table->text('follow_up_action')->nullable(); + $table->date('follow_up_date')->nullable(); + $table->text('feedback')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('complaints'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_071653_create_employee_transfers_table.php b/database/migrations/2025_07_28_071653_create_employee_transfers_table.php new file mode 100644 index 000000000..19bf9af81 --- /dev/null +++ b/database/migrations/2025_07_28_071653_create_employee_transfers_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('from_branch_id')->nullable()->constrained('branches')->nullOnDelete(); + $table->foreignId('to_branch_id')->nullable()->constrained('branches')->nullOnDelete(); + $table->foreignId('from_department_id')->nullable()->constrained('departments')->nullOnDelete(); + $table->foreignId('to_department_id')->nullable()->constrained('departments')->nullOnDelete(); + $table->foreignId('from_designation_id')->nullable()->constrained('designations')->nullOnDelete(); + $table->foreignId('to_designation_id')->nullable()->constrained('designations')->nullOnDelete(); + $table->date('transfer_date'); + $table->date('effective_date'); + $table->text('reason')->nullable(); + $table->string('status')->default('pending'); // pending, approved, rejected + $table->string('documents')->nullable(); // For storing document paths + $table->foreignId('approved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('approved_at')->nullable(); + $table->text('notes')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employee_transfers'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_071922_create_holidays_table.php b/database/migrations/2025_07_28_071922_create_holidays_table.php new file mode 100644 index 000000000..304e0c645 --- /dev/null +++ b/database/migrations/2025_07_28_071922_create_holidays_table.php @@ -0,0 +1,45 @@ +id(); + $table->string('name'); + $table->date('start_date'); + $table->date('end_date')->nullable(); // For multi-day holidays + $table->string('category'); // national, religious, company-specific, etc. + $table->text('description')->nullable(); + $table->boolean('is_recurring')->default(false); // For annual recurring holidays + $table->boolean('is_paid')->default(true); + $table->boolean('is_half_day')->default(false); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create holiday_branch pivot table for location-based holidays + Schema::create('holiday_branch', function (Blueprint $table) { + $table->id(); + $table->foreignId('holiday_id')->constrained('holidays')->onDelete('cascade'); + $table->foreignId('branch_id')->constrained('branches')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('holiday_branch'); + Schema::dropIfExists('holidays'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_083505_create_announcements_table.php b/database/migrations/2025_07_28_083505_create_announcements_table.php new file mode 100644 index 000000000..75ca8a3ed --- /dev/null +++ b/database/migrations/2025_07_28_083505_create_announcements_table.php @@ -0,0 +1,66 @@ +id(); + $table->string('title'); + $table->string('category'); // company news, policy updates, events, etc. + $table->text('description')->nullable(); + $table->longText('content'); + $table->date('start_date'); + $table->date('end_date')->nullable(); + $table->string('attachments')->nullable(); // For storing attachment paths + $table->boolean('is_featured')->default(false); + $table->boolean('is_high_priority')->default(false); + $table->boolean('is_company_wide')->default(true); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create announcement_department pivot table + Schema::create('announcement_department', function (Blueprint $table) { + $table->id(); + $table->foreignId('announcement_id')->constrained('announcements')->onDelete('cascade'); + $table->foreignId('department_id')->constrained('departments')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create announcement_branch pivot table + Schema::create('announcement_branch', function (Blueprint $table) { + $table->id(); + $table->foreignId('announcement_id')->constrained('announcements')->onDelete('cascade'); + $table->foreignId('branch_id')->constrained('branches')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create announcement_views table to track which employees have viewed announcements + Schema::create('announcement_views', function (Blueprint $table) { + $table->id(); + $table->foreignId('announcement_id')->constrained('announcements')->onDelete('cascade'); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->timestamp('viewed_at'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('announcement_views'); + Schema::dropIfExists('announcement_branch'); + Schema::dropIfExists('announcement_department'); + Schema::dropIfExists('announcements'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_083753_create_asset_types_table.php b/database/migrations/2025_07_28_083753_create_asset_types_table.php new file mode 100644 index 000000000..fea654e72 --- /dev/null +++ b/database/migrations/2025_07_28_083753_create_asset_types_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('asset_types'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_083925_create_assets_table.php b/database/migrations/2025_07_28_083925_create_assets_table.php new file mode 100644 index 000000000..5f3d532d3 --- /dev/null +++ b/database/migrations/2025_07_28_083925_create_assets_table.php @@ -0,0 +1,94 @@ +id(); + $table->string('name'); + $table->foreignId('asset_type_id')->constrained('asset_types')->onDelete('restrict'); + $table->string('serial_number')->nullable(); + $table->string('asset_code')->nullable(); + $table->date('purchase_date')->nullable(); + $table->decimal('purchase_cost', 15, 2)->nullable(); + $table->string('status')->default('available'); // available, assigned, under_maintenance, disposed + $table->string('condition')->nullable(); // new, good, fair, poor + $table->text('description')->nullable(); + $table->string('location')->nullable(); + $table->string('supplier')->nullable(); + $table->string('warranty_info')->nullable(); + $table->date('warranty_expiry_date')->nullable(); + $table->string('images')->nullable(); // For storing image paths + $table->string('documents')->nullable(); // For storing document paths + $table->string('qr_code')->nullable(); // For storing QR code path + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create asset_assignments table + Schema::create('asset_assignments', function (Blueprint $table) { + $table->id(); + $table->foreignId('asset_id')->constrained('assets')->onDelete('cascade'); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->date('checkout_date'); + $table->date('expected_return_date')->nullable(); + $table->date('checkin_date')->nullable(); + $table->string('checkout_condition')->nullable(); + $table->string('checkin_condition')->nullable(); + $table->text('notes')->nullable(); + $table->boolean('is_acknowledged')->default(false); + $table->timestamp('acknowledged_at')->nullable(); + $table->foreignId('assigned_by')->constrained('users')->onDelete('cascade'); + $table->foreignId('received_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + }); + + // Create asset_maintenances table + Schema::create('asset_maintenances', function (Blueprint $table) { + $table->id(); + $table->foreignId('asset_id')->constrained('assets')->onDelete('cascade'); + $table->string('maintenance_type'); // repair, preventive, calibration, etc. + $table->date('start_date'); + $table->date('end_date')->nullable(); + $table->decimal('cost', 15, 2)->nullable(); + $table->string('status')->default('scheduled'); // scheduled, in_progress, completed, cancelled + $table->text('details')->nullable(); + $table->text('completion_notes')->nullable(); + $table->string('supplier')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create asset_depreciations table + Schema::create('asset_depreciations', function (Blueprint $table) { + $table->id(); + $table->foreignId('asset_id')->constrained('assets')->onDelete('cascade'); + $table->string('method'); // straight_line, reducing_balance, etc. + $table->integer('useful_life_years'); + $table->decimal('salvage_value', 15, 2)->nullable(); + $table->decimal('current_value', 15, 2); + $table->date('last_calculated_date'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('asset_depreciations'); + Schema::dropIfExists('asset_maintenances'); + Schema::dropIfExists('asset_assignments'); + Schema::dropIfExists('assets'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_084101_create_training_types_table.php b/database/migrations/2025_07_28_084101_create_training_types_table.php new file mode 100644 index 000000000..f4cb4ebe1 --- /dev/null +++ b/database/migrations/2025_07_28_084101_create_training_types_table.php @@ -0,0 +1,40 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->foreignId('branch_id')->constrained('branches')->onDelete('cascade'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create training_type_department pivot table + Schema::create('training_type_department', function (Blueprint $table) { + $table->id(); + $table->foreignId('training_type_id')->constrained('training_types')->onDelete('cascade'); + $table->foreignId('department_id')->constrained('departments')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('training_type_department'); + Schema::dropIfExists('training_types'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_084204_create_training_programs_table.php b/database/migrations/2025_07_28_084204_create_training_programs_table.php new file mode 100644 index 000000000..96efadb2f --- /dev/null +++ b/database/migrations/2025_07_28_084204_create_training_programs_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('name'); + $table->foreignId('training_type_id')->constrained('training_types')->onDelete('restrict'); + $table->text('description')->nullable(); + $table->integer('duration')->nullable(); // Duration in hours + $table->decimal('cost', 15, 2)->nullable(); + $table->integer('capacity')->nullable(); + $table->string('status')->default('draft'); // draft, active, completed, cancelled + $table->string('materials')->nullable(); // For storing training materials paths + $table->text('prerequisites')->nullable(); + $table->boolean('is_mandatory')->default(false); + $table->boolean('is_self_enrollment')->default(false); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('training_programs'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_084319_create_training_sessions_table.php b/database/migrations/2025_07_28_084319_create_training_sessions_table.php new file mode 100644 index 000000000..c19796f8f --- /dev/null +++ b/database/migrations/2025_07_28_084319_create_training_sessions_table.php @@ -0,0 +1,60 @@ +id(); + $table->foreignId('training_program_id')->constrained('training_programs')->onDelete('cascade'); + $table->string('name')->nullable(); + $table->dateTime('start_date'); + $table->dateTime('end_date'); + $table->string('location')->nullable(); // Physical or virtual location + $table->string('location_type')->default('physical'); // physical, virtual + $table->string('meeting_link')->nullable(); // For virtual sessions + $table->string('status')->default('scheduled'); // scheduled, in_progress, completed, cancelled + $table->text('notes')->nullable(); + $table->boolean('is_recurring')->default(false); + $table->string('recurrence_pattern')->nullable(); // daily, weekly, monthly + $table->integer('recurrence_count')->nullable(); // Number of recurrences + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create training_session_trainer pivot table + Schema::create('training_session_trainer', function (Blueprint $table) { + $table->id(); + $table->foreignId('training_session_id')->constrained('training_sessions')->onDelete('cascade'); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); // Trainer/instructor + $table->timestamps(); + }); + + // Create training_session_attendance table + Schema::create('training_session_attendance', function (Blueprint $table) { + $table->id(); + $table->foreignId('training_session_id')->constrained('training_sessions')->onDelete('cascade'); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->boolean('is_present')->default(false); + $table->text('notes')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('training_session_attendance'); + Schema::dropIfExists('training_session_trainer'); + Schema::dropIfExists('training_sessions'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_084441_create_employee_trainings_table.php b/database/migrations/2025_07_28_084441_create_employee_trainings_table.php new file mode 100644 index 000000000..53682fb2d --- /dev/null +++ b/database/migrations/2025_07_28_084441_create_employee_trainings_table.php @@ -0,0 +1,67 @@ +id(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('training_program_id')->constrained('training_programs')->onDelete('cascade'); + $table->string('status')->default('assigned'); // assigned, in_progress, completed, failed + $table->date('assigned_date'); + $table->date('completion_date')->nullable(); + $table->string('certification')->nullable(); // Certification details or path to certificate file + $table->decimal('score', 5, 2)->nullable(); // Score out of 100 + $table->boolean('is_passed')->nullable(); + $table->text('feedback')->nullable(); // Employee feedback on training + $table->text('notes')->nullable(); + $table->foreignId('assigned_by')->constrained('users')->onDelete('cascade'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create training_assessments table + Schema::create('training_assessments', function (Blueprint $table) { + $table->id(); + $table->foreignId('training_program_id')->constrained('training_programs')->onDelete('cascade'); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('type'); // quiz, practical, presentation + $table->decimal('passing_score', 5, 2)->default(70.00); // Default passing score is 70% + $table->text('criteria')->nullable(); // Assessment criteria + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + + // Create employee_assessment_results table + Schema::create('employee_assessment_results', function (Blueprint $table) { + $table->id(); + $table->foreignId('employee_training_id')->constrained('employee_trainings')->onDelete('cascade'); + $table->foreignId('training_assessment_id')->constrained('training_assessments')->onDelete('cascade'); + $table->decimal('score', 5, 2); + $table->boolean('is_passed'); + $table->text('feedback')->nullable(); + $table->date('assessment_date'); + $table->foreignId('assessed_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('employee_assessment_results'); + Schema::dropIfExists('training_assessments'); + Schema::dropIfExists('employee_trainings'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_084640_create_job_categories_table.php b/database/migrations/2025_07_28_084640_create_job_categories_table.php new file mode 100644 index 000000000..715c1ceec --- /dev/null +++ b/database/migrations/2025_07_28_084640_create_job_categories_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_categories'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_084648_create_job_requisitions_table.php b/database/migrations/2025_07_28_084648_create_job_requisitions_table.php new file mode 100644 index 000000000..930b4c0a8 --- /dev/null +++ b/database/migrations/2025_07_28_084648_create_job_requisitions_table.php @@ -0,0 +1,44 @@ +id(); + $table->string('requisition_code')->unique(); + $table->string('title'); + $table->foreignId('job_category_id')->constrained('job_categories')->onDelete('cascade'); + $table->foreignId('department_id')->nullable()->constrained('departments')->onDelete('set null'); + $table->integer('positions_count')->default(1); + $table->decimal('budget_min', 15, 2)->nullable(); + $table->decimal('budget_max', 15, 2)->nullable(); + $table->text('skills_required')->nullable(); + $table->text('education_required')->nullable(); + $table->text('experience_required')->nullable(); + $table->text('description')->nullable(); + $table->text('responsibilities')->nullable(); + $table->enum('status', ['Draft', 'Pending Approval', 'Approved', 'On Hold', 'Closed'])->default('Draft'); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('approval_date')->nullable(); + $table->enum('priority', ['Low', 'Medium', 'High'])->default('Medium'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('job_requisitions'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_093341_create_job_types_table.php b/database/migrations/2025_07_28_093341_create_job_types_table.php new file mode 100644 index 000000000..96a2d6e20 --- /dev/null +++ b/database/migrations/2025_07_28_093341_create_job_types_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('job_types'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_093500_create_job_locations_table.php b/database/migrations/2025_07_28_093500_create_job_locations_table.php new file mode 100644 index 000000000..72ff7f4a7 --- /dev/null +++ b/database/migrations/2025_07_28_093500_create_job_locations_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('name'); + $table->text('address')->nullable(); + $table->string('city')->nullable(); + $table->string('state')->nullable(); + $table->string('country')->nullable(); + $table->string('postal_code')->nullable(); + $table->boolean('is_remote')->default(false); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('job_locations'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_093643_create_job_postings_table.php b/database/migrations/2025_07_28_093643_create_job_postings_table.php new file mode 100644 index 000000000..d3c1dd929 --- /dev/null +++ b/database/migrations/2025_07_28_093643_create_job_postings_table.php @@ -0,0 +1,40 @@ +id(); + $table->foreignId('requisition_id')->constrained('job_requisitions')->onDelete('cascade'); + $table->string('job_code')->unique(); + $table->string('title'); + $table->foreignId('job_type_id')->constrained('job_types')->onDelete('cascade'); + $table->foreignId('location_id')->constrained('job_locations')->onDelete('cascade'); + $table->foreignId('department_id')->nullable()->constrained('departments')->onDelete('set null'); + $table->integer('min_experience')->default(0); + $table->integer('max_experience')->nullable(); + $table->decimal('min_salary', 15, 2)->nullable(); + $table->decimal('max_salary', 15, 2)->nullable(); + $table->text('description')->nullable(); + $table->text('requirements')->nullable(); + $table->text('benefits')->nullable(); + $table->date('application_deadline')->nullable(); + $table->boolean('is_published')->default(false); + $table->timestamp('publish_date')->nullable(); + $table->boolean('is_featured')->default(false); + $table->enum('status', ['Draft', 'Published', 'Closed'])->default('Draft'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('job_postings'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_093809_create_candidate_sources_table.php b/database/migrations/2025_07_28_093809_create_candidate_sources_table.php new file mode 100644 index 000000000..cffc9cb5a --- /dev/null +++ b/database/migrations/2025_07_28_093809_create_candidate_sources_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('candidate_sources'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_093919_create_candidates_table.php b/database/migrations/2025_07_28_093919_create_candidates_table.php new file mode 100644 index 000000000..ba218ae83 --- /dev/null +++ b/database/migrations/2025_07_28_093919_create_candidates_table.php @@ -0,0 +1,43 @@ +id(); + $table->foreignId('job_id')->constrained('job_postings')->onDelete('cascade'); + $table->foreignId('source_id')->constrained('candidate_sources')->onDelete('cascade'); + $table->string('first_name'); + $table->string('last_name'); + $table->string('email'); + $table->string('phone')->nullable(); + $table->string('current_company')->nullable(); + $table->string('current_position')->nullable(); + $table->integer('experience_years')->default(0); + $table->decimal('current_salary', 15, 2)->nullable(); + $table->decimal('expected_salary', 15, 2)->nullable(); + $table->string('notice_period')->nullable(); + $table->string('resume_path')->nullable(); + $table->string('cover_letter_path')->nullable(); + $table->text('skills')->nullable(); + $table->text('education')->nullable(); + $table->string('portfolio_url')->nullable(); + $table->string('linkedin_url')->nullable(); + $table->foreignId('referral_employee_id')->nullable()->constrained('users')->onDelete('set null'); + $table->enum('status', ['New', 'Screening', 'Interview', 'Offer', 'Hired', 'Rejected'])->default('New'); + $table->date('application_date'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('candidates'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_093928_create_interview_types_table.php b/database/migrations/2025_07_28_093928_create_interview_types_table.php new file mode 100644 index 000000000..fa1bce0b5 --- /dev/null +++ b/database/migrations/2025_07_28_093928_create_interview_types_table.php @@ -0,0 +1,25 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('interview_types'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094054_create_interview_rounds_table.php b/database/migrations/2025_07_28_094054_create_interview_rounds_table.php new file mode 100644 index 000000000..dcc7f931f --- /dev/null +++ b/database/migrations/2025_07_28_094054_create_interview_rounds_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('job_id')->constrained('job_postings')->onDelete('cascade'); + $table->string('name'); + $table->integer('sequence_number'); + $table->text('description')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('interview_rounds'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094102_create_interviews_table.php b/database/migrations/2025_07_28_094102_create_interviews_table.php new file mode 100644 index 000000000..1f678d7c1 --- /dev/null +++ b/database/migrations/2025_07_28_094102_create_interviews_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('candidate_id')->constrained('candidates')->onDelete('cascade'); + $table->foreignId('job_id')->constrained('job_postings')->onDelete('cascade'); + $table->foreignId('round_id')->constrained('interview_rounds')->onDelete('cascade'); + $table->foreignId('interview_type_id')->constrained('interview_types')->onDelete('cascade'); + $table->date('scheduled_date'); + $table->time('scheduled_time'); + $table->integer('duration')->default(60); + $table->string('location')->nullable(); + $table->string('meeting_link')->nullable(); + $table->json('interviewers'); + $table->enum('status', ['Scheduled', 'Completed', 'Cancelled', 'No-show'])->default('Scheduled'); + $table->boolean('feedback_submitted')->default(false); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('interviews'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094313_create_interview_feedback_table.php b/database/migrations/2025_07_28_094313_create_interview_feedback_table.php new file mode 100644 index 000000000..4e3897250 --- /dev/null +++ b/database/migrations/2025_07_28_094313_create_interview_feedback_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('interview_id')->constrained('interviews')->onDelete('cascade'); + $table->string('interviewer_id')->nullable(); + $table->integer('technical_rating')->nullable(); + $table->integer('communication_rating')->nullable(); + $table->integer('cultural_fit_rating')->nullable(); + $table->integer('overall_rating')->nullable(); + $table->text('strengths')->nullable(); + $table->text('weaknesses')->nullable(); + $table->text('comments')->nullable(); + $table->enum('recommendation', ['Strong Hire', 'Hire', 'Maybe', 'Reject', 'Strong Reject'])->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('interview_feedback'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094321_create_candidate_assessments_table.php b/database/migrations/2025_07_28_094321_create_candidate_assessments_table.php new file mode 100644 index 000000000..fbf7c95ee --- /dev/null +++ b/database/migrations/2025_07_28_094321_create_candidate_assessments_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('candidate_id')->constrained('candidates')->onDelete('cascade'); + $table->string('assessment_name'); + $table->integer('score')->nullable(); + $table->integer('max_score')->nullable(); + $table->enum('pass_fail_status', ['Pass', 'Fail', 'Pending'])->default('Pending'); + $table->text('comments')->nullable(); + $table->foreignId('conducted_by')->constrained('users')->onDelete('cascade'); + $table->date('assessment_date'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('candidate_assessments'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094334_create_offer_templates_table.php b/database/migrations/2025_07_28_094334_create_offer_templates_table.php new file mode 100644 index 000000000..e72aa1184 --- /dev/null +++ b/database/migrations/2025_07_28_094334_create_offer_templates_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('name'); + $table->longText('template_content'); + $table->json('variables')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('offer_templates'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094647_create_offers_table.php b/database/migrations/2025_07_28_094647_create_offers_table.php new file mode 100644 index 000000000..b01ab3d68 --- /dev/null +++ b/database/migrations/2025_07_28_094647_create_offers_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('candidate_id')->constrained('candidates')->onDelete('cascade'); + $table->foreignId('job_id')->constrained('job_postings')->onDelete('cascade'); + $table->date('offer_date'); + $table->string('position'); + $table->foreignId('department_id')->nullable()->constrained('departments')->onDelete('set null'); + $table->decimal('salary', 15, 2); + $table->decimal('bonus', 15, 2)->nullable(); + $table->string('equity')->nullable(); + $table->text('benefits')->nullable(); + $table->date('start_date'); + $table->date('expiration_date'); + $table->string('offer_letter_path')->nullable(); + $table->enum('status', ['Draft', 'Sent', 'Accepted', 'Negotiating', 'Declined', 'Expired'])->default('Draft'); + $table->date('response_date')->nullable(); + $table->text('decline_reason')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('offers'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094656_create_onboarding_checklists_table.php b/database/migrations/2025_07_28_094656_create_onboarding_checklists_table.php new file mode 100644 index 000000000..14fdb7148 --- /dev/null +++ b/database/migrations/2025_07_28_094656_create_onboarding_checklists_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->boolean('is_default')->default(false); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('onboarding_checklists'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094702_create_checklist_items_table.php b/database/migrations/2025_07_28_094702_create_checklist_items_table.php new file mode 100644 index 000000000..1229e2c55 --- /dev/null +++ b/database/migrations/2025_07_28_094702_create_checklist_items_table.php @@ -0,0 +1,30 @@ +id(); + $table->foreignId('checklist_id')->constrained('onboarding_checklists')->onDelete('cascade'); + $table->string('task_name'); + $table->text('description')->nullable(); + $table->enum('category', ['Documentation', 'IT Setup', 'Training', 'HR', 'Facilities', 'Other'])->default('Other'); + $table->string('assigned_to_role')->nullable(); + $table->integer('due_day')->default(1); + $table->boolean('is_required')->default(true); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('checklist_items'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094712_create_candidate_onboarding_table.php b/database/migrations/2025_07_28_094712_create_candidate_onboarding_table.php new file mode 100644 index 000000000..36983af6e --- /dev/null +++ b/database/migrations/2025_07_28_094712_create_candidate_onboarding_table.php @@ -0,0 +1,27 @@ +id(); + $table->foreignId('candidate_id')->constrained('candidates')->onDelete('cascade'); + $table->foreignId('checklist_id')->constrained('onboarding_checklists')->onDelete('cascade'); + $table->date('start_date'); + $table->foreignId('buddy_employee_id')->nullable()->constrained('users')->onDelete('set null'); + $table->enum('status', ['Pending', 'In Progress', 'Completed'])->default('Pending'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('candidate_onboarding'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094945_create_meeting_types_table.php b/database/migrations/2025_07_28_094945_create_meeting_types_table.php new file mode 100644 index 000000000..0bba987d5 --- /dev/null +++ b/database/migrations/2025_07_28_094945_create_meeting_types_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('color')->default('#3B82F6'); + $table->integer('default_duration')->default(60); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('meeting_types'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_094954_create_meeting_rooms_table.php b/database/migrations/2025_07_28_094954_create_meeting_rooms_table.php new file mode 100644 index 000000000..45f958fbf --- /dev/null +++ b/database/migrations/2025_07_28_094954_create_meeting_rooms_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->enum('type', ['Physical', 'Virtual'])->default('Physical'); + $table->string('location')->nullable(); + $table->integer('capacity'); + $table->json('equipment')->nullable(); + $table->string('booking_url')->nullable(); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('meeting_rooms'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095000_create_meetings_table.php b/database/migrations/2025_07_28_095000_create_meetings_table.php new file mode 100644 index 000000000..27ac8cba4 --- /dev/null +++ b/database/migrations/2025_07_28_095000_create_meetings_table.php @@ -0,0 +1,35 @@ +id(); + $table->string('title'); + $table->text('description')->nullable(); + $table->foreignId('type_id')->constrained('meeting_types')->onDelete('cascade'); + $table->foreignId('room_id')->nullable()->constrained('meeting_rooms')->onDelete('set null'); + $table->date('meeting_date'); + $table->time('start_time'); + $table->time('end_time'); + $table->integer('duration'); + $table->text('agenda')->nullable(); + $table->enum('status', ['Scheduled', 'In Progress', 'Completed', 'Cancelled'])->default('Scheduled'); + $table->enum('recurrence', ['None', 'Daily', 'Weekly', 'Monthly'])->default('None'); + $table->date('recurrence_end_date')->nullable(); + $table->foreignId('organizer_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('meetings'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095007_create_meeting_attendees_table.php b/database/migrations/2025_07_28_095007_create_meeting_attendees_table.php new file mode 100644 index 000000000..bcd1a0c17 --- /dev/null +++ b/database/migrations/2025_07_28_095007_create_meeting_attendees_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('meeting_id')->constrained('meetings')->onDelete('cascade'); + $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); + $table->enum('type', ['Required', 'Optional'])->default('Required'); + $table->enum('rsvp_status', ['Pending', 'Accepted', 'Declined', 'Tentative'])->default('Pending'); + $table->enum('attendance_status', ['Not Attended', 'Present', 'Late', 'Left Early'])->default('Not Attended'); + $table->timestamp('rsvp_date')->nullable(); + $table->text('decline_reason')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + + $table->unique(['meeting_id', 'user_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('meeting_attendees'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095015_create_meeting_minutes_table.php b/database/migrations/2025_07_28_095015_create_meeting_minutes_table.php new file mode 100644 index 000000000..950d3723a --- /dev/null +++ b/database/migrations/2025_07_28_095015_create_meeting_minutes_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('meeting_id')->constrained('meetings')->onDelete('cascade'); + $table->string('topic'); + $table->longText('content'); + $table->enum('type', ['Discussion', 'Decision', 'Action Item', 'Note'])->default('Discussion'); + $table->foreignId('recorded_by')->constrained('users')->onDelete('cascade'); + $table->timestamp('recorded_at'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('meeting_minutes'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095022_create_action_items_table.php b/database/migrations/2025_07_28_095022_create_action_items_table.php new file mode 100644 index 000000000..f91ea91fd --- /dev/null +++ b/database/migrations/2025_07_28_095022_create_action_items_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('meeting_id')->constrained('meetings')->onDelete('cascade'); + $table->string('title'); + $table->text('description')->nullable(); + $table->foreignId('assigned_to')->constrained('users')->onDelete('cascade'); + $table->date('due_date'); + $table->enum('priority', ['Low', 'Medium', 'High', 'Critical'])->default('Medium'); + $table->enum('status', ['Not Started', 'In Progress', 'Completed', 'Overdue'])->default('Not Started'); + $table->integer('progress_percentage')->default(0); + $table->text('notes')->nullable(); + $table->date('completed_date')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('action_items'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095332_create_contract_types_table.php b/database/migrations/2025_07_28_095332_create_contract_types_table.php new file mode 100644 index 000000000..95c89dabe --- /dev/null +++ b/database/migrations/2025_07_28_095332_create_contract_types_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->integer('default_duration_months')->nullable(); + $table->integer('probation_period_months')->default(3); + $table->integer('notice_period_days')->default(30); + $table->boolean('is_renewable')->default(true); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('contract_types'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095345_create_employee_contracts_table.php b/database/migrations/2025_07_28_095345_create_employee_contracts_table.php new file mode 100644 index 000000000..71afda810 --- /dev/null +++ b/database/migrations/2025_07_28_095345_create_employee_contracts_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('contract_number')->unique(); + $table->foreignId('employee_id')->constrained('users')->onDelete('cascade'); + $table->foreignId('contract_type_id')->constrained('contract_types')->onDelete('cascade'); + $table->date('start_date'); + $table->date('end_date')->nullable(); + $table->decimal('basic_salary', 10, 2); + $table->json('allowances')->nullable(); + $table->json('benefits')->nullable(); + $table->text('terms_conditions')->nullable(); + $table->enum('status', ['Draft', 'Pending Approval', 'Active', 'Expired', 'Terminated', 'Renewed'])->default('Draft'); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('approved_at')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('employee_contracts'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095424_create_contract_renewals_table.php b/database/migrations/2025_07_28_095424_create_contract_renewals_table.php new file mode 100644 index 000000000..88e084845 --- /dev/null +++ b/database/migrations/2025_07_28_095424_create_contract_renewals_table.php @@ -0,0 +1,38 @@ +id(); + $table->foreignId('contract_id')->constrained('employee_contracts')->onDelete('cascade'); + $table->string('renewal_number'); + $table->date('current_end_date'); + $table->date('new_start_date'); + $table->date('new_end_date'); + $table->decimal('new_basic_salary', 10, 2); + $table->json('new_allowances')->nullable(); + $table->json('new_benefits')->nullable(); + $table->text('new_terms_conditions')->nullable(); + $table->text('changes_summary')->nullable(); + $table->enum('status', ['Pending', 'Approved', 'Rejected', 'Processed'])->default('Pending'); + $table->text('reason')->nullable(); + $table->foreignId('requested_by')->constrained('users')->onDelete('cascade'); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('approved_at')->nullable(); + $table->text('approval_notes')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('contract_renewals'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095432_create_contract_templates_table.php b/database/migrations/2025_07_28_095432_create_contract_templates_table.php new file mode 100644 index 000000000..7d7319273 --- /dev/null +++ b/database/migrations/2025_07_28_095432_create_contract_templates_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->foreignId('contract_type_id')->constrained('contract_types')->onDelete('cascade'); + $table->longText('template_content'); + $table->json('variables')->nullable(); + $table->json('clauses')->nullable(); + $table->boolean('is_default')->default(false); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('contract_templates'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095440_create_document_categories_table.php b/database/migrations/2025_07_28_095440_create_document_categories_table.php new file mode 100644 index 000000000..6158d1898 --- /dev/null +++ b/database/migrations/2025_07_28_095440_create_document_categories_table.php @@ -0,0 +1,29 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->string('color', 7)->default('#3B82F6'); + $table->string('icon')->default('FileText'); + $table->integer('sort_order')->default(0); + $table->boolean('is_mandatory')->default(false); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('document_categories'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095447_create_hr_documents_table.php b/database/migrations/2025_07_28_095447_create_hr_documents_table.php new file mode 100644 index 000000000..c999e3f9f --- /dev/null +++ b/database/migrations/2025_07_28_095447_create_hr_documents_table.php @@ -0,0 +1,39 @@ +id(); + $table->string('title'); + $table->text('description')->nullable(); + $table->foreignId('category_id')->constrained('document_categories')->onDelete('cascade'); + $table->string('file_name'); + $table->string('file_path'); + $table->string('file_type'); + $table->integer('file_size'); + $table->string('version', 10)->default('1.0'); + + $table->enum('status', ['Draft', 'Under Review', 'Approved', 'Published', 'Archived', 'Expired'])->default('Draft'); + $table->date('effective_date')->nullable(); + $table->date('expiry_date')->nullable(); + $table->boolean('requires_acknowledgment')->default(false); + $table->integer('download_count')->default(0); + $table->foreignId('uploaded_by')->constrained('users')->onDelete('cascade'); + $table->foreignId('approved_by')->nullable()->constrained('users')->onDelete('set null'); + $table->timestamp('approved_at')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('hr_documents'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095505_create_document_acknowledgments_table.php b/database/migrations/2025_07_28_095505_create_document_acknowledgments_table.php new file mode 100644 index 000000000..2d500a6ab --- /dev/null +++ b/database/migrations/2025_07_28_095505_create_document_acknowledgments_table.php @@ -0,0 +1,34 @@ +id(); + $table->foreignId('document_id')->constrained('hr_documents')->onDelete('cascade'); + $table->foreignId('user_id')->constrained('users')->onDelete('cascade'); + $table->enum('status', ['Pending', 'Acknowledged', 'Overdue', 'Exempted'])->default('Pending'); + $table->timestamp('acknowledged_at')->nullable(); + $table->date('due_date')->nullable(); + $table->text('acknowledgment_note')->nullable(); + $table->string('ip_address')->nullable(); + $table->string('user_agent')->nullable(); + $table->foreignId('assigned_by')->constrained('users')->onDelete('cascade'); + $table->timestamp('assigned_at')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + + $table->unique(['document_id', 'user_id']); + }); + } + + public function down(): void + { + Schema::dropIfExists('document_acknowledgments'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_07_28_095522_create_document_templates_table.php b/database/migrations/2025_07_28_095522_create_document_templates_table.php new file mode 100644 index 000000000..d2dafac40 --- /dev/null +++ b/database/migrations/2025_07_28_095522_create_document_templates_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->foreignId('category_id')->constrained('document_categories')->onDelete('cascade'); + $table->longText('template_content'); + $table->json('placeholders')->nullable(); + $table->json('default_values')->nullable(); + $table->boolean('is_default')->default(false); + $table->string('file_format')->default('pdf'); + $table->string('status')->default('active'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('document_templates'); + } +}; \ No newline at end of file diff --git a/database/migrations/2025_08_28_100659_create_media_directories_table.php.php b/database/migrations/2025_08_28_100659_create_media_directories_table.php.php new file mode 100644 index 000000000..563abecf2 --- /dev/null +++ b/database/migrations/2025_08_28_100659_create_media_directories_table.php.php @@ -0,0 +1,31 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->foreignId('parent_id')->nullable()->constrained('media_directories')->onDelete('cascade'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('media_directories'); + } +}; diff --git a/database/migrations/2025_08_28_101613_add_plan_id_to_users_table.php b/database/migrations/2025_08_28_101613_add_plan_id_to_users_table.php new file mode 100644 index 000000000..0896b204f --- /dev/null +++ b/database/migrations/2025_08_28_101613_add_plan_id_to_users_table.php @@ -0,0 +1,32 @@ +foreign('plan_id')->references('id')->on('plans')->nullOnDelete(); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + if (isSaas() && Schema::hasColumn('users', 'plan_id')) { + $table->dropForeign(['plan_id']); + } + }); + } +}; diff --git a/database/migrations/2025_08_28_102125_create_media_table.php b/database/migrations/2025_08_28_102125_create_media_table.php new file mode 100644 index 000000000..d66038f66 --- /dev/null +++ b/database/migrations/2025_08_28_102125_create_media_table.php @@ -0,0 +1,47 @@ +id(); + $table->morphs('model'); + $table->uuid()->nullable()->unique(); + $table->string('collection_name'); + $table->string('name'); + $table->string('file_name'); + $table->string('mime_type')->nullable(); + $table->string('disk'); + $table->string('conversions_disk')->nullable(); + $table->unsignedBigInteger('size'); + $table->json('manipulations'); + $table->json('custom_properties'); + $table->json('generated_conversions'); + $table->json('responsive_images'); + $table->unsignedInteger('order_column')->nullable()->index(); + $table->unsignedBigInteger('directory_id')->nullable(); + $table->unsignedBigInteger('created_by')->nullable(); + + $table->foreign('directory_id')->references('id')->on('media_directories')->onDelete('set null'); + $table->foreign('created_by')->references('id')->on('users')->onDelete(action: 'set null'); + + $table->nullableTimestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('media'); + } +}; diff --git a/database/migrations/2025_10_29_105526_add_foreign_key_users_table.php b/database/migrations/2025_10_29_105526_add_foreign_key_users_table.php new file mode 100644 index 000000000..2eef0d857 --- /dev/null +++ b/database/migrations/2025_10_29_105526_add_foreign_key_users_table.php @@ -0,0 +1,33 @@ +dropForeign(['created_by']); + $table->foreign('created_by')->references('id')->on('users')->onDelete('cascade'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasTable('roles')) { + Schema::table('roles', function (Blueprint $table) { + $table->dropForeign(['created_by']); + }); + } + } +}; diff --git a/database/migrations/2025_11_24_054115_add_new_column_employee_table.php b/database/migrations/2025_11_24_054115_add_new_column_employee_table.php new file mode 100644 index 000000000..26c0f2845 --- /dev/null +++ b/database/migrations/2025_11_24_054115_add_new_column_employee_table.php @@ -0,0 +1,43 @@ +decimal('base_salary', 10, 2)->nullable()->after('date_of_joining'); + } + if (!Schema::hasColumn('employees', 'employee_status')) { + $table->enum('employee_status', ['active', 'inactive', 'terminated', 'probation']) + ->default('active') + ->after('employment_type'); + } + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasTable('employees')) { + Schema::table('employees', function (Blueprint $table) { + if (Schema::hasColumn('employees', 'base_salary')) { + $table->dropColumn('base_salary'); + } + if (Schema::hasColumn('employees', 'employee_status')) { + $table->dropColumn('employee_status'); + } + }); + } + } +}; diff --git a/database/migrations/2025_12_12_055922_add_field_emp_attedance_table.php b/database/migrations/2025_12_12_055922_add_field_emp_attedance_table.php new file mode 100644 index 000000000..3907f9257 --- /dev/null +++ b/database/migrations/2025_12_12_055922_add_field_emp_attedance_table.php @@ -0,0 +1,44 @@ +string('biometric_emp_id')->nullable()->after('employee_id'); + }); + } + + if (Schema::hasTable('attendance_records') && !Schema::hasColumn('attendance_records', 'biometric_id')) { + Schema::table('attendance_records', function (Blueprint $table) { + $table->string('biometric_id')->nullable()->after('employee_id'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasTable('employees') && Schema::hasColumn('employees', 'biometric_emp_id')) { + Schema::table('employees', function (Blueprint $table) { + $table->dropColumn('biometric_emp_id'); + }); + } + + if (Schema::hasTable('attendance_records') && Schema::hasColumn('attendance_records', 'biometric_id')) { + Schema::table('attendance_records', function (Blueprint $table) { + $table->dropColumn('biometric_id'); + }); + } + } +}; diff --git a/database/migrations/2025_12_17_054638_create_ip_restrictions_table.php b/database/migrations/2025_12_17_054638_create_ip_restrictions_table.php new file mode 100644 index 000000000..90a7d481b --- /dev/null +++ b/database/migrations/2025_12_17_054638_create_ip_restrictions_table.php @@ -0,0 +1,30 @@ +id(); + $table->string('ip_address'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('ip_restrictions'); + } +}; diff --git a/database/migrations/2025_12_23_101527_add_column_job_posting_table.php b/database/migrations/2025_12_23_101527_add_column_job_posting_table.php new file mode 100644 index 000000000..6bb9ac30d --- /dev/null +++ b/database/migrations/2025_12_23_101527_add_column_job_posting_table.php @@ -0,0 +1,96 @@ +dropForeign(['requisition_id']); + $table->dropColumn('requisition_id'); + } + + // Add new columns only if they don't exist + if (!Schema::hasColumn('job_postings', 'branch_id')) { + $table->foreignId('branch_id')->nullable()->after('location_id')->constrained('branches')->onDelete('set null'); + } + + if (!Schema::hasColumn('job_postings', 'department_id')) { + $table->foreignId('department_id')->nullable()->after('branch_id')->constrained('departments')->onDelete('set null'); + } + if (!Schema::hasColumn('job_postings', 'priority')) { + $table->enum('priority', ['Low', 'Medium', 'High'])->default('Medium')->after('is_featured'); + } + if (!Schema::hasColumn('job_postings', 'skills')) { + $table->json('skills')->nullable()->after('priority'); + } + if (!Schema::hasColumn('job_postings', 'start_date')) { + $table->date('start_date')->nullable()->after('benefits'); + } + if (!Schema::hasColumn('job_postings', 'positions')) { + $table->integer('positions')->default(1)->after('skills'); + } + + if (!Schema::hasColumn('job_postings', 'applicant')) { + $table->json('applicant')->nullable()->after('positions'); + } + if (!Schema::hasColumn('job_postings', 'visibility')) { + $table->json('visibility')->nullable()->after('applicant'); + } + if (!Schema::hasColumn('job_postings', 'code')) { + $table->string('code')->unique()->nullable()->after('visibility'); + } + if (!Schema::hasColumn('job_postings', 'custom_question')) { + $table->json('custom_question')->nullable()->after('code'); + } + if (!Schema::hasColumn('job_postings', 'application_type')) { + $table->enum('application_type', ['existing', 'custom'])->default('existing')->after('custom_question'); + } + if (!Schema::hasColumn('job_postings', 'application_url')) { + $table->string('application_url')->nullable()->after('application_type'); + } + + // Modify existing columns + $table->decimal('min_experience', 3, 1)->default(0)->change(); + $table->decimal('max_experience', 3, 1)->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('job_postings', function (Blueprint $table) { + // Add back foreign key columns if they don't exist + if (!Schema::hasColumn('job_postings', 'requisition_id')) { + $table->foreignId('requisition_id')->constrained('job_requisitions')->onDelete('cascade'); + } + if (!Schema::hasColumn('job_postings', 'department_id')) { + $table->foreignId('department_id')->nullable()->constrained('departments')->onDelete('set null'); + } + + // Drop added columns if they exist + $columnsToCheck = ['priority', 'skills', 'positions', 'start_date', 'applicant', 'visibility', 'code', 'custom_question', 'application_type', 'application_url', 'branch_id']; + foreach ($columnsToCheck as $column) { + if (Schema::hasColumn('job_postings', $column)) { + if ($column === 'branch_id') { + $table->dropForeign(['branch_id']); + } + $table->dropColumn($column); + } + } + + // Revert column changes + $table->integer('min_experience')->default(0)->change(); + $table->integer('max_experience')->nullable()->change(); + }); + } +}; diff --git a/database/migrations/2025_12_24_060736_create_custom_questions_table.php b/database/migrations/2025_12_24_060736_create_custom_questions_table.php new file mode 100644 index 000000000..89fa6b2ee --- /dev/null +++ b/database/migrations/2025_12_24_060736_create_custom_questions_table.php @@ -0,0 +1,31 @@ +id(); + $table->text('question'); + $table->boolean('required')->default(false); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('custom_questions'); + } +}; diff --git a/database/migrations/2025_12_30_045711_add_slug_column_user_table.php b/database/migrations/2025_12_30_045711_add_slug_column_user_table.php new file mode 100644 index 000000000..2b9b9b7da --- /dev/null +++ b/database/migrations/2025_12_30_045711_add_slug_column_user_table.php @@ -0,0 +1,32 @@ +string('slug')->nullable()->unique()->after('email'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasColumn('users', 'slug')) { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('slug'); + }); + } + } +}; diff --git a/database/migrations/2025_12_31_104657_add_columns_candidate_table.php b/database/migrations/2025_12_31_104657_add_columns_candidate_table.php new file mode 100644 index 000000000..eec902b1e --- /dev/null +++ b/database/migrations/2025_12_31_104657_add_columns_candidate_table.php @@ -0,0 +1,92 @@ +enum('gender', ['male', 'female', 'other'])->nullable()->after('phone'); + } + if (! Schema::hasColumn('candidates', 'date_of_birth')) { + $table->date('date_of_birth')->nullable()->after('gender'); + } + if (! Schema::hasColumn('candidates', 'address')) { + $table->string('address')->nullable()->after('date_of_birth'); + } + if (! Schema::hasColumn('candidates', 'city')) { + $table->string('city')->nullable()->after('address'); + } + if (! Schema::hasColumn('candidates', 'state')) { + $table->string('state')->nullable()->after('city'); + } + if (! Schema::hasColumn('candidates', 'zip_code')) { + $table->string('zip_code')->nullable()->after('state'); + } + if (! Schema::hasColumn('candidates', 'country')) { + $table->string('country')->nullable()->after('zip_code'); + } + if (! Schema::hasColumn('candidates', 'coverletter_message')) { + $table->text('coverletter_message')->nullable()->after('cover_letter_path'); + } + if (! Schema::hasColumn('candidates', 'rating')) { + $table->integer('rating')->nullable()->after('coverletter_message'); + } + if (! Schema::hasColumn('candidates', 'is_archive')) { + $table->boolean('is_archive')->nullable()->after('rating'); + } + if (! Schema::hasColumn('candidates', 'is_employee')) { + $table->boolean('is_employee')->default(0)->nullable()->after('is_archive'); + } + if (! Schema::hasColumn('candidates', 'custom_question')) { + $table->json('custom_question')->nullable()->after('is_employee'); + } + if (! Schema::hasColumn('candidates', 'terms_condition_check')) { + $table->enum('terms_condition_check', ['on', 'off'])->nullable()->after('custom_question'); + } + if (! Schema::hasColumn('candidates', 'final_salary')) { + $table->decimal('final_salary', 15, 2)->nullable()->after('expected_salary'); + } + if (! Schema::hasColumn('candidates', 'branch_id')) { + $table->unsignedBigInteger('branch_id')->nullable()->after('source_id'); + $table->foreign('branch_id')->references('id')->on('branches')->onDelete('cascade'); + } + if (! Schema::hasColumn('candidates', 'department_id')) { + $table->unsignedBigInteger('department_id')->nullable()->after('branch_id'); + $table->foreign('department_id')->references('id')->on('departments')->onDelete('cascade'); + } + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasTable('candidates')) { + Schema::table('candidates', function (Blueprint $table) { + $columnsToCheck = [ + 'gender', 'date_of_birth', 'address', 'city', 'state', + 'zip_code', 'country', 'coverletter_message', 'rating', + 'is_archive', 'is_employee', 'custom_question', 'terms_condition_check', + 'final_salary', 'branch_id', 'department_id' + ]; + + foreach ($columnsToCheck as $column) { + if (Schema::hasColumn('candidates', $column)) { + $table->dropColumn($column); + } + } + }); + } + } +}; diff --git a/database/migrations/2026_01_12_122942_add_columns_candidate_onboarding_table.php b/database/migrations/2026_01_12_122942_add_columns_candidate_onboarding_table.php new file mode 100644 index 000000000..117c93a9f --- /dev/null +++ b/database/migrations/2026_01_12_122942_add_columns_candidate_onboarding_table.php @@ -0,0 +1,68 @@ +CONSTRAINT_NAME); + } + + Schema::table('candidate_onboarding', function (Blueprint $table) { + // Make candidate_id nullable if column exists + if (Schema::hasColumn('candidate_onboarding', 'candidate_id')) { + $table->unsignedBigInteger('candidate_id')->nullable()->change(); + } + + // Add employee_id column with foreign key if it doesn't exist + if (!Schema::hasColumn('candidate_onboarding', 'employee_id')) { + $table->unsignedBigInteger('employee_id')->nullable()->after('candidate_id'); + $table->foreign('employee_id')->references('id')->on('users')->onDelete('cascade'); + } + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasTable('candidate_onboarding')) { + // Check if employee_id foreign key exists + $foreignKeyExists = DB::select( + "SELECT CONSTRAINT_NAME FROM information_schema.REFERENTIAL_CONSTRAINTS WHERE CONSTRAINT_SCHEMA = DATABASE() AND TABLE_NAME = 'candidate_onboarding' AND CONSTRAINT_NAME LIKE '%employee_id%'" + ); + + if (!empty($foreignKeyExists)) { + DB::statement('ALTER TABLE candidate_onboarding DROP FOREIGN KEY ' . $foreignKeyExists[0]->CONSTRAINT_NAME); + } + + Schema::table('candidate_onboarding', function (Blueprint $table) { + // Drop employee_id column if it exists + if (Schema::hasColumn('candidate_onboarding', 'employee_id')) { + $table->dropColumn('employee_id'); + } + + // Restore candidate_id foreign key if column exists + if (Schema::hasColumn('candidate_onboarding', 'candidate_id')) { + $table->unsignedBigInteger('candidate_id')->nullable(false)->change(); + $table->foreign('candidate_id')->references('id')->on('candidates')->onDelete('cascade'); + } + }); + } + } +}; \ No newline at end of file diff --git a/database/migrations/2026_01_30_104115_add_created_by_column_contact_table.php b/database/migrations/2026_01_30_104115_add_created_by_column_contact_table.php new file mode 100644 index 000000000..2950196fb --- /dev/null +++ b/database/migrations/2026_01_30_104115_add_created_by_column_contact_table.php @@ -0,0 +1,42 @@ +foreignId('created_by')->nullable()->after('message')->constrained('users')->onDelete('cascade'); + } + if (!Schema::hasColumn('contacts', 'status')) { + $table->enum('status', ['New', 'Contacted', 'Qualified', 'Converted', 'Closed'])->default('New')->after('created_by'); + } + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (Schema::hasTable('contacts')) { + Schema::table('contacts', function (Blueprint $table) { + if (Schema::hasColumn('contacts', 'created_by')) { + $table->dropForeign(['created_by']); + $table->dropColumn('created_by'); + } + if (Schema::hasColumn('contacts', 'status')) { + $table->dropColumn('status'); + } + }); + } + } +}; diff --git a/database/migrations/2026_02_02_083941_create_news_letters_table.php b/database/migrations/2026_02_02_083941_create_news_letters_table.php new file mode 100644 index 000000000..4659f4710 --- /dev/null +++ b/database/migrations/2026_02_02_083941_create_news_letters_table.php @@ -0,0 +1,31 @@ +id(); + $table->string('email')->nullable(); + $table->string('status')->default('subscribed'); + $table->timestamps(); + }); + } + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('news_letters'); + } +}; diff --git a/database/migrations/2026_02_02_111652_create_login_histories_table.php b/database/migrations/2026_02_02_111652_create_login_histories_table.php new file mode 100644 index 000000000..a079b1b12 --- /dev/null +++ b/database/migrations/2026_02_02_111652_create_login_histories_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('user_id'); + $table->string('ip'); + $table->string('date'); + $table->text('Details'); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('login_histories'); + } +}; diff --git a/database/migrations/2026_02_03_105104_create_noc_templates_table.php b/database/migrations/2026_02_03_105104_create_noc_templates_table.php new file mode 100644 index 000000000..3e8ebe6e6 --- /dev/null +++ b/database/migrations/2026_02_03_105104_create_noc_templates_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('language', 10); + $table->longText('content'); + $table->json('variables')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('noc_templates'); + } +}; diff --git a/database/migrations/2026_02_03_105105_create_joining_letter_templates_table.php b/database/migrations/2026_02_03_105105_create_joining_letter_templates_table.php new file mode 100644 index 000000000..5e511f243 --- /dev/null +++ b/database/migrations/2026_02_03_105105_create_joining_letter_templates_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('language', 10); + $table->longText('content'); + $table->json('variables')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('joining_letter_templates'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_02_03_105106_create_experience_certificate_templates_table.php b/database/migrations/2026_02_03_105106_create_experience_certificate_templates_table.php new file mode 100644 index 000000000..4b81f57f1 --- /dev/null +++ b/database/migrations/2026_02_03_105106_create_experience_certificate_templates_table.php @@ -0,0 +1,33 @@ +id(); + $table->string('language', 10); + $table->longText('content'); + $table->json('variables')->nullable(); + $table->foreignId('created_by')->constrained('users')->onDelete('cascade'); + $table->timestamps(); + }); + } + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('experience_certificate_templates'); + } +}; \ No newline at end of file diff --git a/database/migrations/2026_02_03_105110_modify_assets_and_training_programs_foreign_keys.php b/database/migrations/2026_02_03_105110_modify_assets_and_training_programs_foreign_keys.php new file mode 100644 index 000000000..7409f71e8 --- /dev/null +++ b/database/migrations/2026_02_03_105110_modify_assets_and_training_programs_foreign_keys.php @@ -0,0 +1,104 @@ +dropForeign(['asset_type_id']); + }); + + Schema::table('assets', function (Blueprint $table) { + // Modify the column to be nullable + $table->unsignedBigInteger('asset_type_id')->nullable()->change(); + }); + + Schema::table('assets', function (Blueprint $table) { + // Add the foreign key constraint back with SET NULL on delete + $table->foreign('asset_type_id') + ->references('id') + ->on('asset_types') + ->onDelete('set null'); + }); + } + + // Modify training_programs table + if (Schema::hasTable('training_programs') && Schema::hasColumn('training_programs', 'training_type_id')) { + Schema::table('training_programs', function (Blueprint $table) { + // Drop the existing foreign key constraint + $table->dropForeign(['training_type_id']); + }); + + Schema::table('training_programs', function (Blueprint $table) { + // Modify the column to be nullable + $table->unsignedBigInteger('training_type_id')->nullable()->change(); + }); + + Schema::table('training_programs', function (Blueprint $table) { + // Add the foreign key constraint back with SET NULL on delete + $table->foreign('training_type_id') + ->references('id') + ->on('training_types') + ->onDelete('set null'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // Revert assets table + if (Schema::hasTable('assets') && Schema::hasColumn('assets', 'asset_type_id')) { + Schema::table('assets', function (Blueprint $table) { + // Drop the foreign key constraint + $table->dropForeign(['asset_type_id']); + }); + + Schema::table('assets', function (Blueprint $table) { + // Modify the column back to not nullable + $table->unsignedBigInteger('asset_type_id')->nullable(false)->change(); + }); + + Schema::table('assets', function (Blueprint $table) { + // Add the foreign key constraint back with RESTRICT on delete + $table->foreign('asset_type_id') + ->references('id') + ->on('asset_types') + ->onDelete('restrict'); + }); + } + + // Revert training_programs table + if (Schema::hasTable('training_programs') && Schema::hasColumn('training_programs', 'training_type_id')) { + Schema::table('training_programs', function (Blueprint $table) { + // Drop the foreign key constraint + $table->dropForeign(['training_type_id']); + }); + + Schema::table('training_programs', function (Blueprint $table) { + // Modify the column back to not nullable + $table->unsignedBigInteger('training_type_id')->nullable(false)->change(); + }); + + Schema::table('training_programs', function (Blueprint $table) { + // Add the foreign key constraint back with RESTRICT on delete + $table->foreign('training_type_id') + ->references('id') + ->on('training_types') + ->onDelete('restrict'); + }); + } + } +}; diff --git a/database/migrations/2026_02_10_000001_update_rating_columns_interview_feedback_table.php b/database/migrations/2026_02_10_000001_update_rating_columns_interview_feedback_table.php new file mode 100644 index 000000000..47d3f3408 --- /dev/null +++ b/database/migrations/2026_02_10_000001_update_rating_columns_interview_feedback_table.php @@ -0,0 +1,48 @@ +decimal('technical_rating', 3, 1)->nullable()->change(); + } + if (Schema::hasColumn('interview_feedback', 'communication_rating')) { + $table->decimal('communication_rating', 3, 1)->nullable()->change(); + } + if (Schema::hasColumn('interview_feedback', 'cultural_fit_rating')) { + $table->decimal('cultural_fit_rating', 3, 1)->nullable()->change(); + } + if (Schema::hasColumn('interview_feedback', 'overall_rating')) { + $table->decimal('overall_rating', 3, 1)->nullable()->change(); + } + }); + } + } + + public function down(): void + { + if (Schema::hasTable('interview_feedback')) { + Schema::table('interview_feedback', function (Blueprint $table) { + if (Schema::hasColumn('interview_feedback', 'technical_rating')) { + $table->integer('technical_rating')->nullable()->change(); + } + if (Schema::hasColumn('interview_feedback', 'communication_rating')) { + $table->integer('communication_rating')->nullable()->change(); + } + if (Schema::hasColumn('interview_feedback', 'cultural_fit_rating')) { + $table->integer('cultural_fit_rating')->nullable()->change(); + } + if (Schema::hasColumn('interview_feedback', 'overall_rating')) { + $table->integer('overall_rating')->nullable()->change(); + } + }); + } + } +}; diff --git a/database/migrations/2026_02_10_000002_add_foreign_key_to_login_histories_user_id.php b/database/migrations/2026_02_10_000002_add_foreign_key_to_login_histories_user_id.php new file mode 100644 index 000000000..81f9e59b0 --- /dev/null +++ b/database/migrations/2026_02_10_000002_add_foreign_key_to_login_histories_user_id.php @@ -0,0 +1,54 @@ +unsignedBigInteger('user_id')->change(); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (!Schema::hasTable('login_histories')) { + return; + } + + // Drop FK only if it exists + if (Schema::hasColumn('login_histories', 'user_id')) { + Schema::table('login_histories', function (Blueprint $table) { + $table->dropForeign(['user_id']); + $table->string('user_id')->change(); + }); + } + } +}; diff --git a/database/seeders/ActionItemSeeder.php b/database/seeders/ActionItemSeeder.php new file mode 100644 index 000000000..ca5b084d3 --- /dev/null +++ b/database/seeders/ActionItemSeeder.php @@ -0,0 +1,186 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed action items for consistent data + $actionItemsData = [ + [ + 'title' => 'Implement Code Review Process', + 'description' => 'Research and implement automated code review process using GitHub Actions for better code quality', + 'due_days' => 7, + 'priority' => 'High', + 'status' => 'In Progress', + 'progress_percentage' => 60, + 'notes' => 'GitHub Actions workflow created, testing in progress' + ], + [ + 'title' => 'Prepare Weekly Status Report', + 'description' => 'Create comprehensive weekly status report including progress metrics and risk assessments', + 'due_days' => 3, + 'priority' => 'Medium', + 'status' => 'Completed', + 'progress_percentage' => 100, + 'notes' => 'Report completed and shared with stakeholders' + ], + [ + 'title' => 'Schedule Follow-up Training', + 'description' => 'Organize advanced training session for team members on new technologies and best practices', + 'due_days' => 14, + 'priority' => 'Medium', + 'status' => 'Not Started', + 'progress_percentage' => 0, + 'notes' => 'Waiting for trainer availability confirmation' + ], + [ + 'title' => 'Update Project Documentation', + 'description' => 'Review and update project documentation to reflect recent changes and new requirements', + 'due_days' => 5, + 'priority' => 'Low', + 'status' => 'In Progress', + 'progress_percentage' => 30, + 'notes' => 'Started with API documentation updates' + ], + [ + 'title' => 'Conduct Risk Assessment', + 'description' => 'Perform comprehensive risk analysis for upcoming project phases and prepare mitigation strategies', + 'due_days' => 10, + 'priority' => 'High', + 'status' => 'Not Started', + 'progress_percentage' => 0, + 'notes' => 'Risk assessment template being prepared' + ], + [ + 'title' => 'Client Presentation Preparation', + 'description' => 'Prepare quarterly business review presentation with performance metrics and future roadmap', + 'due_days' => -2, + 'priority' => 'Critical', + 'status' => 'Overdue', + 'progress_percentage' => 80, + 'notes' => 'Presentation 80% complete, pending final review' + ], + [ + 'title' => 'Team Performance Evaluation', + 'description' => 'Complete individual performance evaluations for all team members and prepare development plans', + 'due_days' => 21, + 'priority' => 'Medium', + 'status' => 'Not Started', + 'progress_percentage' => 0, + 'notes' => 'Performance evaluation forms being finalized' + ], + [ + 'title' => 'Budget Review and Planning', + 'description' => 'Review current budget allocation and prepare budget plan for next quarter', + 'due_days' => 15, + 'priority' => 'High', + 'status' => 'In Progress', + 'progress_percentage' => 45, + 'notes' => 'Current budget analysis completed, planning phase started' + ], + [ + 'title' => 'Security Audit Implementation', + 'description' => 'Implement security recommendations from recent audit and update security protocols', + 'due_days' => 30, + 'priority' => 'Critical', + 'status' => 'Not Started', + 'progress_percentage' => 0, + 'notes' => 'Security team assigned, implementation plan pending' + ], + [ + 'title' => 'Customer Feedback Analysis', + 'description' => 'Analyze recent customer feedback and prepare improvement recommendations', + 'due_days' => -5, + 'priority' => 'Medium', + 'status' => 'Completed', + 'progress_percentage' => 100, + 'notes' => 'Analysis completed, recommendations shared with product team' + ] + ]; + + foreach ($companies as $company) { + // Get meetings for this company + $meetings = Meeting::where('created_by', $company->id)->get(); + + if ($meetings->isEmpty()) { + $this->command->warn('No meetings found for company: ' . $company->name . '. Please run MeetingSeeder first.'); + continue; + } + + // Get employees for assignment + $employees = User::whereIn('type', ['manager', 'hr', 'employee']) + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name); + continue; + } + + // Create action items for first 5 meetings + $selectedMeetings = $meetings->take(5); + + foreach ($selectedMeetings as $meetingIndex => $meeting) { + // Create 2 action items per meeting + for ($itemIndex = 0; $itemIndex < 2; $itemIndex++) { + $dataIndex = ($meetingIndex * 2) + $itemIndex; + $actionData = $actionItemsData[$dataIndex % 10]; + + // Select assignee from first 8 employees + $selectedEmployees = $employees->take(8); + $assignee = $selectedEmployees->skip($dataIndex % 8)->first() ?: $selectedEmployees->first(); + + $dueDate = date('Y-m-d', strtotime($meeting->meeting_date . ' +' . $actionData['due_days'] . ' days')); + $completedDate = $actionData['status'] === 'Completed' ? + date('Y-m-d', strtotime($dueDate . ' -1 day')) : null; + + // Check if action item already exists for this meeting and title + if (ActionItem::where('meeting_id', $meeting->id) + ->where('title', $actionData['title']) + ->exists()) { + continue; + } + + try { + ActionItem::create([ + 'meeting_id' => $meeting->id, + 'title' => $actionData['title'], + 'description' => $actionData['description'], + 'assigned_to' => $assignee->id, + 'due_date' => $dueDate, + 'priority' => $actionData['priority'], + 'status' => $actionData['status'], + 'progress_percentage' => $actionData['progress_percentage'], + 'notes' => $actionData['notes'], + 'completed_date' => $completedDate, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create action item: ' . $actionData['title'] . ' for meeting: ' . $meeting->title . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('ActionItem seeder completed successfully!'); + } +} diff --git a/database/seeders/AnnouncementSeeder.php b/database/seeders/AnnouncementSeeder.php new file mode 100644 index 000000000..3c088452c --- /dev/null +++ b/database/seeders/AnnouncementSeeder.php @@ -0,0 +1,216 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed announcements for consistent data + $announcements = [ + [ + 'title' => 'Welcome to New Financial Year ' . $currentYear, + 'category' => 'Company News', + 'description' => 'Important updates and goals for the new financial year', + 'content' => 'Dear Team, As we begin the new financial year, we are excited to share our strategic goals and initiatives. This year, we focus on innovation, employee development, and sustainable growth. We have planned several training programs, team building activities, and performance improvement initiatives. Your dedication and hard work are the foundation of our success. Let us work together to achieve new milestones and create a positive impact in our industry.', + 'start_date' => $currentYear . '-01-01', + 'end_date' => $currentYear . '-01-31', + 'is_featured' => true, + 'is_high_priority' => true, + 'is_company_wide' => true + ], + [ + 'title' => 'Updated Employee Handbook and Policies', + 'category' => 'Policy Updates', + 'description' => 'Important changes to company policies and procedures', + 'content' => 'We have updated our Employee Handbook to reflect current best practices and regulatory requirements. Key changes include updated leave policies, remote work guidelines, performance evaluation criteria, and code of conduct. All employees are required to review the updated handbook and acknowledge receipt. The new policies are effective immediately. Please contact HR department for any clarifications or questions regarding the policy changes.', + 'start_date' => $currentYear . '-02-01', + 'end_date' => $currentYear . '-02-28', + 'is_featured' => true, + 'is_high_priority' => true, + 'is_company_wide' => true + ], + [ + 'title' => 'Annual Performance Review Process', + 'category' => 'HR Updates', + 'description' => 'Guidelines for the annual performance evaluation cycle', + 'content' => 'The annual performance review process will commence next month. This comprehensive evaluation includes self-assessment, peer feedback, and supervisor evaluation. Performance reviews are crucial for career development, goal setting, and compensation decisions. Please prepare your self-assessment forms and gather relevant project documentation. HR will schedule individual meetings with each employee. We encourage open communication and constructive feedback during this process.', + 'start_date' => $currentYear . '-03-01', + 'end_date' => $currentYear . '-04-30', + 'is_featured' => false, + 'is_high_priority' => true, + 'is_company_wide' => true + ], + [ + 'title' => 'New Employee Benefits Program Launch', + 'category' => 'Benefits', + 'description' => 'Enhanced benefits package for all employees', + 'content' => 'We are pleased to announce the launch of our enhanced employee benefits program. New benefits include expanded health insurance coverage, flexible spending accounts, professional development allowances, and wellness programs. The program also includes mental health support, childcare assistance, and transportation allowances. Enrollment is mandatory for all eligible employees. Detailed information and enrollment forms are available on the employee portal.', + 'start_date' => $currentYear . '-04-01', + 'end_date' => $currentYear . '-05-31', + 'is_featured' => true, + 'is_high_priority' => false, + 'is_company_wide' => true + ], + [ + 'title' => 'IT Department System Maintenance', + 'category' => 'IT Updates', + 'description' => 'Scheduled maintenance for IT department systems', + 'content' => 'The IT department will undergo scheduled system maintenance to upgrade servers and network infrastructure. This maintenance is specific to IT department operations and will not affect other departments. IT staff should prepare for temporary system downtime and coordinate with the maintenance team. All IT projects and deployments will be paused during this period. Please ensure all critical data is backed up before the maintenance window.', + 'start_date' => $currentYear . '-05-15', + 'end_date' => $currentYear . '-05-16', + 'is_featured' => false, + 'is_high_priority' => false, + 'is_company_wide' => false + ], + [ + 'title' => 'Sales Team Quarterly Meeting', + 'category' => 'Events', + 'description' => 'Important quarterly review for sales department', + 'content' => 'The sales department will conduct its quarterly review meeting to discuss performance metrics, target achievements, and upcoming strategies. This meeting is mandatory for all sales team members. We will review individual and team performance, discuss market trends, and plan for the next quarter. Sales managers will present departmental goals and new client acquisition strategies.', + 'start_date' => $currentYear . '-06-01', + 'end_date' => $currentYear . '-06-15', + 'is_featured' => false, + 'is_high_priority' => true, + 'is_company_wide' => false + ], + [ + 'title' => 'HR Department Policy Training', + 'category' => 'Training', + 'description' => 'Specialized training for HR department staff', + 'content' => 'The HR department will conduct specialized training on new employment laws, compliance requirements, and updated HR policies. This training is mandatory for all HR staff and covers recent regulatory changes, best practices in employee relations, and new HR technology implementations. External trainers will provide certification upon completion.', + 'start_date' => $currentYear . '-07-01', + 'end_date' => $currentYear . '-12-31', + 'is_featured' => true, + 'is_high_priority' => false, + 'is_company_wide' => false + ], + [ + 'title' => 'Finance Department Audit Preparation', + 'category' => 'Finance', + 'description' => 'Annual audit preparation for finance team', + 'content' => 'The finance department must prepare for the annual external audit. All finance team members are required to organize financial records, prepare documentation, and coordinate with auditors. This process is critical for compliance and requires full cooperation from all finance staff. Detailed preparation guidelines and timelines will be provided.', + 'start_date' => $currentYear . '-08-01', + 'end_date' => $currentYear . '-08-31', + 'is_featured' => false, + 'is_high_priority' => true, + 'is_company_wide' => false + ] + ]; + + foreach ($companies as $company) { + // Get HR users who can create announcements + $hrUsers = User::where('type', 'hr') + ->where('created_by', $company->id) + ->get(); + + $creator = $hrUsers->isNotEmpty() ? $hrUsers->first() : $company; + + // Get branches and departments for this company + $branches = Branch::where('created_by', $company->id)->get(); + $departments = Department::where('created_by', $company->id)->get(); + + foreach ($announcements as $index => $announcementData) { + // Check if announcement already exists for this company + if (Announcement::where('title', $announcementData['title'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + $announcement = Announcement::create([ + 'title' => $announcementData['title'], + 'category' => $announcementData['category'], + 'description' => $announcementData['description'], + 'content' => $announcementData['content'], + 'start_date' => $announcementData['start_date'], + 'end_date' => $announcementData['end_date'], + 'attachments' => null, + 'is_featured' => $announcementData['is_featured'], + 'is_high_priority' => $announcementData['is_high_priority'], + 'is_company_wide' => $announcementData['is_company_wide'], + 'created_by' => $creator->id, + ]); + + // Attach to branches and departments based on scope + if ($announcementData['is_company_wide']) { + // Company-wide: attach to all branches and departments + if ($branches->isNotEmpty()) { + $announcement->branches()->attach($branches->pluck('id')); + } + if ($departments->isNotEmpty()) { + $announcement->departments()->attach($departments->pluck('id')); + } + } else { + // Department/Branch specific: attach to one branch and one department + if ($branches->isNotEmpty()) { + $announcement->branches()->attach($branches->first()->id); + } + if ($departments->isNotEmpty()) { + $announcement->departments()->attach($departments->first()->id); + } + } + + // Create announcement views for some employees + $this->createAnnouncementViews($announcement, $company); + } catch (\Exception $e) { + $this->command->error('Failed to create announcement: ' . $announcementData['title'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Announcement seeder completed successfully!'); + } + + /** + * Create announcement views for employees + */ + private function createAnnouncementViews($announcement, $company) + { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + return; + } + + // Create views for first 3 employees (consistent data) + $viewedEmployees = $employees->take(5); + + foreach ($viewedEmployees as $employee) { + try { + DB::table('announcement_views')->insert([ + 'announcement_id' => $announcement->id, + 'employee_id' => $employee->id, + 'viewed_at' => now(), + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + continue; + } + } + } +} diff --git a/database/seeders/AssetSeeder.php b/database/seeders/AssetSeeder.php new file mode 100644 index 000000000..a3b330ac7 --- /dev/null +++ b/database/seeders/AssetSeeder.php @@ -0,0 +1,210 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed assets data for consistent results + $assetsData = [ + 'Computer Hardware' => [ + ['name' => 'Dell OptiPlex 7090', 'serial' => 'DL001', 'cost' => 85000, 'condition' => 'new'], + ['name' => 'HP EliteBook 840', 'serial' => 'HP001', 'cost' => 95000, 'condition' => 'good'], + ['name' => 'Lenovo ThinkPad X1', 'serial' => 'LN001', 'cost' => 120000, 'condition' => 'new'], + ['name' => 'MacBook Pro 16"', 'serial' => 'AP001', 'cost' => 250000, 'condition' => 'good'] + ], + 'Mobile Devices' => [ + ['name' => 'iPhone 14 Pro', 'serial' => 'IP001', 'cost' => 130000, 'condition' => 'new'], + ['name' => 'Samsung Galaxy S23', 'serial' => 'SM001', 'cost' => 80000, 'condition' => 'good'], + ['name' => 'iPad Air 5th Gen', 'serial' => 'ID001', 'cost' => 60000, 'condition' => 'new'] + ], + 'Office Equipment' => [ + ['name' => 'Canon ImageRunner Printer', 'serial' => 'CN001', 'cost' => 45000, 'condition' => 'good'], + ['name' => 'Epson EcoTank Printer', 'serial' => 'EP001', 'cost' => 25000, 'condition' => 'new'], + ['name' => 'Brother MFC Scanner', 'serial' => 'BR001', 'cost' => 35000, 'condition' => 'fair'] + ], + 'Furniture' => [ + ['name' => 'Executive Office Desk', 'serial' => 'FU001', 'cost' => 15000, 'condition' => 'good'], + ['name' => 'Ergonomic Office Chair', 'serial' => 'FU002', 'cost' => 12000, 'condition' => 'new'], + ['name' => 'Conference Table 12-Seater', 'serial' => 'FU003', 'cost' => 35000, 'condition' => 'good'] + ], + 'Vehicles' => [ + ['name' => 'Toyota Camry 2023', 'serial' => 'TC001', 'cost' => 3500000, 'condition' => 'new'], + ['name' => 'Honda City 2022', 'serial' => 'HC001', 'cost' => 1800000, 'condition' => 'good'] + ], + 'Network Equipment' => [ + ['name' => 'Cisco Catalyst Switch', 'serial' => 'CS001', 'cost' => 75000, 'condition' => 'new'], + ['name' => 'TP-Link Wireless Router', 'serial' => 'TP001', 'cost' => 8000, 'condition' => 'good'] + ] + ]; + + foreach ($companies as $company) { + // Get asset types for this company + $assetTypes = AssetType::where('created_by', $company->id)->get(); + + if ($assetTypes->isEmpty()) { + $this->command->warn('No asset types found for company: ' . $company->name . '. Please run AssetTypeSeeder first.'); + continue; + } + + // Get employees for assignments + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + foreach ($assetTypes as $assetType) { + $typeAssets = $assetsData[$assetType->name] ?? []; + + foreach ($typeAssets as $index => $assetData) { + $assetCode = strtoupper(substr($assetType->name, 0, 3)) . str_pad($index + 1, 3, '0', STR_PAD_LEFT); + + // Check if asset already exists + if (Asset::where('asset_code', $assetCode)->where('created_by', $company->id)->exists()) { + continue; + } + + try { + $asset = Asset::create([ + 'name' => $assetData['name'], + 'asset_type_id' => $assetType->id, + 'serial_number' => $assetData['serial'], + 'asset_code' => $assetCode, + 'purchase_date' => $currentYear . '-01-15', + 'purchase_cost' => $assetData['cost'], + 'status' => $index % 3 === 0 ? 'assigned' : 'available', + 'condition' => $assetData['condition'], + 'description' => 'Standard ' . $assetType->name . ' for business operations', + 'location' => 'Main Office', + 'supplier' => 'Tech Solutions Pvt Ltd', + 'warranty_info' => '2 Year Manufacturer Warranty', + 'warranty_expiry_date' => ($currentYear + 2) . '-01-15', + 'images' => null, + 'documents' => null, + 'qr_code' => null, + 'created_by' => $company->id, + ]); + + // Create asset assignment for assigned assets + if ($asset->status === 'assigned' && $employees->isNotEmpty()) { + $this->createAssetAssignment($asset, $employees->first(), $company); + } + + // Create asset depreciation + $this->createAssetDepreciation($asset, $company); + + // Create maintenance record for all assets + $this->createAssetMaintenance($asset, $company, $index); + } catch (\Exception $e) { + $this->command->error('Failed to create asset: ' . $assetData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('Asset seeder completed successfully!'); + } + + /** + * Create asset assignment + */ + private function createAssetAssignment($asset, $employee, $company) + { + try { + DB::table('asset_assignments')->insert([ + 'asset_id' => $asset->id, + 'employee_id' => $employee->id, + 'checkout_date' => date('Y-m-d'), + 'expected_return_date' => null, + 'checkin_date' => null, + 'checkout_condition' => $asset->condition, + 'checkin_condition' => null, + 'notes' => 'Asset assigned for regular business use', + 'is_acknowledged' => true, + 'acknowledged_at' => now(), + 'assigned_by' => $company->id, + 'received_by' => null, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + // Continue on error + } + } + + /** + * Create asset depreciation + */ + private function createAssetDepreciation($asset, $company) + { + $usefulLife = match ($asset->assetType->name) { + 'Computer Hardware', 'Mobile Devices' => 3, + 'Office Equipment', 'Network Equipment' => 5, + 'Furniture' => 10, + 'Vehicles' => 8, + default => 5 + }; + + $salvageValue = $asset->purchase_cost * 0.1; // 10% salvage value + $currentValue = $asset->purchase_cost - (($asset->purchase_cost - $salvageValue) / $usefulLife); + + try { + DB::table('asset_depreciations')->insert([ + 'asset_id' => $asset->id, + 'method' => 'straight_line', + 'useful_life_years' => $usefulLife, + 'salvage_value' => $salvageValue, + 'current_value' => $currentValue, + 'last_calculated_date' => date('Y-m-d'), + 'created_by' => $company->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + // Continue on error + } + } + + /** + * Create asset maintenance + */ + private function createAssetMaintenance($asset, $company, $index) + { + try { + DB::table('asset_maintenances')->insert([ + 'asset_id' => $asset->id, + 'maintenance_type' => 'preventive', + 'start_date' => date('Y-m-d'), + 'end_date' => date('Y-m-d', strtotime('+7 days')), + 'cost' => $asset->purchase_cost * 0.05, // 5% of purchase cost + 'status' => ['scheduled', 'in_progress', 'completed', 'cancelled'][$index % 4], + 'details' => 'Regular preventive maintenance and system updates', + 'completion_notes' => 'Maintenance completed successfully, asset in good condition', + 'supplier' => 'Maintenance Services Ltd', + 'created_by' => $company->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + // Continue on error + } + } +} diff --git a/database/seeders/AssetTypeSeeder.php b/database/seeders/AssetTypeSeeder.php new file mode 100644 index 000000000..9f6f937db --- /dev/null +++ b/database/seeders/AssetTypeSeeder.php @@ -0,0 +1,98 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed asset types for consistent data + $assetTypes = [ + [ + 'name' => 'Computer Hardware', + 'description' => 'Desktop computers, laptops, servers, and other computing equipment used for business operations' + ], + [ + 'name' => 'Mobile Devices', + 'description' => 'Smartphones, tablets, and other portable electronic devices assigned to employees' + ], + [ + 'name' => 'Office Equipment', + 'description' => 'Printers, scanners, photocopiers, fax machines, and other office machinery' + ], + [ + 'name' => 'Furniture', + 'description' => 'Office desks, chairs, cabinets, conference tables, and other workplace furniture' + ], + [ + 'name' => 'Vehicles', + 'description' => 'Company cars, trucks, vans, and other vehicles used for business purposes' + ], + [ + 'name' => 'Software Licenses', + 'description' => 'Software applications, operating systems, and digital licenses for business use' + ], + [ + 'name' => 'Network Equipment', + 'description' => 'Routers, switches, modems, access points, and other networking hardware' + ], + [ + 'name' => 'Audio Visual Equipment', + 'description' => 'Projectors, monitors, speakers, cameras, and presentation equipment' + ], + [ + 'name' => 'Security Equipment', + 'description' => 'CCTV cameras, access control systems, alarms, and security devices' + ], + [ + 'name' => 'Tools and Machinery', + 'description' => 'Specialized tools, machinery, and equipment specific to business operations' + ], + [ + 'name' => 'Communication Equipment', + 'description' => 'Telephone systems, headsets, video conferencing equipment, and communication devices' + ], + [ + 'name' => 'Storage Equipment', + 'description' => 'Filing cabinets, safes, storage units, and data storage devices' + ] + ]; + + foreach ($companies as $company) { + foreach ($assetTypes as $assetTypeData) { + // Check if asset type already exists for this company + if (AssetType::where('name', $assetTypeData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + AssetType::create([ + 'name' => $assetTypeData['name'], + 'description' => $assetTypeData['description'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create asset type: ' . $assetTypeData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('AssetType seeder completed successfully!'); + } +} diff --git a/database/seeders/AttendancePolicySeeder.php b/database/seeders/AttendancePolicySeeder.php new file mode 100644 index 000000000..bcd742fa1 --- /dev/null +++ b/database/seeders/AttendancePolicySeeder.php @@ -0,0 +1,102 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed attendance policies for consistent data + $attendancePolicies = [ + [ + 'name' => 'Standard Attendance Policy', + 'description' => 'Default attendance policy with standard grace periods and overtime rates', + 'late_arrival_grace' => 15, + 'early_departure_grace' => 15, + 'half_day_threshold' => 4.00, + 'overtime_rate_per_hour' => 150.00, + 'status' => 'active' + ], + [ + 'name' => 'Flexible Attendance Policy', + 'description' => 'Flexible attendance policy with extended grace periods for remote and flexible workers', + 'late_arrival_grace' => 30, + 'early_departure_grace' => 30, + 'half_day_threshold' => 3.50, + 'overtime_rate_per_hour' => 175.00, + 'status' => 'active' + ], + [ + 'name' => 'Strict Attendance Policy', + 'description' => 'Strict attendance policy with minimal grace periods for critical operations', + 'late_arrival_grace' => 5, + 'early_departure_grace' => 5, + 'half_day_threshold' => 4.50, + 'overtime_rate_per_hour' => 200.00, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($attendancePolicies as $policyData) { + // Check if attendance policy already exists for this company + if (AttendancePolicy::where('name', $policyData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + AttendancePolicy::create([ + 'name' => $policyData['name'], + 'description' => $policyData['description'], + 'late_arrival_grace' => $policyData['late_arrival_grace'], + 'early_departure_grace' => $policyData['early_departure_grace'], + 'half_day_threshold' => $policyData['half_day_threshold'], + 'overtime_rate_per_hour' => $policyData['overtime_rate_per_hour'], + 'status' => $policyData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create attendance policy: ' . $policyData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + // Assign attendance policies to employees after creating policies + foreach ($companies as $company) { + $companyPolicies = AttendancePolicy::where('created_by', $company->id)->get(); + $employeeUsers = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($companyPolicies->isNotEmpty() && $employeeUsers->isNotEmpty()) { + // Assign Standard Attendance Policy to all employees by default + $standardPolicy = $companyPolicies->where('name', 'Standard Attendance Policy')->first(); + if ($standardPolicy) { + foreach ($employeeUsers as $employeeUser) { + // Get employee record from employee table using user_id + $employee = \App\Models\Employee::where('user_id', $employeeUser->id)->first(); + if ($employee && !$employee->attendance_policy_id) { + $employee->update(['attendance_policy_id' => $standardPolicy->id]); + } + } + } + } + } + + $this->command->info('AttendancePolicy seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/AttendanceRecordSeeder.php b/database/seeders/AttendanceRecordSeeder.php new file mode 100644 index 000000000..69f423368 --- /dev/null +++ b/database/seeders/AttendanceRecordSeeder.php @@ -0,0 +1,202 @@ +clearWeekendRecords(); + + $companies = User::where('type', 'company')->get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found.'); + return; + } + $currentYear = date('Y'); + + foreach ($companies as $company) { + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + continue; + } + + // Process May to August 2025 + for ($month = 5; $month <= 8; $month++) { + $this->processMonth($company, $employees, $currentYear, $month); + } + } + + $this->command->info('AttendanceRecord seeder completed successfully!'); + } + + private function clearWeekendRecords() + { + // Delete all weekend attendance records for 2025 + for ($month = 1; $month <= 12; $month++) { + $startDate = Carbon::create(2025, $month, 1); + $endDate = $startDate->copy()->endOfMonth(); + + for ($day = 1; $day <= $endDate->day; $day++) { + $currentDate = Carbon::create(2025, $month, $day); + + // If it's Saturday or Sunday, delete records + if ($currentDate->dayOfWeek == 6 || $currentDate->dayOfWeek == 0) { + AttendanceRecord::whereDate('date', $currentDate->format('Y-m-d'))->delete(); + } + } + } + } + + private function processMonth($company, $employees, $year, $month) + { + $startDate = Carbon::create($year, $month, 1); + $endDate = $startDate->copy()->endOfMonth(); + + for ($day = 1; $day <= $endDate->day; $day++) { + $currentDate = Carbon::create($year, $month, $day); + + // Skip Saturday (6) and Sunday (0) + if ($currentDate->dayOfWeek == 6 || $currentDate->dayOfWeek == 0) { + continue; + } + + $dateString = $currentDate->format('Y-m-d'); + + // Process each employee for this date + foreach ($employees as $employee) { + $this->createAttendanceForEmployee($company, $employee, $dateString); + } + } + } + + private function createAttendanceForEmployee($company, $employee, $dateString) + { + // Get employee record + $employeeRecord = Employee::where('user_id', $employee->id)->first(); + + if (!$employeeRecord || !$employeeRecord->shift_id || !$employeeRecord->attendance_policy_id) { + return; + } + + $shift = Shift::find($employeeRecord->shift_id); + $attendancePolicy = AttendancePolicy::find($employeeRecord->attendance_policy_id); + + if (!$shift || !$attendancePolicy) { + return; + } + + // Check if employee is on leave + $isOnLeave = LeaveApplication::where('employee_id', $employee->id) + ->where('status', 'approved') + ->where('start_date', '<=', $dateString) + ->where('end_date', '>=', $dateString) + ->exists(); + + // Check if record already exists + $existingRecord = AttendanceRecord::where('employee_id', $employee->id) + ->where('date', $dateString) + ->first(); + + if ($existingRecord) { + echo "Skipping {$dateString} for employee {$employee->id} - already exists\n"; + return; + } + + + if ($isOnLeave) { + // Create leave attendance + $record = AttendanceRecord::create([ + 'employee_id' => $employee->id, + 'shift_id' => $shift->id, + 'attendance_policy_id' => $attendancePolicy->id, + 'date' => $dateString, + 'clock_in' => null, + 'clock_out' => null, + 'status' => 'on_leave', + 'notes' => 'Employee on approved leave', + 'created_by' => $company->id, + ]); + } else { + // Create regular attendance + $pattern = $this->getAttendancePattern($dateString, $shift); + + $record = AttendanceRecord::create([ + 'employee_id' => $employee->id, + 'shift_id' => $shift->id, + 'attendance_policy_id' => $attendancePolicy->id, + 'date' => $dateString, + 'clock_in' => $pattern['clock_in'], + 'clock_out' => $pattern['clock_out'], + 'status' => $pattern['status'], + 'notes' => $pattern['notes'], + 'created_by' => $company->id, + ]); + } + + // Process attendance calculations + $record->processAttendance(); + } + + private function getAttendancePattern($dateString, $shift) + { + $date = Carbon::parse($dateString); + $dayOfMonth = $date->day; + $shiftStart = Carbon::parse($shift->start_time); + $shiftEnd = Carbon::parse($shift->end_time); + + // Different patterns including overtime + $patternType = $dayOfMonth % 4; + + switch ($patternType) { + case 0: // Late arrival + $lateClockIn = $shiftStart->copy()->addMinutes(25); + return [ + 'clock_in' => $lateClockIn->format('H:i:s'), + 'clock_out' => $shiftEnd->format('H:i:s'), + 'status' => 'present', + 'notes' => 'Late arrival' + ]; + + case 1: // Early departure + $earlyClockOut = $shiftEnd->copy()->subMinutes(40); + return [ + 'clock_in' => $shiftStart->format('H:i:s'), + 'clock_out' => $earlyClockOut->format('H:i:s'), + 'status' => 'present', + 'notes' => 'Early departure' + ]; + + case 2: // Overtime + $overtimeClockOut = $shiftEnd->copy()->addHours(2); + return [ + 'clock_in' => $shiftStart->format('H:i:s'), + 'clock_out' => $overtimeClockOut->format('H:i:s'), + 'status' => 'present', + 'notes' => 'Overtime work' + ]; + + default: // Regular attendance + return [ + 'clock_in' => $shiftStart->format('H:i:s'), + 'clock_out' => $shiftEnd->format('H:i:s'), + 'status' => 'present', + 'notes' => 'Regular attendance' + ]; + } + } +} diff --git a/database/seeders/AttendanceRegularizationSeeder.php b/database/seeders/AttendanceRegularizationSeeder.php new file mode 100644 index 000000000..d92107d5d --- /dev/null +++ b/database/seeders/AttendanceRegularizationSeeder.php @@ -0,0 +1,143 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found.'); + return; + } + $currentYear = date('Y'); + foreach ($companies as $company) { + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->limit(4) + ->get(); + + if ($employees->isEmpty()) { + continue; + } + + // Process May to August 2025 + for ($month = 5; $month <= 8; $month++) { + $this->processMonth($company, $employees, $currentYear, $month); + } + } + + $this->command->info('AttendanceRegularization seeder completed successfully!'); + } + + private function processMonth($company, $employees, $year, $month) + { + foreach ($employees as $employee) { + // Get attendance records for this employee in this month + $attendanceRecords = AttendanceRecord::where('employee_id', $employee->id) + ->whereYear('date', $year) + ->whereMonth('date', $month) + ->where('status', '!=', 'on_leave') + ->get(); + + if ($attendanceRecords->isEmpty()) { + continue; + } + + // Create 2-3 regularization requests per employee per month + $recordsToRegularize = $attendanceRecords->take(3); + + foreach ($recordsToRegularize as $index => $record) { + $this->createRegularization($company, $employee, $record, $index); + } + } + } + + private function createRegularization($company, $employee, $attendanceRecord, $index) + { + // Check if regularization already exists + $existingRegularization = AttendanceRegularization::where('employee_id', $employee->id) + ->where('attendance_record_id', $attendanceRecord->id) + ->first(); + + if ($existingRegularization) { + return; + } + + // Get regularization pattern based on index + $pattern = $this->getRegularizationPattern($attendanceRecord, $index); + + // Get manager for approval + $manager = User::where('type', 'employee') + ->where('created_by', $company->id) + ->where('id', '!=', $employee->id) + ->first() ?? $company; + + AttendanceRegularization::create([ + 'employee_id' => $employee->id, + 'attendance_record_id' => $attendanceRecord->id, + 'date' => $attendanceRecord->date, + 'requested_clock_in' => $pattern['requested_clock_in'], + 'requested_clock_out' => $pattern['requested_clock_out'], + 'original_clock_in' => $attendanceRecord->clock_in, + 'original_clock_out' => $attendanceRecord->clock_out, + 'reason' => $pattern['reason'], + 'status' => $pattern['status'], + 'manager_comments' => $pattern['manager_comments'], + 'approved_by' => $pattern['approved_by'] ? $manager->id : null, + 'approved_at' => $pattern['approved_at'], + 'created_by' => $employee->id, + ]); + } + + private function getRegularizationPattern($attendanceRecord, $index) + { + $originalClockIn = $attendanceRecord->clock_in ? Carbon::parse($attendanceRecord->clock_in) : null; + $originalClockOut = $attendanceRecord->clock_out ? Carbon::parse($attendanceRecord->clock_out) : null; + + switch ($index % 3) { + case 0: // Pending - Late arrival correction + $requestedClockIn = $originalClockIn ? $originalClockIn->copy()->subMinutes(30) : Carbon::parse('09:00:00'); + return [ + 'requested_clock_in' => $requestedClockIn->format('H:i:s'), + 'requested_clock_out' => $originalClockOut ? $originalClockOut->format('H:i:s') : null, + 'reason' => 'Traffic jam caused delay, requesting time correction', + 'status' => 'pending', + 'manager_comments' => null, + 'approved_by' => false, + 'approved_at' => null, + ]; + + case 1: // Pending - Early departure correction + $requestedClockOut = $originalClockOut ? $originalClockOut->copy()->addMinutes(45) : Carbon::parse('18:00:00'); + return [ + 'requested_clock_in' => $originalClockIn ? $originalClockIn->format('H:i:s') : null, + 'requested_clock_out' => $requestedClockOut->format('H:i:s'), + 'reason' => 'Had to leave early for medical appointment, worked from home later', + 'status' => 'pending', + 'manager_comments' => null, + 'approved_by' => false, + 'approved_at' => null, + ]; + + default: // Pending - Missing punch correction + return [ + 'requested_clock_in' => '09:00:00', + 'requested_clock_out' => '18:00:00', + 'reason' => 'Forgot to punch in/out, was present full day', + 'status' => 'pending', + 'manager_comments' => null, + 'approved_by' => false, + 'approved_at' => null, + ]; + } + } +} diff --git a/database/seeders/AwardSeeder.php b/database/seeders/AwardSeeder.php new file mode 100644 index 000000000..4aaf382bb --- /dev/null +++ b/database/seeders/AwardSeeder.php @@ -0,0 +1,167 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Gift options for awards + $gifts = [ + 'Certificate of Excellence', + 'Trophy', + 'Medal', + 'Gift Voucher', + 'Plaque', + 'Watch', + 'Laptop Bag', + 'Bonus Cash', + 'Extra Leave Days', + 'Dinner Voucher' + ]; + + // Award descriptions based on award types + $awardDescriptions = [ + 'Employee of the Month' => [ + 'Recognized for exceptional performance and dedication in achieving monthly targets', + 'Outstanding contribution to team productivity and maintaining high work standards', + 'Demonstrated excellent customer service and positive attitude throughout the month' + ], + 'Employee of the Year' => [ + 'Consistently delivered exceptional results and exceeded expectations throughout the year', + 'Showed remarkable leadership qualities and mentored junior team members effectively', + 'Achieved all annual goals while maintaining highest quality standards' + ], + 'Excellence Award' => [ + 'Demonstrated excellence in project execution and delivered outstanding results', + 'Maintained highest quality standards and attention to detail in all assignments', + 'Consistently exceeded performance expectations and set new benchmarks' + ], + 'Innovation Award' => [ + 'Developed innovative solution that improved process efficiency by 30%', + 'Introduced creative approach that reduced operational costs significantly', + 'Implemented new technology that enhanced team productivity and workflow' + ], + 'Leadership Award' => [ + 'Effectively led cross-functional team and delivered project ahead of schedule', + 'Mentored team members and contributed to their professional development', + 'Demonstrated exceptional leadership during challenging project situations' + ], + 'Customer Service Award' => [ + 'Achieved highest customer satisfaction rating and received excellent feedback', + 'Resolved complex customer issues with professionalism and efficiency', + 'Built strong client relationships resulting in increased business opportunities' + ], + 'Team Player Award' => [ + 'Collaborated effectively across departments and facilitated smooth project execution', + 'Supported team members during peak workload and maintained positive team dynamics', + 'Contributed to team success through excellent cooperation and communication skills' + ], + 'Achievement Award' => [ + 'Successfully completed challenging project within tight deadline and budget constraints', + 'Achieved 120% of assigned targets and contributed significantly to company revenue', + 'Accomplished major milestone that had positive impact on business growth' + ], + 'Long Service Award' => [ + 'Completed 5 years of dedicated service with consistent performance and loyalty', + 'Demonstrated unwavering commitment and contributed to organizational stability', + 'Served as valuable team member for 10 years with exemplary attendance record' + ], + 'Safety Award' => [ + 'Maintained zero safety incidents and promoted safety awareness among colleagues', + 'Implemented safety protocols that reduced workplace accidents by 50%', + 'Demonstrated exceptional commitment to workplace safety and emergency preparedness' + ], + 'Quality Award' => [ + 'Achieved zero defect rate and maintained highest quality standards in deliverables', + 'Implemented quality improvement measures that enhanced overall product quality', + 'Consistently delivered error-free work and exceeded quality benchmarks' + ], + 'Sales Achievement Award' => [ + 'Exceeded annual sales target by 150% and acquired 25 new major clients', + 'Generated highest revenue in company history through strategic sales initiatives', + 'Achieved outstanding sales performance and contributed to market expansion' + ] + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get award types for this company + $awardTypes = AwardType::where('created_by', $company->id)->get(); + + if ($awardTypes->isEmpty()) { + $this->command->warn('No award types found for company: ' . $company->name . '. Please run AwardTypeSeeder first.'); + continue; + } + + // Create 10-15 awards for this company + $awardCount = rand(5, 7); + + for ($i = 0; $i < $awardCount; $i++) { + $employee = $employees->take(7)->random(); + $awardType = $awardTypes->random(); + + try { + Award::create([ + 'employee_id' => $employee->id, + 'award_type_id' => $awardType->id, + 'award_date' => $faker->dateTimeBetween('-2 years', 'now')->format('Y-m-d'), + 'gift' => $faker->randomElement($gifts), + 'monetary_value' => $faker->optional(0.6)->randomFloat(2, 500, 10000), + 'description' => $this->getAwardDescription($awardType->name, $awardDescriptions), + 'certificate' => randomImage(), + 'photo' => randomImage(), + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create award for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Award seeder completed successfully!'); + } + + /** + * Get award description based on award type + */ + private function getAwardDescription($awardTypeName, $awardDescriptions) + { + if (isset($awardDescriptions[$awardTypeName])) { + $descriptions = $awardDescriptions[$awardTypeName]; + return $descriptions[array_rand($descriptions)]; + } + + // Default description if award type not found + return 'Recognized for outstanding performance and valuable contribution to the organization'; + } +} diff --git a/database/seeders/AwardTypeSeeder.php b/database/seeders/AwardTypeSeeder.php new file mode 100644 index 000000000..605ee441a --- /dev/null +++ b/database/seeders/AwardTypeSeeder.php @@ -0,0 +1,68 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Award types with realistic data + $awardTypes = [ + ['name' => 'Employee of the Month', 'description' => 'Recognition for outstanding performance and dedication during a specific month'], + ['name' => 'Employee of the Year', 'description' => 'Annual recognition for exceptional contribution and consistent excellence throughout the year'], + ['name' => 'Excellence Award', 'description' => 'Recognition for achieving excellence in work quality, innovation, and professional standards'], + ['name' => 'Innovation Award', 'description' => 'Recognition for creative thinking, innovative solutions, and process improvements'], + ['name' => 'Leadership Award', 'description' => 'Recognition for exceptional leadership qualities, team management, and mentoring abilities'], + ['name' => 'Customer Service Award', 'description' => 'Recognition for outstanding customer service, client satisfaction, and relationship management'], + ['name' => 'Team Player Award', 'description' => 'Recognition for collaboration, teamwork, and positive contribution to team dynamics'], + ['name' => 'Achievement Award', 'description' => 'Recognition for achieving specific goals, targets, or milestones in projects or performance'], + ['name' => 'Long Service Award', 'description' => 'Recognition for loyalty, commitment, and long-term service to the organization'], + ['name' => 'Safety Award', 'description' => 'Recognition for maintaining workplace safety standards and promoting safety awareness'], + ['name' => 'Quality Award', 'description' => 'Recognition for maintaining high quality standards and continuous improvement in work processes'], + ['name' => 'Sales Achievement Award', 'description' => 'Recognition for exceptional sales performance, revenue generation, and client acquisition'] + ]; + + foreach ($companies as $company) { + // Create 6-8 award types for each company + $awardCount = rand(6, 8); + + for ($i = 0; $i < $awardCount; $i++) { + $awardType = $awardTypes[$i]; + + // Check if award type already exists for this company + if (AwardType::where('name', $awardType['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + AwardType::create([ + 'name' => $awardType['name'], + 'description' => $awardType['description'], + 'status' => 'active', + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create award type: ' . $awardType['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('AwardType seeder completed successfully!'); + } +} diff --git a/database/seeders/BranchSeeder.php b/database/seeders/BranchSeeder.php new file mode 100644 index 000000000..0b17cf21d --- /dev/null +++ b/database/seeders/BranchSeeder.php @@ -0,0 +1,74 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Branch names array + $branchNames = [ + 'Main Office', + 'Downtown Branch', + 'North Branch', + 'South Branch', + 'East Branch', + 'West Branch', + 'Corporate Center', + 'Business Park', + 'City Center' + ]; + + foreach ($companies as $company) { + // Create 8-9 branches for each company + $branchCount = rand(8, 9); + + for ($i = 0; $i < $branchCount; $i++) { + $branchName = $branchNames[$i]; + + // Check if branch already exists for this company + if (Branch::where('name', $branchName)->where('created_by', $company->id)->exists()) { + continue; + } + + try { + Branch::create([ + 'name' => $branchName, + 'address' => $faker->streetAddress, + 'city' => $faker->city, + 'state' => $faker->state, + 'country' => $faker->country, + 'zip_code' => $faker->postcode, + 'phone' => $faker->phoneNumber, + 'email' => strtolower(str_replace(' ', '', $branchName)) . '@' . strtolower(str_replace(' ', '', $company->name)) . '.com', + 'status' => 'active', + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create branch: ' . $branchName . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Branch seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/CandidateAssessmentSeeder.php b/database/seeders/CandidateAssessmentSeeder.php new file mode 100644 index 000000000..292fcb898 --- /dev/null +++ b/database/seeders/CandidateAssessmentSeeder.php @@ -0,0 +1,167 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed assessment data for consistent results + $assessmentData = [ + [ + 'assessment_name' => 'Technical Skills Assessment', + 'score' => 85, + 'max_score' => 100, + 'pass_fail_status' => 'Pass', + 'comments' => 'Strong technical knowledge demonstrated. Excellent problem-solving skills and good understanding of programming concepts.', + 'days_ago' => 3 + ], + [ + 'assessment_name' => 'Aptitude Test', + 'score' => 78, + 'max_score' => 100, + 'pass_fail_status' => 'Pass', + 'comments' => 'Good analytical and logical reasoning abilities. Shows potential for learning and adaptation.', + 'days_ago' => 5 + ], + [ + 'assessment_name' => 'Communication Skills Test', + 'score' => 92, + 'max_score' => 100, + 'pass_fail_status' => 'Pass', + 'comments' => 'Excellent verbal and written communication skills. Clear articulation and professional presentation.', + 'days_ago' => 2 + ], + [ + 'assessment_name' => 'Personality Assessment', + 'score' => 88, + 'max_score' => 100, + 'pass_fail_status' => 'Pass', + 'comments' => 'Well-balanced personality traits. Shows good teamwork potential and leadership qualities.', + 'days_ago' => 4 + ], + [ + 'assessment_name' => 'Domain Knowledge Test', + 'score' => 65, + 'max_score' => 100, + 'pass_fail_status' => 'Fail', + 'comments' => 'Basic understanding of domain concepts but lacks depth in specialized areas. Requires additional training.', + 'days_ago' => 6 + ], + [ + 'assessment_name' => 'Coding Challenge', + 'score' => 95, + 'max_score' => 100, + 'pass_fail_status' => 'Pass', + 'comments' => 'Outstanding coding skills with clean, efficient solutions. Demonstrates excellent algorithmic thinking.', + 'days_ago' => 1 + ], + [ + 'assessment_name' => 'Case Study Analysis', + 'score' => 72, + 'max_score' => 100, + 'pass_fail_status' => 'Pass', + 'comments' => 'Good analytical approach to business problems. Shows practical thinking and solution-oriented mindset.', + 'days_ago' => 7 + ], + [ + 'assessment_name' => 'Group Discussion', + 'score' => 80, + 'max_score' => 100, + 'pass_fail_status' => 'Pass', + 'comments' => 'Active participation in group discussions. Good listening skills and ability to contribute meaningfully.', + 'days_ago' => 8 + ], + [ + 'assessment_name' => 'Presentation Skills', + 'score' => 45, + 'max_score' => 100, + 'pass_fail_status' => 'Fail', + 'comments' => 'Needs significant improvement in presentation delivery. Lacks confidence and clarity in communication.', + 'days_ago' => 9 + ], + [ + 'assessment_name' => 'Logical Reasoning Test', + 'score' => null, + 'max_score' => 100, + 'pass_fail_status' => 'Pending', + 'comments' => 'Assessment scheduled but not yet completed. Candidate to appear for test next week.', + 'days_ago' => 0 + ] + ]; + + foreach ($companies as $company) { + // Get candidates for this company + $candidates = Candidate::where('created_by', $company->id)->get(); + + if ($candidates->isEmpty()) { + $this->command->warn('No candidates found for company: ' . $company->name . '. Please run CandidateSeeder first.'); + continue; + } + + // Get employees who can conduct assessments + $assessors = User::whereIn('type', ['manager', 'hr', 'employee']) + ->where('created_by', $company->id) + ->get(); + + if ($assessors->isEmpty()) { + $this->command->warn('No assessors found for company: ' . $company->name); + continue; + } + + // Create assessments for first 5 candidates + $selectedCandidates = $candidates->take(7); + + foreach ($selectedCandidates as $candIndex => $candidate) { + // Create 2 assessments per candidate + for ($assessIndex = 0; $assessIndex < 2; $assessIndex++) { + $dataIndex = ($candIndex * 2) + $assessIndex; + $assessment = $assessmentData[$dataIndex % 10]; + + // Select assessor from first 5 + $selectedAssessors = $assessors->take(5); + $assessor = $selectedAssessors->random(); + + $assessmentDate = $assessment['days_ago'] > 0 ? + date('Y-m-d', strtotime('-' . $assessment['days_ago'] . ' days')) : + date('Y-m-d'); + + try { + CandidateAssessment::create([ + 'candidate_id' => $candidate->id, + 'assessment_name' => $assessment['assessment_name'], + 'score' => $assessment['score'], + 'max_score' => $assessment['max_score'], + 'pass_fail_status' => $assessment['pass_fail_status'], + 'comments' => $assessment['comments'], + 'conducted_by' => $assessor->id, + 'assessment_date' => $assessmentDate, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create assessment for candidate: ' . $candidate->first_name . ' ' . $candidate->last_name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('CandidateAssessment seeder completed successfully!'); + } +} diff --git a/database/seeders/CandidateOnboardingSeeder.php b/database/seeders/CandidateOnboardingSeeder.php new file mode 100644 index 000000000..b402e9569 --- /dev/null +++ b/database/seeders/CandidateOnboardingSeeder.php @@ -0,0 +1,127 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed onboarding statuses for consistent data + $onboardingStatuses = ['Completed', 'In Progress', 'Completed', 'Pending', 'Completed', 'In Progress', 'Completed', 'Pending', 'Completed', 'In Progress']; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->where('status', 'active') + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get onboarding checklists for this company + $onboardingChecklists = OnboardingChecklist::where('created_by', $company->id)->get(); + + if ($onboardingChecklists->isEmpty()) { + $this->command->warn('No onboarding checklists found for company: ' . $company->name . '. Please run OnboardingChecklistSeeder first.'); + continue; + } + + // Get employees for buddy assignment (excluding current employee) + $buddyEmployees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + foreach ($employees as $index => $employee) { + // Check if onboarding already exists for this employee + if (CandidateOnboarding::where('employee_id', $employee->id)->where('created_by', $company->id)->exists()) { + continue; + } + + // Select checklist based on job title or use default + $checklist = $this->selectChecklistForEmployee($employee, $onboardingChecklists); + $buddyEmployee = $buddyEmployees->where('id', '!=', $employee->id)->random(); + + $status = $onboardingStatuses[$index % 10]; + $startDate = date('Y-m-d', strtotime('-' . ($index + 1) . ' days')); + + try { + CandidateOnboarding::create([ + 'candidate_id' => null, + 'employee_id' => $employee->id, + 'checklist_id' => $checklist->id, + 'start_date' => $startDate, + 'buddy_employee_id' => $buddyEmployee?->id, + 'status' => $status, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create onboarding for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('CandidateOnboarding seeder completed successfully!'); + } + + /** + * Select appropriate checklist based on employee's job title + */ + private function selectChecklistForEmployee($employee, $checklists) + { + $jobTitle = strtolower($employee->employee->designation->name ?? ''); + + // Match checklist based on job title keywords + if (str_contains($jobTitle, 'developer') || str_contains($jobTitle, 'engineer') || str_contains($jobTitle, 'devops')) { + $checklist = $checklists->where('name', 'Technical Team Onboarding')->first(); + if ($checklist) + return $checklist; + } + + if (str_contains($jobTitle, 'manager') || str_contains($jobTitle, 'director') || str_contains($jobTitle, 'lead')) { + $checklist = $checklists->where('name', 'Management Level Onboarding')->first(); + if ($checklist) + return $checklist; + } + + if (str_contains($jobTitle, 'sales') || str_contains($jobTitle, 'business development')) { + $checklist = $checklists->where('name', 'Sales Team Onboarding')->first(); + if ($checklist) + return $checklist; + } + + if (str_contains($jobTitle, 'customer') || str_contains($jobTitle, 'support')) { + $checklist = $checklists->where('name', 'Customer Service Onboarding')->first(); + if ($checklist) + return $checklist; + } + + if (str_contains($jobTitle, 'intern')) { + $checklist = $checklists->where('name', 'Intern Onboarding Program')->first(); + if ($checklist) + return $checklist; + } + + // Default to standard onboarding or first available checklist + $defaultChecklist = $checklists->where('is_default', true)->first(); + return $defaultChecklist ?: $checklists->first(); + } +} diff --git a/database/seeders/CandidateSeeder.php b/database/seeders/CandidateSeeder.php new file mode 100644 index 000000000..56fa90f4f --- /dev/null +++ b/database/seeders/CandidateSeeder.php @@ -0,0 +1,168 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // 30 candidates: 5 Offer, 5 New, 5 Offer, 5 Interview, 5 Screening, 5 Offer + $candidates = [ + ['first_name' => 'Rajesh', 'last_name' => 'Kumar', 'email' => 'rajesh.kumar@email.com', 'phone' => '+91-9876543210', 'gender' => 'male', 'date_of_birth' => '1990-05-15', 'address' => '123 MG Road', 'city' => 'Bangalore', 'state' => 'Karnataka', 'zip_code' => '560001', 'country' => 'India', 'current_company' => 'Tech Solutions Pvt Ltd', 'current_position' => 'Senior Developer', 'experience_years' => 6, 'current_salary' => 95000, 'expected_salary' => 120000, 'notice_period' => '2 months', 'skills' => 'JavaScript, React, Node.js, Python, SQL, MongoDB', 'education' => 'B.Tech in Computer Science from IIT Delhi', 'portfolio_url' => 'https://rajeshkumar.dev', 'linkedin_url' => 'https://linkedin.com/in/rajeshkumar', 'coverletter_message' => 'I am excited to apply for this position and bring my 6 years of experience in software development.', 'status' => 'Offer', 'source' => 'LinkedIn'], + ['first_name' => 'Priya', 'last_name' => 'Sharma', 'email' => 'priya.sharma@email.com', 'phone' => '+91-9876543211', 'gender' => 'female', 'date_of_birth' => '1992-08-22', 'address' => '456 Park Street', 'city' => 'Mumbai', 'state' => 'Maharashtra', 'zip_code' => '400001', 'country' => 'India', 'current_company' => 'Digital Marketing Agency', 'current_position' => 'Marketing Manager', 'experience_years' => 4, 'current_salary' => 70000, 'expected_salary' => 85000, 'notice_period' => '1 month', 'skills' => 'Digital Marketing, SEO, SEM, Social Media, Analytics', 'education' => 'MBA in Marketing from Mumbai University', 'portfolio_url' => null, 'linkedin_url' => 'https://linkedin.com/in/priyasharma', 'coverletter_message' => 'With 4 years of experience in digital marketing, I am confident I can contribute to your team.', 'status' => 'Offer', 'source' => 'Naukri.com'], + ['first_name' => 'Amit', 'last_name' => 'Patel', 'email' => 'amit.patel@email.com', 'phone' => '+91-9876543212', 'current_company' => 'HR Consultancy Services', 'current_position' => 'HR Specialist', 'experience_years' => 3, 'current_salary' => 60000, 'expected_salary' => 75000, 'notice_period' => '1 month', 'skills' => 'Recruitment, Employee Relations, Performance Management', 'education' => 'Masters in Human Resources from Pune University', 'portfolio_url' => null, 'linkedin_url' => 'https://linkedin.com/in/amitpatel', 'status' => 'Offer', 'source' => 'Employee Referral'], + ['first_name' => 'Sneha', 'last_name' => 'Reddy', 'email' => 'sneha.reddy@email.com', 'phone' => '+91-9876543213', 'current_company' => 'Financial Services Ltd', 'current_position' => 'Financial Analyst', 'experience_years' => 2, 'current_salary' => 55000, 'expected_salary' => 70000, 'notice_period' => '2 months', 'skills' => 'Financial Analysis, Excel, SQL, Financial Modeling', 'education' => 'CA from ICAI, B.Com from Osmania University', 'portfolio_url' => null, 'linkedin_url' => 'https://linkedin.com/in/snehareddy', 'status' => 'Offer', 'source' => 'Company Website'], + ['first_name' => 'Vikram', 'last_name' => 'Singh', 'email' => 'vikram.singh@email.com', 'phone' => '+91-9876543214', 'current_company' => 'Operations Excellence Corp', 'current_position' => 'Operations Executive', 'experience_years' => 5, 'current_salary' => 80000, 'expected_salary' => 100000, 'notice_period' => '3 months', 'skills' => 'Operations Management, Process Improvement, Team Leadership', 'education' => 'MBA in Operations from Bangalore University', 'portfolio_url' => null, 'linkedin_url' => 'https://linkedin.com/in/vikramsingh', 'status' => 'Offer', 'source' => 'Indeed'], + ['first_name' => 'Kavya', 'last_name' => 'Nair', 'email' => 'kavya.nair@email.com', 'phone' => '+91-9876543215', 'current_company' => 'Customer Care Solutions', 'current_position' => 'Customer Support Lead', 'experience_years' => 3, 'current_salary' => 50000, 'expected_salary' => 65000, 'notice_period' => '1 month', 'skills' => 'Customer Service, CRM, Communication, Problem Solving', 'education' => 'BBA from Kerala University', 'portfolio_url' => null, 'linkedin_url' => 'https://linkedin.com/in/kavyanair', 'status' => 'Offer', 'source' => 'Recruitment Agency'], + ['first_name' => 'Arjun', 'last_name' => 'Gupta', 'email' => 'arjun.gupta@email.com', 'phone' => '+91-9876543216', 'current_company' => null, 'current_position' => 'Fresh Graduate', 'experience_years' => 0, 'current_salary' => null, 'expected_salary' => 35000, 'notice_period' => 'Immediate', 'skills' => 'Java, Python, Web Development, Database Management', 'education' => 'B.Tech in Computer Science from NIT Warangal', 'portfolio_url' => 'https://arjungupta.github.io', 'linkedin_url' => 'https://linkedin.com/in/arjungupta', 'status' => 'Offer', 'source' => 'Campus Recruitment'], + ['first_name' => 'Meera', 'last_name' => 'Joshi', 'email' => 'meera.joshi@email.com', 'phone' => '+91-9876543217', 'current_company' => 'Design Studio', 'current_position' => 'UI Designer', 'experience_years' => 2, 'current_salary' => 45000, 'expected_salary' => 60000, 'notice_period' => '1 month', 'skills' => 'UI/UX Design, Figma, Adobe Creative Suite, Prototyping', 'education' => 'Bachelor of Design from NIFT Delhi', 'portfolio_url' => 'https://meerajoshi.design', 'linkedin_url' => 'https://linkedin.com/in/meerajoshi', 'status' => 'Offer', 'source' => 'Walk-in Interview'], + ['first_name' => 'Rohit', 'last_name' => 'Agarwal', 'email' => 'rohit.agarwal@email.com', 'phone' => '+91-9876543218', 'current_company' => 'Software Solutions Inc', 'current_position' => 'Backend Developer', 'experience_years' => 4, 'current_salary' => 85000, 'expected_salary' => 105000, 'notice_period' => '2 months', 'skills' => 'Java, Spring Boot, Microservices, AWS', 'education' => 'B.Tech in Computer Science from VIT', 'portfolio_url' => null, 'linkedin_url' => 'https://linkedin.com/in/rohitagarwal', 'status' => 'Offer', 'source' => 'LinkedIn'], + ['first_name' => 'Anita', 'last_name' => 'Desai', 'email' => 'anita.desai@email.com', 'phone' => '+91-9876543219', 'current_company' => 'Marketing Pro', 'current_position' => 'Content Manager', 'experience_years' => 3, 'current_salary' => 65000, 'expected_salary' => 80000, 'notice_period' => '1 month', 'skills' => 'Content Marketing, Copywriting, Social Media', 'education' => 'Masters in Mass Communication', 'portfolio_url' => null, 'linkedin_url' => 'https://linkedin.com/in/anitadesai', 'status' => 'Offer', 'source' => 'Naukri.com'], + ['first_name' => 'Karan', 'last_name' => 'Mehta', 'email' => 'karan.mehta@email.com', 'phone' => '+91-9876543220', 'gender' => 'male', 'date_of_birth' => '1989-03-10', 'address' => '789 Finance Street', 'city' => 'Delhi', 'state' => 'Delhi', 'zip_code' => '110001', 'country' => 'India', 'current_company' => 'Finance Corp', 'current_position' => 'Senior Analyst', 'experience_years' => 5, 'current_salary' => 90000, 'expected_salary' => 110000, 'notice_period' => '3 months', 'skills' => 'Financial Planning, Risk Analysis, Investment', 'education' => 'MBA in Finance from IIM', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Offer', 'source' => 'Company Website'], + ['first_name' => 'Deepika', 'last_name' => 'Rao', 'email' => 'deepika.rao@email.com', 'phone' => '+91-9876543221', 'gender' => 'female', 'date_of_birth' => '1991-06-18', 'address' => '321 HR Avenue', 'city' => 'Chennai', 'state' => 'Tamil Nadu', 'zip_code' => '600001', 'country' => 'India', 'current_company' => 'HR Solutions', 'current_position' => 'Talent Manager', 'experience_years' => 4, 'current_salary' => 75000, 'expected_salary' => 95000, 'notice_period' => '2 months', 'skills' => 'Talent Acquisition, HR Analytics, Employee Engagement', 'education' => 'Masters in HR Management', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Screening', 'source' => 'Employee Referral'], + ['first_name' => 'Sanjay', 'last_name' => 'Verma', 'email' => 'sanjay.verma@email.com', 'phone' => '+91-9876543222', 'gender' => 'male', 'date_of_birth' => '1987-11-25', 'address' => '654 Operations Road', 'city' => 'Pune', 'state' => 'Maharashtra', 'zip_code' => '411001', 'country' => 'India', 'current_company' => 'Operations Hub', 'current_position' => 'Process Manager', 'experience_years' => 6, 'current_salary' => 100000, 'expected_salary' => 125000, 'notice_period' => '3 months', 'skills' => 'Process Optimization, Six Sigma, Lean Management', 'education' => 'MBA in Operations Management', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'New', 'source' => 'Indeed'], + ['first_name' => 'Pooja', 'last_name' => 'Iyer', 'email' => 'pooja.iyer@email.com', 'phone' => '+91-9876543223', 'gender' => 'female', 'date_of_birth' => '1990-09-14', 'address' => '987 Service Lane', 'city' => 'Hyderabad', 'state' => 'Telangana', 'zip_code' => '500001', 'country' => 'India', 'current_company' => 'Customer First', 'current_position' => 'Service Manager', 'experience_years' => 4, 'current_salary' => 70000, 'expected_salary' => 85000, 'notice_period' => '2 months', 'skills' => 'Customer Relations, Service Excellence, Team Management', 'education' => 'MBA in Service Management', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Interview', 'source' => 'Recruitment Agency'], + ['first_name' => 'Rahul', 'last_name' => 'Jain', 'email' => 'rahul.jain@email.com', 'phone' => '+91-9876543224', 'gender' => 'male', 'date_of_birth' => '1998-12-05', 'address' => '234 Campus Road', 'city' => 'Pilani', 'state' => 'Rajasthan', 'zip_code' => '333031', 'country' => 'India', 'current_company' => null, 'current_position' => 'Recent Graduate', 'experience_years' => 0, 'current_salary' => null, 'expected_salary' => 40000, 'notice_period' => 'Immediate', 'skills' => 'Python, Machine Learning, Data Analysis', 'education' => 'B.Tech in Computer Science from BITS', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'New', 'source' => 'Campus Recruitment'], + ['first_name' => 'Neha', 'last_name' => 'Kapoor', 'email' => 'neha.kapoor@email.com', 'phone' => '+91-9876543225', 'gender' => 'female', 'date_of_birth' => '1993-04-20', 'address' => '567 Design Street', 'city' => 'Jaipur', 'state' => 'Rajasthan', 'zip_code' => '302001', 'country' => 'India', 'current_company' => 'Creative Agency', 'current_position' => 'Graphic Designer', 'experience_years' => 3, 'current_salary' => 55000, 'expected_salary' => 70000, 'notice_period' => '1 month', 'skills' => 'Graphic Design, Branding, Adobe Creative Suite', 'education' => 'Bachelor of Fine Arts', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Screening', 'source' => 'Walk-in Interview'], + ['first_name' => 'Arun', 'last_name' => 'Krishnan', 'email' => 'arun.krishnan@email.com', 'phone' => '+91-9876543226', 'gender' => 'male', 'date_of_birth' => '1989-07-30', 'address' => '890 Tech Park', 'city' => 'Kochi', 'state' => 'Kerala', 'zip_code' => '682001', 'country' => 'India', 'current_company' => 'Tech Innovations', 'current_position' => 'DevOps Engineer', 'experience_years' => 5, 'current_salary' => 95000, 'expected_salary' => 115000, 'notice_period' => '2 months', 'skills' => 'Docker, Kubernetes, CI/CD, AWS, Jenkins', 'education' => 'B.Tech in Information Technology', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Screening', 'source' => 'LinkedIn'], + ['first_name' => 'Swati', 'last_name' => 'Bansal', 'email' => 'swati.bansal@email.com', 'phone' => '+91-9876543227', 'gender' => 'female', 'date_of_birth' => '1990-02-12', 'address' => '123 Product Lane', 'city' => 'Gurgaon', 'state' => 'Haryana', 'zip_code' => '122001', 'country' => 'India', 'current_company' => 'Digital Solutions', 'current_position' => 'Product Manager', 'experience_years' => 4, 'current_salary' => 110000, 'expected_salary' => 135000, 'notice_period' => '3 months', 'skills' => 'Product Strategy, Agile, User Research, Analytics', 'education' => 'MBA in Product Management', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Offer', 'source' => 'Naukri.com'], + ['first_name' => 'Manish', 'last_name' => 'Gupta', 'email' => 'manish.gupta@email.com', 'phone' => '+91-9876543228', 'gender' => 'male', 'date_of_birth' => '1992-10-08', 'address' => '456 Data Street', 'city' => 'Noida', 'state' => 'Uttar Pradesh', 'zip_code' => '201301', 'country' => 'India', 'current_company' => 'Data Analytics Co', 'current_position' => 'Data Scientist', 'experience_years' => 3, 'current_salary' => 85000, 'expected_salary' => 105000, 'notice_period' => '2 months', 'skills' => 'Machine Learning, Python, R, SQL, Statistics', 'education' => 'Masters in Data Science', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Interview', 'source' => 'Company Website'], + ['first_name' => 'Ravi', 'last_name' => 'Tiwari', 'email' => 'ravi.tiwari@email.com', 'phone' => '+91-9876543229', 'gender' => 'male', 'date_of_birth' => '1991-01-28', 'address' => '789 Consulting Plaza', 'city' => 'Kolkata', 'state' => 'West Bengal', 'zip_code' => '700001', 'country' => 'India', 'current_company' => 'Consulting Firm', 'current_position' => 'Business Analyst', 'experience_years' => 3, 'current_salary' => 75000, 'expected_salary' => 90000, 'notice_period' => '2 months', 'skills' => 'Business Analysis, Requirements Gathering, Process Mapping', 'education' => 'MBA from ISB Hyderabad', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'New', 'source' => 'Indeed'], + ['first_name' => 'Divya', 'last_name' => 'Menon', 'email' => 'divya.menon@email.com', 'phone' => '+91-9876543230', 'gender' => 'female', 'date_of_birth' => '1992-05-16', 'address' => '321 QA Road', 'city' => 'Bangalore', 'state' => 'Karnataka', 'zip_code' => '560002', 'country' => 'India', 'current_company' => 'Tech Corp', 'current_position' => 'QA Engineer', 'experience_years' => 3, 'current_salary' => 60000, 'expected_salary' => 75000, 'notice_period' => '1 month', 'skills' => 'Manual Testing, Automation, Selenium', 'education' => 'B.Tech in Computer Science', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'New', 'source' => 'LinkedIn'], + ['first_name' => 'Suresh', 'last_name' => 'Nair', 'email' => 'suresh.nair@email.com', 'phone' => '+91-9876543231', 'gender' => 'male', 'date_of_birth' => '1988-08-22', 'address' => '654 Sales Avenue', 'city' => 'Mumbai', 'state' => 'Maharashtra', 'zip_code' => '400002', 'country' => 'India', 'current_company' => 'Sales Pro', 'current_position' => 'Sales Manager', 'experience_years' => 5, 'current_salary' => 80000, 'expected_salary' => 100000, 'notice_period' => '2 months', 'skills' => 'Sales, Negotiation, CRM', 'education' => 'MBA in Sales', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Offer', 'source' => 'Company Website'], + ['first_name' => 'Lakshmi', 'last_name' => 'Pillai', 'email' => 'lakshmi.pillai@email.com', 'phone' => '+91-9876543232', 'gender' => 'female', 'date_of_birth' => '1990-11-30', 'address' => '987 Finance Hub', 'city' => 'Chennai', 'state' => 'Tamil Nadu', 'zip_code' => '600002', 'country' => 'India', 'current_company' => 'Finance Hub', 'current_position' => 'Accountant', 'experience_years' => 4, 'current_salary' => 65000, 'expected_salary' => 80000, 'notice_period' => '1 month', 'skills' => 'Accounting, Tally, GST', 'education' => 'B.Com, CA Inter', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Offer', 'source' => 'Naukri.com'], + ['first_name' => 'Vishal', 'last_name' => 'Yadav', 'email' => 'vishal.yadav@email.com', 'phone' => '+91-9876543233', 'gender' => 'male', 'date_of_birth' => '1989-03-18', 'address' => '234 IT Park', 'city' => 'Pune', 'state' => 'Maharashtra', 'zip_code' => '411002', 'country' => 'India', 'current_company' => 'IT Services', 'current_position' => 'System Admin', 'experience_years' => 4, 'current_salary' => 70000, 'expected_salary' => 85000, 'notice_period' => '2 months', 'skills' => 'Linux, Windows Server, Networking', 'education' => 'B.Tech in IT', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Offer', 'source' => 'Indeed'], + ['first_name' => 'Anjali', 'last_name' => 'Saxena', 'email' => 'anjali.saxena@email.com', 'phone' => '+91-9876543234', 'gender' => 'female', 'date_of_birth' => '1994-07-25', 'address' => '567 Media Street', 'city' => 'Delhi', 'state' => 'Delhi', 'zip_code' => '110002', 'country' => 'India', 'current_company' => 'Media House', 'current_position' => 'Content Writer', 'experience_years' => 2, 'current_salary' => 40000, 'expected_salary' => 55000, 'notice_period' => '1 month', 'skills' => 'Content Writing, SEO, Blogging', 'education' => 'MA in English', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Interview', 'source' => 'Walk-in Interview'], + ['first_name' => 'Harish', 'last_name' => 'Kumar', 'email' => 'harish.kumar@email.com', 'phone' => '+91-9876543235', 'gender' => 'male', 'date_of_birth' => '1987-12-10', 'address' => '890 Logistics Road', 'city' => 'Ahmedabad', 'state' => 'Gujarat', 'zip_code' => '380001', 'country' => 'India', 'current_company' => 'Logistics Co', 'current_position' => 'Operations Manager', 'experience_years' => 6, 'current_salary' => 95000, 'expected_salary' => 115000, 'notice_period' => '3 months', 'skills' => 'Supply Chain, Logistics, Operations', 'education' => 'MBA in Operations', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Interview', 'source' => 'Employee Referral'], + ['first_name' => 'Nisha', 'last_name' => 'Agarwal', 'email' => 'nisha.agarwal@email.com', 'phone' => '+91-9876543236', 'gender' => 'female', 'date_of_birth' => '1992-04-14', 'address' => '123 Design Hub', 'city' => 'Jaipur', 'state' => 'Rajasthan', 'zip_code' => '302002', 'country' => 'India', 'current_company' => 'Design Co', 'current_position' => 'Web Designer', 'experience_years' => 3, 'current_salary' => 50000, 'expected_salary' => 65000, 'notice_period' => '1 month', 'skills' => 'HTML, CSS, JavaScript, Photoshop', 'education' => 'Bachelor in Design', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Interview', 'source' => 'LinkedIn'], + ['first_name' => 'Ramesh', 'last_name' => 'Babu', 'email' => 'ramesh.babu@email.com', 'phone' => '+91-9876543237', 'gender' => 'male', 'date_of_birth' => '1988-09-05', 'address' => '456 Retail Street', 'city' => 'Coimbatore', 'state' => 'Tamil Nadu', 'zip_code' => '641001', 'country' => 'India', 'current_company' => 'Retail Chain', 'current_position' => 'Store Manager', 'experience_years' => 5, 'current_salary' => 60000, 'expected_salary' => 75000, 'notice_period' => '2 months', 'skills' => 'Retail Management, Inventory, Sales', 'education' => 'MBA in Retail', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Screening', 'source' => 'Company Website'], + ['first_name' => 'Shweta', 'last_name' => 'Mishra', 'email' => 'shweta.mishra@email.com', 'phone' => '+91-9876543238', 'gender' => 'female', 'date_of_birth' => '1993-06-20', 'address' => '789 Pharma Lane', 'city' => 'Lucknow', 'state' => 'Uttar Pradesh', 'zip_code' => '226001', 'country' => 'India', 'current_company' => 'Pharma Ltd', 'current_position' => 'Medical Rep', 'experience_years' => 2, 'current_salary' => 45000, 'expected_salary' => 60000, 'notice_period' => '1 month', 'skills' => 'Sales, Medical Knowledge, Communication', 'education' => 'B.Pharm', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Screening', 'source' => 'Recruitment Agency'], + ['first_name' => 'Tarun', 'last_name' => 'Malhotra', 'email' => 'tarun.malhotra@email.com', 'phone' => '+91-9876543239', 'gender' => 'male', 'date_of_birth' => '1990-10-15', 'address' => '321 Consulting Tower', 'city' => 'Bangalore', 'state' => 'Karnataka', 'zip_code' => '560003', 'country' => 'India', 'current_company' => 'Consulting Group', 'current_position' => 'Consultant', 'experience_years' => 4, 'current_salary' => 85000, 'expected_salary' => 105000, 'notice_period' => '2 months', 'skills' => 'Strategy, Analysis, Presentation', 'education' => 'MBA from IIM', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Offer', 'source' => 'Naukri.com'], + ['first_name' => 'Geeta', 'last_name' => 'Devi', 'email' => 'geeta.devi@email.com', 'phone' => '+91-9876543240', 'gender' => 'female', 'date_of_birth' => '1991-02-28', 'address' => '654 Education Road', 'city' => 'Patna', 'state' => 'Bihar', 'zip_code' => '800001', 'country' => 'India', 'current_company' => 'Education Hub', 'current_position' => 'Teacher', 'experience_years' => 3, 'current_salary' => 35000, 'expected_salary' => 45000, 'notice_period' => '1 month', 'skills' => 'Teaching, Communication, Curriculum', 'education' => 'B.Ed, MA', 'portfolio_url' => null, 'linkedin_url' => null, 'status' => 'Offer', 'source' => 'Walk-in Interview'] + ]; + + foreach ($companies as $company) { + // Get job postings for this company + $jobPostings = JobPosting::where('created_by', $company->id)->get(); + + if ($jobPostings->isEmpty()) { + $this->command->warn('No job postings found for company: ' . $company->name . '. Please run JobPostingSeeder first.'); + continue; + } + + // Get candidate sources for this company + $candidateSources = CandidateSource::where('created_by', $company->id)->get(); + + if ($candidateSources->isEmpty()) { + $this->command->warn('No candidate sources found for company: ' . $company->name . '. Please run CandidateSourceSeeder first.'); + continue; + } + + // Get employees for referrals + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + // Get custom questions + $customQuestions = CustomQuestion::where('created_by', $company->id)->pluck('id')->toArray(); + + // Create 30 candidates + foreach ($candidates as $index => $candidateData) { + // Cycle through job postings + $jobPosting = $jobPostings[$index % $jobPostings->count()]; + + // Find matching source + $source = $candidateSources->where('name', $candidateData['source'])->first(); + if (!$source) $source = $candidateSources->first(); + + // Set referral employee for employee referral source + $referralEmployee = null; + if ($candidateData['source'] === 'Employee Referral' && $employees->isNotEmpty()) { + $referralEmployee = $employees->first(); + } + + // Check if candidate already exists for this company + if (Candidate::where('email', $candidateData['email'])->where('created_by', $company->id)->exists()) { + continue; + } + + $applicationDate = date('Y-m-d', strtotime('-' . ($index + 1) . ' days')); + + // Generate custom question answers + $customQuestionAnswers = null; + if (!empty($jobPosting->custom_question)) { + $customQuestionAnswers = []; + $jobCustomQuestions = CustomQuestion::whereIn('id', $jobPosting->custom_question)->get(); + foreach ($jobCustomQuestions as $question) { + $customQuestionAnswers[$question->question] = 'Sample answer for: ' . $question->question; + } + } + + try { + Candidate::create([ + 'job_id' => $jobPosting->id, + 'source_id' => $source->id, + 'branch_id' => $jobPosting->branch_id, + 'department_id' => $jobPosting->department_id, + 'first_name' => $candidateData['first_name'], + 'last_name' => $candidateData['last_name'], + 'email' => $candidateData['email'], + 'phone' => $candidateData['phone'], + 'gender' => $candidateData['gender'] ?? null, + 'date_of_birth' => $candidateData['date_of_birth'] ?? null, + 'address' => $candidateData['address'] ?? null, + 'city' => $candidateData['city'] ?? null, + 'state' => $candidateData['state'] ?? null, + 'zip_code' => $candidateData['zip_code'] ?? null, + 'country' => $candidateData['country'] ?? null, + 'current_company' => $candidateData['current_company'], + 'current_position' => $candidateData['current_position'], + 'experience_years' => $candidateData['experience_years'], + 'current_salary' => $candidateData['current_salary'], + 'expected_salary' => $candidateData['expected_salary'], + 'final_salary' => null, + 'notice_period' => $candidateData['notice_period'], + 'resume_path' => 'resumes/' . strtolower($candidateData['first_name']) . '_' . strtolower($candidateData['last_name']) . '_resume.pdf', + 'cover_letter_path' => null, + 'coverletter_message' => $candidateData['coverletter_message'] ?? null, + 'skills' => $candidateData['skills'], + 'education' => $candidateData['education'], + 'portfolio_url' => null, + 'linkedin_url' => null, + 'referral_employee_id' => $referralEmployee?->id, + 'rating' => $candidateData['rating'] ?? null, + 'is_archive' => false, + 'is_employee' => false, + 'custom_question' => $customQuestionAnswers, + 'terms_condition_check' => 'on', + 'status' => $candidateData['status'], + 'application_date' => $applicationDate, + 'created_by' => $company->id, + ]); + + } catch (\Exception $e) { + $this->command->error('Failed to create candidate: ' . $candidateData['first_name'] . ' ' . $candidateData['last_name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Candidate seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/CandidateSourceSeeder.php b/database/seeders/CandidateSourceSeeder.php new file mode 100644 index 000000000..3329b11fd --- /dev/null +++ b/database/seeders/CandidateSourceSeeder.php @@ -0,0 +1,111 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed candidate sources for consistent data + $candidateSources = [ + [ + 'name' => 'Company Website', + 'description' => 'Candidates who applied directly through the company career page and job portal', + 'status' => 'active' + ], + [ + 'name' => 'LinkedIn', + 'description' => 'Professional networking platform for recruiting and sourcing qualified candidates', + 'status' => 'active' + ], + [ + 'name' => 'Naukri.com', + 'description' => 'Leading job portal in India for posting jobs and finding qualified candidates', + 'status' => 'active' + ], + [ + 'name' => 'Indeed', + 'description' => 'Global job search engine and recruitment platform for candidate sourcing', + 'status' => 'active' + ], + [ + 'name' => 'Employee Referral', + 'description' => 'Candidates referred by existing employees through internal referral program', + 'status' => 'active' + ], + [ + 'name' => 'Recruitment Agency', + 'description' => 'External recruitment agencies and headhunters providing candidate sourcing services', + 'status' => 'active' + ], + [ + 'name' => 'Campus Recruitment', + 'description' => 'Fresh graduates recruited directly from colleges and universities through campus drives', + 'status' => 'active' + ], + [ + 'name' => 'Walk-in Interview', + 'description' => 'Candidates who attended walk-in interviews and recruitment events organized by company', + 'status' => 'active' + ], + [ + 'name' => 'Social Media', + 'description' => 'Candidates sourced through social media platforms like Facebook, Twitter, and Instagram', + 'status' => 'active' + ], + [ + 'name' => 'Job Fair', + 'description' => 'Candidates met and recruited through job fairs and career expo events', + 'status' => 'active' + ], + [ + 'name' => 'Direct Application', + 'description' => 'Unsolicited applications received directly from candidates via email or post', + 'status' => 'active' + ], + [ + 'name' => 'Professional Network', + 'description' => 'Candidates sourced through professional contacts, industry connections, and networking events', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($candidateSources as $sourceData) { + // Check if candidate source already exists for this company + if (CandidateSource::where('name', $sourceData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + CandidateSource::create([ + 'name' => $sourceData['name'], + 'description' => $sourceData['description'], + 'status' => $sourceData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create candidate source: ' . $sourceData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('CandidateSource seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/ChecklistItemSeeder.php b/database/seeders/ChecklistItemSeeder.php new file mode 100644 index 000000000..1263bf177 --- /dev/null +++ b/database/seeders/ChecklistItemSeeder.php @@ -0,0 +1,129 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed checklist items by checklist type + $checklistItems = [ + 'Standard Employee Onboarding' => [ + ['task_name' => 'Complete Employment Contract', 'description' => 'Review and sign employment contract and related documents', 'category' => 'Documentation', 'assigned_to_role' => 'HR', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'Submit Personal Documents', 'description' => 'Provide ID proof, address proof, and educational certificates', 'category' => 'Documentation', 'assigned_to_role' => 'HR', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'IT Equipment Setup', 'description' => 'Receive laptop, phone, and other necessary IT equipment', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'Create System Accounts', 'description' => 'Setup email, system access, and security credentials', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 2, 'is_required' => true], + ['task_name' => 'Company Orientation Session', 'description' => 'Attend company overview and culture orientation', 'category' => 'Training', 'assigned_to_role' => 'HR', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Office Tour and Introductions', 'description' => 'Tour office facilities and meet team members', 'category' => 'Facilities', 'assigned_to_role' => 'Manager', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Benefits Enrollment', 'description' => 'Complete health insurance and benefits registration', 'category' => 'HR', 'assigned_to_role' => 'HR', 'due_day' => 5, 'is_required' => true], + ['task_name' => 'Safety Training', 'description' => 'Complete workplace safety and emergency procedures training', 'category' => 'Training', 'assigned_to_role' => 'HR', 'due_day' => 7, 'is_required' => true] + ], + 'Technical Team Onboarding' => [ + ['task_name' => 'Development Environment Setup', 'description' => 'Install and configure development tools and IDEs', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'Code Repository Access', 'description' => 'Grant access to version control systems and repositories', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 2, 'is_required' => true], + ['task_name' => 'Technical Documentation Review', 'description' => 'Review system architecture and technical documentation', 'category' => 'Training', 'assigned_to_role' => 'Manager', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Code Standards Training', 'description' => 'Learn coding standards and development practices', 'category' => 'Training', 'assigned_to_role' => 'Manager', 'due_day' => 5, 'is_required' => true], + ['task_name' => 'Project Assignment', 'description' => 'Assign initial project and define responsibilities', 'category' => 'Other', 'assigned_to_role' => 'Manager', 'due_day' => 7, 'is_required' => true], + ['task_name' => 'Security Clearance', 'description' => 'Complete security training and access permissions', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 3, 'is_required' => true] + ], + 'Management Level Onboarding' => [ + ['task_name' => 'Executive Briefing', 'description' => 'Strategic overview and company direction briefing', 'category' => 'Training', 'assigned_to_role' => 'CEO', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'Leadership Team Introductions', 'description' => 'Meet with senior leadership and key stakeholders', 'category' => 'Other', 'assigned_to_role' => 'CEO', 'due_day' => 2, 'is_required' => true], + ['task_name' => 'Department Overview', 'description' => 'Review department structure, goals, and challenges', 'category' => 'Training', 'assigned_to_role' => 'HR', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Budget and Financial Review', 'description' => 'Review departmental budget and financial responsibilities', 'category' => 'Training', 'assigned_to_role' => 'Finance', 'due_day' => 5, 'is_required' => true], + ['task_name' => 'Management Tools Access', 'description' => 'Setup access to management dashboards and reporting tools', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 3, 'is_required' => true] + ], + 'Sales Team Onboarding' => [ + ['task_name' => 'CRM System Training', 'description' => 'Complete training on customer relationship management system', 'category' => 'Training', 'assigned_to_role' => 'Sales Manager', 'due_day' => 2, 'is_required' => true], + ['task_name' => 'Product Knowledge Training', 'description' => 'Learn about products, services, and value propositions', 'category' => 'Training', 'assigned_to_role' => 'Sales Manager', 'due_day' => 5, 'is_required' => true], + ['task_name' => 'Sales Process Training', 'description' => 'Understand sales methodology and processes', 'category' => 'Training', 'assigned_to_role' => 'Sales Manager', 'due_day' => 7, 'is_required' => true], + ['task_name' => 'Territory Assignment', 'description' => 'Assign sales territory and customer accounts', 'category' => 'Other', 'assigned_to_role' => 'Sales Manager', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Sales Tools Setup', 'description' => 'Configure sales tools, presentations, and materials', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 2, 'is_required' => true] + ], + 'Remote Employee Onboarding' => [ + ['task_name' => 'Equipment Shipping', 'description' => 'Ship laptop, monitor, and other equipment to home address', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'Remote Setup Assistance', 'description' => 'Provide technical support for home office setup', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 2, 'is_required' => true], + ['task_name' => 'Virtual Team Introductions', 'description' => 'Schedule video calls with team members and stakeholders', 'category' => 'Other', 'assigned_to_role' => 'Manager', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Remote Work Policy Training', 'description' => 'Review remote work policies and expectations', 'category' => 'Training', 'assigned_to_role' => 'HR', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Communication Tools Training', 'description' => 'Training on video conferencing and collaboration tools', 'category' => 'Training', 'assigned_to_role' => 'IT', 'due_day' => 2, 'is_required' => true] + ], + 'Intern Onboarding Program' => [ + ['task_name' => 'Internship Agreement', 'description' => 'Complete internship agreement and program expectations', 'category' => 'Documentation', 'assigned_to_role' => 'HR', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'Mentor Assignment', 'description' => 'Assign mentor and schedule regular check-ins', 'category' => 'Other', 'assigned_to_role' => 'Manager', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'Project Overview', 'description' => 'Present internship project goals and deliverables', 'category' => 'Training', 'assigned_to_role' => 'Manager', 'due_day' => 2, 'is_required' => true], + ['task_name' => 'Basic System Access', 'description' => 'Provide limited system access for internship duties', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 1, 'is_required' => true] + ], + 'Customer Service Onboarding' => [ + ['task_name' => 'Customer Service System Training', 'description' => 'Training on helpdesk and ticketing systems', 'category' => 'Training', 'assigned_to_role' => 'CS Manager', 'due_day' => 2, 'is_required' => true], + ['task_name' => 'Product Knowledge Training', 'description' => 'Comprehensive training on products and services', 'category' => 'Training', 'assigned_to_role' => 'CS Manager', 'due_day' => 5, 'is_required' => true], + ['task_name' => 'Customer Interaction Protocols', 'description' => 'Learn customer communication standards and escalation procedures', 'category' => 'Training', 'assigned_to_role' => 'CS Manager', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Quality Assurance Training', 'description' => 'Understand quality metrics and performance standards', 'category' => 'Training', 'assigned_to_role' => 'CS Manager', 'due_day' => 7, 'is_required' => true] + ], + 'Finance Department Onboarding' => [ + ['task_name' => 'Financial Systems Access', 'description' => 'Setup access to accounting and financial management systems', 'category' => 'IT Setup', 'assigned_to_role' => 'IT', 'due_day' => 1, 'is_required' => true], + ['task_name' => 'Compliance Training', 'description' => 'Training on financial regulations and compliance requirements', 'category' => 'Training', 'assigned_to_role' => 'Finance Manager', 'due_day' => 3, 'is_required' => true], + ['task_name' => 'Accounting Procedures Training', 'description' => 'Learn company accounting processes and procedures', 'category' => 'Training', 'assigned_to_role' => 'Finance Manager', 'due_day' => 5, 'is_required' => true], + ['task_name' => 'Financial Reporting Training', 'description' => 'Training on financial reporting requirements and schedules', 'category' => 'Training', 'assigned_to_role' => 'Finance Manager', 'due_day' => 7, 'is_required' => true] + ] + ]; + + foreach ($companies as $company) { + // Get onboarding checklists for this company + $onboardingChecklists = OnboardingChecklist::where('created_by', $company->id)->get(); + + if ($onboardingChecklists->isEmpty()) { + $this->command->warn('No onboarding checklists found for company: ' . $company->name . '. Please run OnboardingChecklistSeeder first.'); + continue; + } + + foreach ($onboardingChecklists as $checklist) { + $items = $checklistItems[$checklist->name] ?? []; + + foreach ($items as $itemData) { + // Check if checklist item already exists + if (ChecklistItem::where('task_name', $itemData['task_name']) + ->where('checklist_id', $checklist->id) + ->exists() + ) { + continue; + } + + try { + ChecklistItem::create([ + 'checklist_id' => $checklist->id, + 'task_name' => $itemData['task_name'], + 'description' => $itemData['description'], + 'category' => $itemData['category'], + 'assigned_to_role' => $itemData['assigned_to_role'], + 'due_day' => $itemData['due_day'], + 'is_required' => $itemData['is_required'], + 'status' => 'active', + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create checklist item: ' . $itemData['task_name'] . ' for checklist: ' . $checklist->name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('ChecklistItem seeder completed successfully!'); + } +} diff --git a/database/seeders/ComplaintSeeder.php b/database/seeders/ComplaintSeeder.php new file mode 100644 index 000000000..cf51beed2 --- /dev/null +++ b/database/seeders/ComplaintSeeder.php @@ -0,0 +1,173 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Complaint types and their subjects/descriptions + $complaintData = [ + 'Harassment' => [ + 'subjects' => ['Verbal Harassment', 'Sexual Harassment', 'Bullying Behavior', 'Intimidation'], + 'descriptions' => [ + 'Verbal Harassment' => 'Employee has been subjected to inappropriate verbal comments, offensive language, and unprofessional behavior that creates a hostile work environment.', + 'Sexual Harassment' => 'Employee experienced unwelcome sexual advances, inappropriate comments, or conduct of a sexual nature that affects work performance and comfort.', + 'Bullying Behavior' => 'Employee is being bullied through aggressive behavior, public humiliation, and persistent negative treatment by colleagues or supervisors.', + 'Intimidation' => 'Employee feels intimidated and threatened by aggressive behavior, verbal abuse, and hostile actions from other team members.' + ] + ], + 'Discrimination' => [ + 'subjects' => ['Age Discrimination', 'Gender Discrimination', 'Racial Discrimination', 'Religious Discrimination'], + 'descriptions' => [ + 'Age Discrimination' => 'Employee believes they are being treated unfairly due to their age, affecting career opportunities and workplace treatment.', + 'Gender Discrimination' => 'Employee experiences unequal treatment, opportunities, or compensation based on gender identity or expression.', + 'Racial Discrimination' => 'Employee faces discriminatory treatment, comments, or actions based on race, ethnicity, or cultural background.', + 'Religious Discrimination' => 'Employee encounters discrimination related to religious beliefs, practices, or observances in the workplace.' + ] + ], + 'Workplace Conditions' => [ + 'subjects' => ['Unsafe Working Conditions', 'Poor Hygiene Standards', 'Inadequate Equipment', 'Excessive Workload'], + 'descriptions' => [ + 'Unsafe Working Conditions' => 'Workplace poses safety risks due to inadequate safety measures, faulty equipment, or hazardous environmental conditions.', + 'Poor Hygiene Standards' => 'Workplace maintains poor cleanliness and hygiene standards that affect employee health and comfort.', + 'Inadequate Equipment' => 'Employee lacks proper tools, equipment, or resources necessary to perform job duties effectively and safely.', + 'Excessive Workload' => 'Employee is consistently assigned unreasonable workload that affects work-life balance and job performance.' + ] + ], + 'Policy Violation' => [ + 'subjects' => ['Attendance Policy Violation', 'Code of Conduct Breach', 'Conflict of Interest', 'Misuse of Resources'], + 'descriptions' => [ + 'Attendance Policy Violation' => 'Colleague consistently violates attendance policies without consequences, creating unfair work distribution among team members.', + 'Code of Conduct Breach' => 'Employee witnessed or experienced behavior that violates company code of conduct and professional standards.', + 'Conflict of Interest' => 'Supervisor or colleague has undisclosed conflicts of interest that affect decision-making and fairness in the workplace.', + 'Misuse of Resources' => 'Company resources, equipment, or facilities are being misused for personal purposes by employees or management.' + ] + ], + 'Management Issues' => [ + 'subjects' => ['Unfair Treatment', 'Lack of Support', 'Poor Communication', 'Favoritism'], + 'descriptions' => [ + 'Unfair Treatment' => 'Employee receives unfair treatment from management regarding assignments, evaluations, or opportunities compared to colleagues.', + 'Lack of Support' => 'Management fails to provide necessary support, guidance, or resources needed for employee success and development.', + 'Poor Communication' => 'Management demonstrates poor communication practices, leading to confusion, misunderstandings, and workplace inefficiency.', + 'Favoritism' => 'Management shows clear favoritism towards certain employees, affecting fair treatment and equal opportunities for all staff.' + ] + ] + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get HR personnel for assignment + $hrPersonnel = User::where('type', 'hr') + ->where('created_by', $company->id) + ->get(); + + // Create 4-8 complaints for this company + $complaintCount = rand(4, 8); + + for ($i = 0; $i < $complaintCount; $i++) { + $complainant = $employees->random(); + $complaintType = $faker->randomElement(array_keys($complaintData)); + $typeData = $complaintData[$complaintType]; + $subject = $faker->randomElement($typeData['subjects']); + $description = $typeData['descriptions'][$subject]; + + // 70% chance of complaint against another employee + $otherEmployees = $employees->where('id', '!=', $complainant->id); + $againstEmployee = $faker->boolean(70) && $otherEmployees->isNotEmpty() ? $otherEmployees->random() : null; + + $complaintDate = $faker->dateTimeBetween('-1 year', 'now'); + $status = $faker->randomElement(['submitted', 'under investigation', 'resolved', 'dismissed']); + $isAnonymous = $faker->boolean(20); // 20% anonymous complaints + + $assignedTo = $hrPersonnel->isNotEmpty() ? $hrPersonnel->random() : null; + $resolutionDeadline = $status !== 'resolved' && $status !== 'dismissed' ? + $faker->dateTimeBetween('now', '+30 days') : null; + + // Resolution data for resolved/dismissed complaints + $resolutionDate = in_array($status, ['resolved', 'dismissed']) ? + $faker->dateTimeBetween($complaintDate, 'now') : null; + + $resolutionAction = $resolutionDate ? $this->getResolutionAction($status, $complaintType) : null; + + $followUpDate = $resolutionDate && $faker->boolean(60) ? + $faker->dateTimeBetween($resolutionDate, '+30 days') : null; + + try { + Complaint::create([ + 'employee_id' => $complainant->id, + 'against_employee_id' => $againstEmployee?->id, + 'complaint_type' => $complaintType, + 'subject' => $subject, + 'complaint_date' => $complaintDate->format('Y-m-d'), + 'description' => $description, + 'status' => $status, + 'documents' => randomImage(), + 'is_anonymous' => $isAnonymous, + 'assigned_to' => $assignedTo?->id, + 'resolution_deadline' => $resolutionDeadline?->format('Y-m-d'), + 'investigation_notes' => $status !== 'submitted' ? $faker->paragraph(2) : null, + 'resolution_action' => $resolutionAction, + 'resolution_date' => $resolutionDate?->format('Y-m-d'), + 'follow_up_action' => $followUpDate ? $faker->sentence(8) : null, + 'follow_up_date' => $followUpDate?->format('Y-m-d'), + 'feedback' => $resolutionDate && $faker->boolean(40) ? $faker->sentence(10) : null, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create complaint for employee: ' . $complainant->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Complaint seeder completed successfully!'); + } + + /** + * Get resolution action based on status and complaint type + */ + private function getResolutionAction($status, $complaintType) + { + if ($status === 'resolved') { + $resolutionActions = [ + 'Harassment' => 'Conducted thorough investigation, provided counseling to involved parties, and implemented additional training on workplace behavior.', + 'Discrimination' => 'Investigated the matter, found evidence of discriminatory behavior, provided sensitivity training, and updated company policies.', + 'Workplace Conditions' => 'Assessed workplace conditions, implemented safety improvements, and provided necessary equipment and resources.', + 'Policy Violation' => 'Reviewed policy violations, took appropriate disciplinary action, and reinforced policy awareness through training.', + 'Management Issues' => 'Addressed management concerns through coaching, improved communication protocols, and established regular feedback mechanisms.' + ]; + + return $resolutionActions[$complaintType] ?? 'Matter investigated and resolved through appropriate corrective measures and policy implementation.'; + } else { + return 'After thorough investigation, the complaint was found to be unsubstantiated and dismissed with proper documentation.'; + } + } +} diff --git a/database/seeders/ContactSeeder.php b/database/seeders/ContactSeeder.php new file mode 100644 index 000000000..6821a540f --- /dev/null +++ b/database/seeders/ContactSeeder.php @@ -0,0 +1,130 @@ +first(); + + if (!$superAdmin) { + $this->command->warn('No super admin user found. Please run UserSeeder first.'); + return; + } + + $this->createContactsForUser($superAdmin); + } else { + // For non-SaaS mode - create contacts for companies + $companies = User::where('type', 'company')->get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + foreach ($companies as $company) { + $this->createContactsForUser($company); + } + } + + $this->command->info('ContactSeeder completed successfully!'); + } + + private function createContactsForUser($user) + { + // Sample contact data + $contacts = [ + [ + 'name' => 'John Smith', + 'email' => 'john.smith@example.com', + 'subject' => 'Inquiry about HR Management System', + 'message' => 'Hello, I am interested in learning more about your HR management system. Could you please provide more details about the features and pricing?', + 'status' => 'New' + ], + [ + 'name' => 'Sarah Johnson', + 'email' => 'sarah.johnson@company.com', + 'subject' => 'Demo Request', + 'message' => 'Hi, we are looking for an HR solution for our growing company. Would it be possible to schedule a demo to see the system in action?', + 'status' => 'Contacted' + ], + [ + 'name' => 'Michael Brown', + 'email' => 'michael.brown@business.org', + 'subject' => 'Integration Questions', + 'message' => 'We currently use several HR tools and would like to know about integration capabilities with your system. Can you help us understand the options?', + 'status' => 'Qualified' + ], + [ + 'name' => 'Emily Davis', + 'email' => 'emily.davis@startup.io', + 'subject' => 'Pricing Information', + 'message' => 'Could you please send us detailed pricing information for your HR management system? We are a startup with about 25 employees.', + 'status' => 'New' + ], + [ + 'name' => 'David Wilson', + 'email' => 'david.wilson@enterprise.com', + 'subject' => 'Enterprise Solution Inquiry', + 'message' => 'We are an enterprise with 500+ employees looking for a comprehensive HR solution. What enterprise features do you offer?', + 'status' => 'Contacted' + ], + [ + 'name' => 'Lisa Anderson', + 'email' => 'lisa.anderson@tech.com', + 'subject' => 'Support Question', + 'message' => 'What kind of customer support do you provide? We need 24/7 support for our global operations.', + 'status' => 'Closed' + ], + [ + 'name' => 'Robert Taylor', + 'email' => 'robert.taylor@consulting.net', + 'subject' => 'Partnership Opportunity', + 'message' => 'We are a consulting firm and would like to explore partnership opportunities. Could we schedule a call to discuss this?', + 'status' => 'Converted' + ], + [ + 'name' => 'Jennifer Martinez', + 'email' => 'jennifer.martinez@nonprofit.org', + 'subject' => 'Non-profit Discount', + 'message' => 'Do you offer any special pricing for non-profit organizations? We are interested in your HR management solution.', + 'status' => 'New' + ] + ]; + + // Create all contacts for each user + foreach ($contacts as $contact) { + + try { + Contact::updateOrCreate( + [ + 'email' => $contact['email'], + 'created_by' => $user->id + ], + [ + 'name' => $contact['name'], + 'subject' => $contact['subject'], + 'message' => $contact['message'], + 'status' => $contact['status'], + ] + ); + } catch (\Exception $e) { + $this->command->error('Failed to create/update contact: ' . $contact['name'] . ' for user: ' . $user->name); + continue; + } + } + } +} \ No newline at end of file diff --git a/database/seeders/ContractRenewalSeeder.php b/database/seeders/ContractRenewalSeeder.php new file mode 100644 index 000000000..ae6ab8be5 --- /dev/null +++ b/database/seeders/ContractRenewalSeeder.php @@ -0,0 +1,116 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed renewal data for consistent results + $renewalData = [ + ['salary_increase' => 10000, 'allowances' => 5000, 'benefits' => 'Health Insurance Premium', 'status' => 'Approved', 'reason' => 'Annual performance review and salary increment', 'changes' => 'Salary increased by 10%, additional health benefits added'], + ['salary_increase' => 8000, 'allowances' => 3000, 'benefits' => 'Performance Bonus', 'status' => 'Processed', 'reason' => 'Contract renewal with performance-based increment', 'changes' => 'Salary adjustment based on market standards'], + ['salary_increase' => 5000, 'allowances' => 2000, 'benefits' => 'Training Allowance', 'status' => 'Pending', 'reason' => 'Standard contract renewal request', 'changes' => 'Minor salary adjustment and training benefits'], + ['salary_increase' => 12000, 'allowances' => 6000, 'benefits' => 'Executive Benefits', 'status' => 'Approved', 'reason' => 'Promotion and role enhancement', 'changes' => 'Significant salary increase due to promotion'], + ['salary_increase' => 0, 'allowances' => 1000, 'benefits' => 'Basic Benefits', 'status' => 'Rejected', 'reason' => 'Contract renewal without salary change', 'changes' => 'No salary change, only allowance adjustment'], + ['salary_increase' => 15000, 'allowances' => 7000, 'benefits' => 'Comprehensive Package', 'status' => 'Approved', 'reason' => 'Retention package for key employee', 'changes' => 'Comprehensive benefits upgrade and salary increase'], + ['salary_increase' => 3000, 'allowances' => 1500, 'benefits' => 'Standard Benefits', 'status' => 'Pending', 'reason' => 'Regular contract renewal', 'changes' => 'Standard increment as per company policy'], + ['salary_increase' => 7000, 'allowances' => 3500, 'benefits' => 'Enhanced Benefits', 'status' => 'Processed', 'reason' => 'Contract extension with benefits upgrade', 'changes' => 'Benefits package enhanced with additional perks'] + ]; + + foreach ($companies as $company) { + // Get employee contracts for this company that can be renewed + $contracts = EmployeeContract::where('created_by', $company->id) + ->whereIn('status', ['Active', 'Expired']) + ->whereNotNull('end_date') + ->get(); + + if ($contracts->isEmpty()) { + $this->command->warn('No renewable contracts found for company: ' . $company->name . '. Please run EmployeeContractSeeder first.'); + continue; + } + + // Get managers/HR for approval + $approvers = User::whereIn('type', ['manager', 'hr']) + ->where('created_by', $company->id) + ->get(); + + $renewalCounter = ($company->id - 1) * 100 + 1; + + // Create renewals for first 5 contracts + $selectedContracts = $contracts->take(7); + + foreach ($selectedContracts as $index => $contract) { + // Check if renewal already exists for this contract + if (ContractRenewal::where('contract_id', $contract->id)->where('created_by', $company->id)->exists()) { + continue; + } + + $renewal = $renewalData[$index % 8]; + $renewalNumber = 'REN-' . $currentYear . '-' . str_pad($renewalCounter, 4, '0', STR_PAD_LEFT); + + // Select approver and requester + $approver = $approvers->isNotEmpty() ? $approvers->first() : null; + $requester = $approvers->isNotEmpty() ? $approvers->first() : $contract->employee; + + $currentEndDate = $contract->end_date; + $newStartDate = date('Y-m-d', strtotime($currentEndDate . ' +1 day')); + $newEndDate = date('Y-m-d', strtotime($newStartDate . ' +12 months')); + + $newBasicSalary = $contract->basic_salary + $renewal['salary_increase']; + $newAllowances = json_encode(['total' => ($renewal['allowances'])]); + $newBenefits = json_encode([$renewal['benefits']]); + + $newTermsConditions = 'Renewed contract terms and conditions. Previous contract extended with updated compensation and benefits package.'; + + try { + ContractRenewal::create([ + 'contract_id' => $contract->id, + 'renewal_number' => $renewalNumber, + 'current_end_date' => $currentEndDate, + 'new_start_date' => $newStartDate, + 'new_end_date' => $newEndDate, + 'new_basic_salary' => $newBasicSalary, + 'new_allowances' => $newAllowances, + 'new_benefits' => $newBenefits, + 'new_terms_conditions' => $newTermsConditions, + 'changes_summary' => $renewal['changes'], + 'status' => $renewal['status'], + 'reason' => $renewal['reason'], + 'requested_by' => $requester->id, + 'approved_by' => in_array($renewal['status'], ['Approved', 'Processed']) ? $approver?->id : null, + 'approved_at' => in_array($renewal['status'], ['Approved', 'Processed']) ? now() : null, + 'approval_notes' => in_array($renewal['status'], ['Approved', 'Processed']) ? 'Renewal approved based on performance and company policy' : null, + 'created_by' => $company->id, + ]); + + $renewalCounter++; + } catch (\Exception $e) { + $this->command->error('Failed to create contract renewal for contract: ' . $contract->contract_number . ' in company: ' . $company->name); + $renewalCounter++; + continue; + } + } + } + + $this->command->info('ContractRenewal seeder completed successfully!'); + } +} diff --git a/database/seeders/ContractTemplateSeeder.php b/database/seeders/ContractTemplateSeeder.php new file mode 100644 index 000000000..5258ff648 --- /dev/null +++ b/database/seeders/ContractTemplateSeeder.php @@ -0,0 +1,276 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed contract templates for consistent data + $contractTemplates = [ + [ + 'name' => 'Standard Permanent Employment Contract', + 'description' => 'Standard template for permanent full-time employees with comprehensive terms and conditions', + 'contract_type' => 'Permanent Full-time', + 'template_content' => 'EMPLOYMENT AGREEMENT + +This Employment Agreement is entered into between {{company_name}} and {{employee_name}}. + +POSITION AND DUTIES: +Employee is hired as {{job_title}} in the {{department}} department, reporting to {{manager_name}}. + +COMPENSATION: +- Basic Salary: {{basic_salary}} per annum +- Allowances: {{allowances}} +- Benefits: {{benefits}} + +EMPLOYMENT TERMS: +- Start Date: {{start_date}} +- Probation Period: {{probation_period}} months +- Notice Period: {{notice_period}} days +- Working Hours: {{working_hours}} + +CONFIDENTIALITY: +Employee agrees to maintain confidentiality of company information and trade secrets. + +TERMINATION: +Either party may terminate this agreement with {{notice_period}} days written notice. + +This agreement is governed by the laws of {{jurisdiction}}. + +Signed: +{{employee_name}} - Employee +{{hr_manager}} - HR Manager +Date: {{contract_date}}', + 'variables' => ['company_name', 'employee_name', 'job_title', 'department', 'manager_name', 'basic_salary', 'allowances', 'benefits', 'start_date', 'probation_period', 'notice_period', 'working_hours', 'jurisdiction', 'hr_manager', 'contract_date'], + 'clauses' => ['Confidentiality', 'Non-Compete', 'Termination', 'Benefits', 'Working Hours'], + 'is_default' => true, + 'status' => 'active' + ], + [ + 'name' => 'Fixed-Term Contract Template', + 'description' => 'Template for fixed-term contracts with specific start and end dates', + 'contract_type' => 'Fixed-term Contract', + 'template_content' => 'FIXED-TERM EMPLOYMENT CONTRACT + +This Fixed-Term Contract is between {{company_name}} and {{employee_name}}. + +CONTRACT PERIOD: +- Start Date: {{start_date}} +- End Date: {{end_date}} +- Duration: {{contract_duration}} months + +POSITION: +Employee is engaged as {{job_title}} for the specified contract period. + +COMPENSATION: +- Monthly Salary: {{monthly_salary}} +- Total Contract Value: {{total_value}} +- Payment Terms: {{payment_terms}} + +RENEWAL: +This contract may be renewed by mutual agreement before expiration. + +EARLY TERMINATION: +Contract may be terminated early with {{notice_period}} days notice or payment in lieu. + +DELIVERABLES: +{{project_deliverables}} + +Signed: +{{employee_name}} - Contractor +{{project_manager}} - Project Manager +Date: {{contract_date}}', + 'variables' => ['company_name', 'employee_name', 'start_date', 'end_date', 'contract_duration', 'job_title', 'monthly_salary', 'total_value', 'payment_terms', 'notice_period', 'project_deliverables', 'project_manager', 'contract_date'], + 'clauses' => ['Contract Period', 'Renewal Terms', 'Early Termination', 'Deliverables'], + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Part-Time Employment Contract', + 'description' => 'Contract template for part-time employees with flexible working arrangements', + 'contract_type' => 'Part-time Contract', + 'template_content' => 'PART-TIME EMPLOYMENT AGREEMENT + +Agreement between {{company_name}} and {{employee_name}} for part-time employment. + +WORKING ARRANGEMENT: +- Position: {{job_title}} +- Working Hours: {{weekly_hours}} hours per week +- Schedule: {{work_schedule}} +- Flexible Hours: {{flexible_arrangement}} + +COMPENSATION: +- Hourly Rate: {{hourly_rate}} +- Monthly Salary: {{monthly_salary}} +- Pro-rated Benefits: {{prorated_benefits}} + +BENEFITS: +- Health Insurance: {{health_coverage}} +- Paid Leave: {{leave_entitlement}} days annually +- Professional Development: {{training_budget}} + +PERFORMANCE: +Performance will be evaluated based on {{performance_metrics}}. + +TERMINATION: +Either party may terminate with {{notice_period}} days notice. + +Signed: +{{employee_name}} - Employee +{{hr_representative}} - HR Representative +Date: {{contract_date}}', + 'variables' => ['company_name', 'employee_name', 'job_title', 'weekly_hours', 'work_schedule', 'flexible_arrangement', 'hourly_rate', 'monthly_salary', 'prorated_benefits', 'health_coverage', 'leave_entitlement', 'training_budget', 'performance_metrics', 'notice_period', 'hr_representative', 'contract_date'], + 'clauses' => ['Working Hours', 'Flexible Arrangement', 'Pro-rated Benefits', 'Performance Metrics'], + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Consultant Agreement Template', + 'description' => 'Independent contractor agreement for consulting services', + 'contract_type' => 'Consultant Agreement', + 'template_content' => 'INDEPENDENT CONTRACTOR AGREEMENT + +This Agreement is between {{company_name}} and {{contractor_name}}. + +SERVICES: +Contractor will provide {{service_description}} as an independent contractor. + +SCOPE OF WORK: +{{scope_of_work}} + +COMPENSATION: +- Contract Rate: {{contract_rate}} +- Payment Schedule: {{payment_schedule}} +- Total Contract Value: {{total_value}} +- Expenses: {{expense_policy}} + +DELIVERABLES: +{{deliverables_list}} + +TIMELINE: +- Start Date: {{start_date}} +- Completion Date: {{end_date}} +- Milestones: {{project_milestones}} + +INTELLECTUAL PROPERTY: +All work products belong to {{company_name}}. + +CONFIDENTIALITY: +Contractor agrees to maintain strict confidentiality. + +TERMINATION: +Either party may terminate with {{notice_period}} days notice. + +Signed: +{{contractor_name}} - Contractor +{{project_manager}} - Project Manager +Date: {{contract_date}}', + 'variables' => ['company_name', 'contractor_name', 'service_description', 'scope_of_work', 'contract_rate', 'payment_schedule', 'total_value', 'expense_policy', 'deliverables_list', 'start_date', 'end_date', 'project_milestones', 'notice_period', 'project_manager', 'contract_date'], + 'clauses' => ['Scope of Work', 'Payment Terms', 'Intellectual Property', 'Confidentiality', 'Termination'], + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Internship Agreement Template', + 'description' => 'Agreement template for internship programs and student placements', + 'contract_type' => 'Internship Contract', + 'template_content' => 'INTERNSHIP AGREEMENT + +Agreement between {{company_name}} and {{intern_name}} for internship program. + +INTERNSHIP DETAILS: +- Position: {{internship_title}} +- Department: {{department}} +- Duration: {{internship_duration}} +- Start Date: {{start_date}} +- End Date: {{end_date}} + +SUPERVISION: +- Supervisor: {{supervisor_name}} +- Mentor: {{mentor_name}} + +LEARNING OBJECTIVES: +{{learning_objectives}} + +COMPENSATION: +- Monthly Stipend: {{stipend_amount}} +- Benefits: {{intern_benefits}} + +EVALUATION: +Performance will be evaluated {{evaluation_frequency}} based on {{evaluation_criteria}}. + +CONFIDENTIALITY: +Intern agrees to maintain confidentiality of company information. + +CERTIFICATE: +Upon successful completion, intern will receive {{certificate_type}}. + +Signed: +{{intern_name}} - Intern +{{supervisor_name}} - Supervisor +Date: {{contract_date}}', + 'variables' => ['company_name', 'intern_name', 'internship_title', 'department', 'internship_duration', 'start_date', 'end_date', 'supervisor_name', 'mentor_name', 'learning_objectives', 'stipend_amount', 'intern_benefits', 'evaluation_frequency', 'evaluation_criteria', 'certificate_type', 'contract_date'], + 'clauses' => ['Learning Objectives', 'Supervision', 'Evaluation', 'Confidentiality', 'Certificate'], + 'is_default' => false, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + // Get contract types for this company + $contractTypes = ContractType::where('created_by', $company->id)->get(); + + if ($contractTypes->isEmpty()) { + $this->command->warn('No contract types found for company: ' . $company->name . '. Please run ContractTypeSeeder first.'); + continue; + } + + foreach ($contractTemplates as $templateData) { + // Find matching contract type + $contractType = $contractTypes->where('name', $templateData['contract_type'])->first(); + if (!$contractType) $contractType = $contractTypes->first(); + + // Check if template already exists for this company + if (ContractTemplate::where('name', $templateData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + ContractTemplate::create([ + 'name' => $templateData['name'], + 'description' => $templateData['description'], + 'contract_type_id' => $contractType->id, + 'template_content' => $templateData['template_content'], + 'variables' => $templateData['variables'], + 'clauses' => $templateData['clauses'], + 'is_default' => $templateData['is_default'], + 'status' => $templateData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create contract template: ' . $templateData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('ContractTemplate seeder completed successfully!'); + } +} diff --git a/database/seeders/ContractTypeSeeder.php b/database/seeders/ContractTypeSeeder.php new file mode 100644 index 000000000..e5804d445 --- /dev/null +++ b/database/seeders/ContractTypeSeeder.php @@ -0,0 +1,127 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed contract types for consistent data + $contractTypes = [ + [ + 'name' => 'Permanent Full-time', + 'description' => 'Permanent employment contract with full-time working hours and comprehensive benefits package', + 'default_duration_months' => null, + 'probation_period_months' => 6, + 'notice_period_days' => 60, + 'is_renewable' => false, + 'status' => 'active' + ], + [ + 'name' => 'Fixed-term Contract', + 'description' => 'Fixed-term employment contract for specific duration with defined start and end dates', + 'default_duration_months' => 12, + 'probation_period_months' => 3, + 'notice_period_days' => 30, + 'is_renewable' => true, + 'status' => 'active' + ], + [ + 'name' => 'Part-time Contract', + 'description' => 'Part-time employment contract with reduced working hours and pro-rated benefits', + 'default_duration_months' => null, + 'probation_period_months' => 3, + 'notice_period_days' => 30, + 'is_renewable' => false, + 'status' => 'active' + ], + [ + 'name' => 'Temporary Contract', + 'description' => 'Short-term temporary contract for immediate staffing needs or project-based work', + 'default_duration_months' => 6, + 'probation_period_months' => 1, + 'notice_period_days' => 15, + 'is_renewable' => true, + 'status' => 'active' + ], + [ + 'name' => 'Consultant Agreement', + 'description' => 'Independent contractor agreement for specialized consulting services and expertise', + 'default_duration_months' => 6, + 'probation_period_months' => 0, + 'notice_period_days' => 30, + 'is_renewable' => true, + 'status' => 'active' + ], + [ + 'name' => 'Internship Contract', + 'description' => 'Educational internship contract for students and recent graduates with learning objectives', + 'default_duration_months' => 3, + 'probation_period_months' => 0, + 'notice_period_days' => 7, + 'is_renewable' => true, + 'status' => 'active' + ], + [ + 'name' => 'Probationary Contract', + 'description' => 'Initial probationary employment contract with extended evaluation period', + 'default_duration_months' => 6, + 'probation_period_months' => 6, + 'notice_period_days' => 15, + 'is_renewable' => true, + 'status' => 'active' + ], + [ + 'name' => 'Seasonal Contract', + 'description' => 'Seasonal employment contract for specific periods or seasonal business requirements', + 'default_duration_months' => 4, + 'probation_period_months' => 1, + 'notice_period_days' => 15, + 'is_renewable' => true, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($contractTypes as $typeData) { + // Check if contract type already exists for this company + if (ContractType::where('name', $typeData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + ContractType::create([ + 'name' => $typeData['name'], + 'description' => $typeData['description'], + 'default_duration_months' => $typeData['default_duration_months'], + 'probation_period_months' => $typeData['probation_period_months'], + 'notice_period_days' => $typeData['notice_period_days'], + 'is_renewable' => $typeData['is_renewable'], + 'status' => $typeData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create contract type: ' . $typeData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('ContractType seeder completed successfully!'); + } +} diff --git a/database/seeders/CouponSeeder.php b/database/seeders/CouponSeeder.php new file mode 100644 index 000000000..61a6fc100 --- /dev/null +++ b/database/seeders/CouponSeeder.php @@ -0,0 +1,167 @@ +command->warn('No users found. Please run UserSeeder first.'); + return; + } + + $coupons = [ + [ + 'name' => 'Summer Sale', + 'type' => 'percentage', + 'minimum_spend' => 50.00, + 'maximum_spend' => 500.00, + 'discount_amount' => 20.00, + 'use_limit_per_coupon' => 100, + 'use_limit_per_user' => 1, + 'expiry_date' => now()->addMonths(3), + 'code' => 'SUMMER20', + 'code_type' => 'manual', + 'status' => true, + 'created_by' => $user->id, + ], + [ + 'name' => 'New Customer Discount', + 'type' => 'flat', + 'minimum_spend' => 25.00, + 'maximum_spend' => null, + 'discount_amount' => 10.00, + 'use_limit_per_coupon' => null, + 'use_limit_per_user' => 1, + 'expiry_date' => now()->addMonths(6), + 'code' => 'WELCOME10', + 'code_type' => 'manual', + 'status' => true, + 'created_by' => $user->id, + ], + [ + 'name' => 'Flash Sale', + 'type' => 'percentage', + 'minimum_spend' => 100.00, + 'maximum_spend' => 1000.00, + 'discount_amount' => 15.00, + 'use_limit_per_coupon' => 50, + 'use_limit_per_user' => 2, + 'expiry_date' => now()->addWeeks(2), + 'code' => 'FLASH15', + 'code_type' => 'manual', + 'status' => true, + 'created_by' => $user->id, + ], + [ + 'name' => 'Auto Generated Coupon', + 'type' => 'flat', + 'minimum_spend' => null, + 'maximum_spend' => null, + 'discount_amount' => 5.00, + 'use_limit_per_coupon' => 200, + 'use_limit_per_user' => 3, + 'expiry_date' => now()->addMonths(1), + 'code' => 'AUTO5OFF', + 'code_type' => 'auto', + 'status' => false, + 'created_by' => $user->id, + ] + ]; + + // Add more coupons for pagination testing + $additionalCoupons = [ + [ + 'name' => 'Black Friday Deal', + 'type' => 'percentage', + 'minimum_spend' => 200.00, + 'maximum_spend' => 2000.00, + 'discount_amount' => 30.00, + 'use_limit_per_coupon' => 500, + 'use_limit_per_user' => 1, + 'expiry_date' => now()->addMonths(2), + 'code' => 'BLACKFRI30', + 'code_type' => 'manual', + 'status' => true, + 'created_by' => $user->id, + ], + [ + 'name' => 'Holiday Special', + 'type' => 'flat', + 'minimum_spend' => 75.00, + 'maximum_spend' => null, + 'discount_amount' => 25.00, + 'use_limit_per_coupon' => 300, + 'use_limit_per_user' => 2, + 'expiry_date' => now()->addMonths(4), + 'code' => 'HOLIDAY25', + 'code_type' => 'manual', + 'status' => true, + 'created_by' => $user->id, + ], + [ + 'name' => 'Student Discount', + 'type' => 'percentage', + 'minimum_spend' => 30.00, + 'maximum_spend' => 300.00, + 'discount_amount' => 10.00, + 'use_limit_per_coupon' => null, + 'use_limit_per_user' => 5, + 'expiry_date' => now()->addMonths(12), + 'code' => 'STUDENT10', + 'code_type' => 'manual', + 'status' => true, + 'created_by' => $user->id, + ], + [ + 'name' => 'VIP Member Bonus', + 'type' => 'flat', + 'minimum_spend' => 150.00, + 'maximum_spend' => 1500.00, + 'discount_amount' => 50.00, + 'use_limit_per_coupon' => 100, + 'use_limit_per_user' => 1, + 'expiry_date' => now()->addMonths(6), + 'code' => 'VIP50BONUS', + 'code_type' => 'manual', + 'status' => false, + 'created_by' => $user->id, + ], + [ + 'name' => 'Weekend Sale', + 'type' => 'percentage', + 'minimum_spend' => 40.00, + 'maximum_spend' => 400.00, + 'discount_amount' => 12.00, + 'use_limit_per_coupon' => 200, + 'use_limit_per_user' => 3, + 'expiry_date' => now()->addWeeks(8), + 'code' => 'WEEKEND12', + 'code_type' => 'manual', + 'status' => true, + 'created_by' => $user->id, + ] + ]; + + $allCoupons = array_merge($coupons, $additionalCoupons); + + foreach ($allCoupons as $couponData) { + if (!\App\Models\Coupon::where('code', $couponData['code'])->exists()) { + \App\Models\Coupon::create($couponData); + } + } + + $this->command->info('Sample coupons created successfully!'); + } + } +} diff --git a/database/seeders/CurrencySeeder.php b/database/seeders/CurrencySeeder.php new file mode 100644 index 000000000..70ccc3d7b --- /dev/null +++ b/database/seeders/CurrencySeeder.php @@ -0,0 +1,135 @@ + 'US Dollar', 'code' => 'USD', 'symbol' => '$', 'description' => 'United States Dollar', 'is_default' => true], + ['name' => 'Euro', 'code' => 'EUR', 'symbol' => '€', 'description' => 'Euro', 'is_default' => false], + ['name' => 'British Pound', 'code' => 'GBP', 'symbol' => '£', 'description' => 'British Pound Sterling', 'is_default' => false], + ['name' => 'Japanese Yen', 'code' => 'JPY', 'symbol' => '¥', 'description' => 'Japanese Yen', 'is_default' => false], + ['name' => 'Canadian Dollar', 'code' => 'CAD', 'symbol' => 'C$', 'description' => 'Canadian Dollar', 'is_default' => false], + ['name' => 'Australian Dollar', 'code' => 'AUD', 'symbol' => 'A$', 'description' => 'Australian Dollar', 'is_default' => false], + ['name' => 'Swiss Franc', 'code' => 'CHF', 'symbol' => 'CHF', 'description' => 'Swiss Franc', 'is_default' => false], + ['name' => 'Chinese Yuan', 'code' => 'CNY', 'symbol' => '¥', 'description' => 'Chinese Yuan', 'is_default' => false], + ['name' => 'Swedish Krona', 'code' => 'SEK', 'symbol' => 'kr', 'description' => 'Swedish Krona', 'is_default' => false], + ['name' => 'New Zealand Dollar', 'code' => 'NZD', 'symbol' => 'NZ$', 'description' => 'New Zealand Dollar', 'is_default' => false], + ['name' => 'Mexican Peso', 'code' => 'MXN', 'symbol' => '$', 'description' => 'Mexican Peso', 'is_default' => false], + ['name' => 'Singapore Dollar', 'code' => 'SGD', 'symbol' => 'S$', 'description' => 'Singapore Dollar', 'is_default' => false], + ['name' => 'Hong Kong Dollar', 'code' => 'HKD', 'symbol' => 'HK$', 'description' => 'Hong Kong Dollar', 'is_default' => false], + ['name' => 'Norwegian Krone', 'code' => 'NOK', 'symbol' => 'kr', 'description' => 'Norwegian Krone', 'is_default' => false], + ['name' => 'South Korean Won', 'code' => 'KRW', 'symbol' => '₩', 'description' => 'South Korean Won', 'is_default' => false], + ['name' => 'Turkish Lira', 'code' => 'TRY', 'symbol' => '₺', 'description' => 'Turkish Lira', 'is_default' => false], + ['name' => 'Russian Ruble', 'code' => 'RUB', 'symbol' => '₽', 'description' => 'Russian Ruble', 'is_default' => false], + ['name' => 'Indian Rupee', 'code' => 'INR', 'symbol' => '₹', 'description' => 'Indian Rupee', 'is_default' => false], + ['name' => 'Brazilian Real', 'code' => 'BRL', 'symbol' => 'R$', 'description' => 'Brazilian Real', 'is_default' => false], + ['name' => 'South African Rand', 'code' => 'ZAR', 'symbol' => 'R', 'description' => 'South African Rand', 'is_default' => false], + ['name' => 'Polish Zloty', 'code' => 'PLN', 'symbol' => 'zł', 'description' => 'Polish Zloty', 'is_default' => false], + ['name' => 'Israeli Shekel', 'code' => 'ILS', 'symbol' => '₪', 'description' => 'Israeli Shekel', 'is_default' => false], + ['name' => 'Danish Krone', 'code' => 'DKK', 'symbol' => 'kr', 'description' => 'Danish Krone', 'is_default' => false], + ['name' => 'Czech Koruna', 'code' => 'CZK', 'symbol' => 'Kč', 'description' => 'Czech Koruna', 'is_default' => false], + ['name' => 'Hungarian Forint', 'code' => 'HUF', 'symbol' => 'Ft', 'description' => 'Hungarian Forint', 'is_default' => false], + ['name' => 'Romanian Leu', 'code' => 'RON', 'symbol' => 'lei', 'description' => 'Romanian Leu', 'is_default' => false], + ['name' => 'Croatian Kuna', 'code' => 'HRK', 'symbol' => 'kn', 'description' => 'Croatian Kuna', 'is_default' => false], + ['name' => 'Bulgarian Lev', 'code' => 'BGN', 'symbol' => 'лв', 'description' => 'Bulgarian Lev', 'is_default' => false], + ['name' => 'Thai Baht', 'code' => 'THB', 'symbol' => '฿', 'description' => 'Thai Baht', 'is_default' => false], + ['name' => 'Malaysian Ringgit', 'code' => 'MYR', 'symbol' => 'RM', 'description' => 'Malaysian Ringgit', 'is_default' => false], + ['name' => 'Indonesian Rupiah', 'code' => 'IDR', 'symbol' => 'Rp', 'description' => 'Indonesian Rupiah', 'is_default' => false], + ['name' => 'Philippine Peso', 'code' => 'PHP', 'symbol' => '₱', 'description' => 'Philippine Peso', 'is_default' => false], + ['name' => 'Vietnamese Dong', 'code' => 'VND', 'symbol' => '₫', 'description' => 'Vietnamese Dong', 'is_default' => false], + ['name' => 'Argentine Peso', 'code' => 'ARS', 'symbol' => '$', 'description' => 'Argentine Peso', 'is_default' => false], + ['name' => 'Chilean Peso', 'code' => 'CLP', 'symbol' => '$', 'description' => 'Chilean Peso', 'is_default' => false], + ['name' => 'Colombian Peso', 'code' => 'COP', 'symbol' => '$', 'description' => 'Colombian Peso', 'is_default' => false], + ['name' => 'Peruvian Sol', 'code' => 'PEN', 'symbol' => 'S/', 'description' => 'Peruvian Sol', 'is_default' => false], + ['name' => 'Uruguayan Peso', 'code' => 'UYU', 'symbol' => '$U', 'description' => 'Uruguayan Peso', 'is_default' => false], + ['name' => 'Egyptian Pound', 'code' => 'EGP', 'symbol' => '£', 'description' => 'Egyptian Pound', 'is_default' => false], + ['name' => 'Nigerian Naira', 'code' => 'NGN', 'symbol' => '₦', 'description' => 'Nigerian Naira', 'is_default' => false], + ['name' => 'Kenyan Shilling', 'code' => 'KES', 'symbol' => 'KSh', 'description' => 'Kenyan Shilling', 'is_default' => false], + ['name' => 'Moroccan Dirham', 'code' => 'MAD', 'symbol' => 'DH', 'description' => 'Moroccan Dirham', 'is_default' => false], + ['name' => 'Tunisian Dinar', 'code' => 'TND', 'symbol' => 'د.ت', 'description' => 'Tunisian Dinar', 'is_default' => false], + ['name' => 'UAE Dirham', 'code' => 'AED', 'symbol' => 'د.إ', 'description' => 'UAE Dirham', 'is_default' => false], + ['name' => 'Saudi Riyal', 'code' => 'SAR', 'symbol' => '﷼', 'description' => 'Saudi Riyal', 'is_default' => false], + ['name' => 'Qatari Riyal', 'code' => 'QAR', 'symbol' => '﷼', 'description' => 'Qatari Riyal', 'is_default' => false], + ['name' => 'Kuwaiti Dinar', 'code' => 'KWD', 'symbol' => 'د.ك', 'description' => 'Kuwaiti Dinar', 'is_default' => false], + ['name' => 'Bahraini Dinar', 'code' => 'BHD', 'symbol' => '.د.ب', 'description' => 'Bahraini Dinar', 'is_default' => false], + ['name' => 'Omani Rial', 'code' => 'OMR', 'symbol' => '﷼', 'description' => 'Omani Rial', 'is_default' => false], + ['name' => 'Jordanian Dinar', 'code' => 'JOD', 'symbol' => 'د.ا', 'description' => 'Jordanian Dinar', 'is_default' => false], + ['name' => 'Lebanese Pound', 'code' => 'LBP', 'symbol' => '£', 'description' => 'Lebanese Pound', 'is_default' => false], + ['name' => 'Pakistani Rupee', 'code' => 'PKR', 'symbol' => '₨', 'description' => 'Pakistani Rupee', 'is_default' => false], + ['name' => 'Bangladeshi Taka', 'code' => 'BDT', 'symbol' => '৳', 'description' => 'Bangladeshi Taka', 'is_default' => false], + ['name' => 'Sri Lankan Rupee', 'code' => 'LKR', 'symbol' => '₨', 'description' => 'Sri Lankan Rupee', 'is_default' => false], + ['name' => 'Nepalese Rupee', 'code' => 'NPR', 'symbol' => '₨', 'description' => 'Nepalese Rupee', 'is_default' => false], + ['name' => 'Myanmar Kyat', 'code' => 'MMK', 'symbol' => 'K', 'description' => 'Myanmar Kyat', 'is_default' => false], + ['name' => 'Cambodian Riel', 'code' => 'KHR', 'symbol' => '៛', 'description' => 'Cambodian Riel', 'is_default' => false], + ['name' => 'Laotian Kip', 'code' => 'LAK', 'symbol' => '₭', 'description' => 'Laotian Kip', 'is_default' => false], + ['name' => 'Mongolian Tugrik', 'code' => 'MNT', 'symbol' => '₮', 'description' => 'Mongolian Tugrik', 'is_default' => false], + ['name' => 'Kazakhstani Tenge', 'code' => 'KZT', 'symbol' => '₸', 'description' => 'Kazakhstani Tenge', 'is_default' => false], + ['name' => 'Uzbekistani Som', 'code' => 'UZS', 'symbol' => 'лв', 'description' => 'Uzbekistani Som', 'is_default' => false], + ['name' => 'Ukrainian Hryvnia', 'code' => 'UAH', 'symbol' => '₴', 'description' => 'Ukrainian Hryvnia', 'is_default' => false], + ['name' => 'Belarusian Ruble', 'code' => 'BYN', 'symbol' => 'Br', 'description' => 'Belarusian Ruble', 'is_default' => false], + ['name' => 'Moldovan Leu', 'code' => 'MDL', 'symbol' => 'L', 'description' => 'Moldovan Leu', 'is_default' => false], + ['name' => 'Georgian Lari', 'code' => 'GEL', 'symbol' => '₾', 'description' => 'Georgian Lari', 'is_default' => false], + ['name' => 'Armenian Dram', 'code' => 'AMD', 'symbol' => '֏', 'description' => 'Armenian Dram', 'is_default' => false], + ['name' => 'Azerbaijani Manat', 'code' => 'AZN', 'symbol' => '₼', 'description' => 'Azerbaijani Manat', 'is_default' => false], + ['name' => 'Icelandic Krona', 'code' => 'ISK', 'symbol' => 'kr', 'description' => 'Icelandic Krona', 'is_default' => false], + ['name' => 'Albanian Lek', 'code' => 'ALL', 'symbol' => 'L', 'description' => 'Albanian Lek', 'is_default' => false], + ['name' => 'Serbian Dinar', 'code' => 'RSD', 'symbol' => 'дин', 'description' => 'Serbian Dinar', 'is_default' => false], + ['name' => 'Bosnian Mark', 'code' => 'BAM', 'symbol' => 'KM', 'description' => 'Bosnian Mark', 'is_default' => false], + ['name' => 'North Macedonian Denar', 'code' => 'MKD', 'symbol' => 'ден', 'description' => 'North Macedonian Denar', 'is_default' => false], + ['name' => 'Ethiopian Birr', 'code' => 'ETB', 'symbol' => 'Br', 'description' => 'Ethiopian Birr', 'is_default' => false], + ['name' => 'Ghanaian Cedi', 'code' => 'GHS', 'symbol' => '₵', 'description' => 'Ghanaian Cedi', 'is_default' => false], + ['name' => 'Tanzanian Shilling', 'code' => 'TZS', 'symbol' => 'TSh', 'description' => 'Tanzanian Shilling', 'is_default' => false], + ['name' => 'Ugandan Shilling', 'code' => 'UGX', 'symbol' => 'USh', 'description' => 'Ugandan Shilling', 'is_default' => false], + ['name' => 'Zambian Kwacha', 'code' => 'ZMW', 'symbol' => 'ZK', 'description' => 'Zambian Kwacha', 'is_default' => false], + ['name' => 'Botswana Pula', 'code' => 'BWP', 'symbol' => 'P', 'description' => 'Botswana Pula', 'is_default' => false], + ['name' => 'Namibian Dollar', 'code' => 'NAD', 'symbol' => 'N$', 'description' => 'Namibian Dollar', 'is_default' => false], + ['name' => 'Mauritian Rupee', 'code' => 'MUR', 'symbol' => '₨', 'description' => 'Mauritian Rupee', 'is_default' => false], + ['name' => 'Seychellois Rupee', 'code' => 'SCR', 'symbol' => '₨', 'description' => 'Seychellois Rupee', 'is_default' => false], + ['name' => 'Maldivian Rufiyaa', 'code' => 'MVR', 'symbol' => '.ރ', 'description' => 'Maldivian Rufiyaa', 'is_default' => false], + ['name' => 'Fijian Dollar', 'code' => 'FJD', 'symbol' => 'FJ$', 'description' => 'Fijian Dollar', 'is_default' => false], + ['name' => 'Papua New Guinean Kina', 'code' => 'PGK', 'symbol' => 'K', 'description' => 'Papua New Guinean Kina', 'is_default' => false], + ['name' => 'Tongan Paʻanga', 'code' => 'TOP', 'symbol' => 'T$', 'description' => 'Tongan Paʻanga', 'is_default' => false], + ['name' => 'Samoan Tala', 'code' => 'WST', 'symbol' => 'T', 'description' => 'Samoan Tala', 'is_default' => false], + ['name' => 'Vanuatu Vatu', 'code' => 'VUV', 'symbol' => 'VT', 'description' => 'Vanuatu Vatu', 'is_default' => false], + ['name' => 'Solomon Islands Dollar', 'code' => 'SBD', 'symbol' => 'SI$', 'description' => 'Solomon Islands Dollar', 'is_default' => false], + ['name' => 'Brunei Dollar', 'code' => 'BND', 'symbol' => 'B$', 'description' => 'Brunei Dollar', 'is_default' => false], + ['name' => 'East Caribbean Dollar', 'code' => 'XCD', 'symbol' => 'EC$', 'description' => 'East Caribbean Dollar', 'is_default' => false], + ['name' => 'Barbadian Dollar', 'code' => 'BBD', 'symbol' => 'Bds$', 'description' => 'Barbadian Dollar', 'is_default' => false], + ['name' => 'Jamaican Dollar', 'code' => 'JMD', 'symbol' => 'J$', 'description' => 'Jamaican Dollar', 'is_default' => false], + ['name' => 'Trinidad and Tobago Dollar', 'code' => 'TTD', 'symbol' => 'TT$', 'description' => 'Trinidad and Tobago Dollar', 'is_default' => false], + ['name' => 'Bahamian Dollar', 'code' => 'BSD', 'symbol' => 'B$', 'description' => 'Bahamian Dollar', 'is_default' => false], + ['name' => 'Belize Dollar', 'code' => 'BZD', 'symbol' => 'BZ$', 'description' => 'Belize Dollar', 'is_default' => false], + ['name' => 'Costa Rican Colon', 'code' => 'CRC', 'symbol' => '₡', 'description' => 'Costa Rican Colon', 'is_default' => false], + ['name' => 'Guatemalan Quetzal', 'code' => 'GTQ', 'symbol' => 'Q', 'description' => 'Guatemalan Quetzal', 'is_default' => false], + ['name' => 'Honduran Lempira', 'code' => 'HNL', 'symbol' => 'L', 'description' => 'Honduran Lempira', 'is_default' => false], + ['name' => 'Nicaraguan Cordoba', 'code' => 'NIO', 'symbol' => 'C$', 'description' => 'Nicaraguan Cordoba', 'is_default' => false], + ['name' => 'Panamanian Balboa', 'code' => 'PAB', 'symbol' => 'B/.', 'description' => 'Panamanian Balboa', 'is_default' => false], + ['name' => 'Dominican Peso', 'code' => 'DOP', 'symbol' => 'RD$', 'description' => 'Dominican Peso', 'is_default' => false], + ['name' => 'Haitian Gourde', 'code' => 'HTG', 'symbol' => 'G', 'description' => 'Haitian Gourde', 'is_default' => false], + ['name' => 'Cuban Peso', 'code' => 'CUP', 'symbol' => '₱', 'description' => 'Cuban Peso', 'is_default' => false], + ['name' => 'Bolivian Boliviano', 'code' => 'BOB', 'symbol' => '$b', 'description' => 'Bolivian Boliviano', 'is_default' => false], + ['name' => 'Paraguayan Guarani', 'code' => 'PYG', 'symbol' => 'Gs', 'description' => 'Paraguayan Guarani', 'is_default' => false], + ['name' => 'Guyanese Dollar', 'code' => 'GYD', 'symbol' => 'G$', 'description' => 'Guyanese Dollar', 'is_default' => false], + ['name' => 'Surinamese Dollar', 'code' => 'SRD', 'symbol' => 'Sr$', 'description' => 'Surinamese Dollar', 'is_default' => false], + ['name' => 'Venezuelan Bolivar', 'code' => 'VES', 'symbol' => 'Bs.S', 'description' => 'Venezuelan Bolivar', 'is_default' => false], + ['name' => 'Ecuadorian Sucre', 'code' => 'ECS', 'symbol' => 'S/.', 'description' => 'Ecuadorian Sucre', 'is_default' => false], + ]; + + foreach ($currencies as $currency) { + Currency::firstOrCreate( + ['code' => $currency['code'], 'name' => $currency['name']], + $currency + ); + } + } +} diff --git a/database/seeders/CustomQuestionSeeder.php b/database/seeders/CustomQuestionSeeder.php new file mode 100644 index 000000000..834e35a82 --- /dev/null +++ b/database/seeders/CustomQuestionSeeder.php @@ -0,0 +1,58 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Custom questions array + $customQuestions = [ + ['question' => 'What is your expected salary range?', 'required' => 1], + ['question' => 'Do you have any previous experience in this field?', 'required' => 1], + ['question' => 'Are you willing to relocate for this position?', 'required' => 0], + ['question' => 'What are your career goals for the next 5 years?', 'required' => 0], + ['question' => 'Do you have any certifications relevant to this role?', 'required' => 0], + ['question' => 'What is your notice period in your current job?', 'required' => 1], + ['question' => 'Are you comfortable working in a team environment?', 'required' => 1], + ['question' => 'Do you have any questions about the company culture?', 'required' => 0], + ]; + + foreach ($companies as $company) { + foreach ($customQuestions as $questionData) { + // Check if question already exists for this company + if (CustomQuestion::where('question', $questionData['question'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + CustomQuestion::create([ + 'question' => $questionData['question'], + 'required' => $questionData['required'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create custom question: ' . $questionData['question'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Custom Question seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 000000000..6ff3056f2 --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,184 @@ +call([ + // Core system seeders + PermissionSeeder::class, + RoleSeeder::class, + PlanSeeder::class, + DefaultSuperAdminSeeder::class, + DefaultCompanySeeder::class, + DefaultCompanyUserSeeder::class, + CurrencySeeder::class, + EmailTemplateSeeder::class, + LandingPageCustomPageSeeder::class, + + + CouponSeeder::class, + PlanOrderSeeder::class, + PlanRequestSeeder::class, + ReferralSettingSeeder::class, + ReferralSeeder::class, + PayoutRequestSeeder::class, + WebhookSeeder::class, + MediaItemSeeder::class, + + + + // HRM module seeders + BranchSeeder::class, + DepartmentSeeder::class, + DesignationSeeder::class, + DocumentTypeSeeder::class, + EmployeeSeeder::class, + AwardTypeSeeder::class, + AwardSeeder::class, + PromotionSeeder::class, + ResignationSeeder::class, + TerminationSeeder::class, + WarningSeeder::class, + TripSeeder::class, + ComplaintSeeder::class, + EmployeeTransferSeeder::class, + HolidaySeeder::class, + AnnouncementSeeder::class, + AssetTypeSeeder::class, + AssetSeeder::class, + + // Performance Module Seeders + PerformanceIndicatorCategorySeeder::class, + PerformanceIndicatorSeeder::class, + GoalTypeSeeder::class, + EmployeeGoalSeeder::class, + ReviewCycleSeeder::class, + EmployeeReviewSeeder::class, + + // Trainning Seeders + TrainingTypeSeeder::class, + TrainingProgramSeeder::class, + TrainingSessionSeeder::class, + EmployeeTrainingSeeder::class, + + // Recruitment Module Seeders + JobCategorySeeder::class, + JobRequisitionSeeder::class, + JobTypeSeeder::class, + JobLocationSeeder::class, + JobPostingSeeder::class, + CandidateSourceSeeder::class, + CandidateSeeder::class, + InterviewTypeSeeder::class, + InterviewRoundSeeder::class, + InterviewSeeder::class, + InterviewFeedbackSeeder::class, + CandidateAssessmentSeeder::class, + OfferTemplateSeeder::class, + OfferSeeder::class, + OnboardingChecklistSeeder::class, + ChecklistItemSeeder::class, + CandidateOnboardingSeeder::class, + + // Contract Management Seeders + ContractTypeSeeder::class, + EmployeeContractSeeder::class, + ContractRenewalSeeder::class, + ContractTemplateSeeder::class, + + // // Document Management Seeders + DocumentCategorySeeder::class, + HrDocumentSeeder::class, + DocumentAcknowledgmentSeeder::class, + DocumentTemplateSeeder::class, + + + + // // Meeting Management Seeders + MeetingTypeSeeder::class, + MeetingRoomSeeder::class, + MeetingSeeder::class, + MeetingAttendeeSeeder::class, + MeetingMinuteSeeder::class, + ActionItemSeeder::class, + + // Leave management Seeders + LeaveTypeSeeder::class, + LeavePolicySeeder::class, + LeaveApplicationSeeder::class, + LeaveBalanceSeeder::class, + + // // Attendance Management Seeders + ShiftSeeder::class, + AttendancePolicySeeder::class, + AttendanceRecordSeeder::class, + AttendanceRegularizationSeeder::class, + + // // Time Tracking Seeders + TimeEntrySeeder::class, + + // // Payroll Management Seeders + SalaryComponentSeeder::class, + EmployeeSalarySeeder::class, + PayrollRunSeeder::class, + // PayslipSeeder::class, + + + ContactSeeder::class, + NewsletterSeeder::class, + NocTemplateSeeder::class, + JoiningLetterTemplateSeeder::class, + + + + + + + + // User and business seeders + //CompanySeeder::class, + //StaffRoleSeeder::class, + // Business-related seeders + //ContactSeeder::class, + //MediaItemSeeder::class, + // System configuration seeders + //CouponSeeder::class, + //PlanOrderSeeder::class, + //PlanRequestSeeder::class, + //ReferralSettingSeeder::class, + // New seeders + //ReferralSeeder::class, + //PayoutRequestSeeder::class, + //WebhookSeeder::class, + ]); + } else { + $this->call([ + PermissionSeeder::class, + RoleSeeder::class, + PlanSeeder::class, + DefaultSuperAdminSeeder::class, + DefaultCompanySeeder::class, + DefaultCompanyUserSeeder::class, + CurrencySeeder::class, + EmailTemplateSeeder::class, + LandingPageCustomPageSeeder::class, + NocTemplateSeeder::class, + JoiningLetterTemplateSeeder::class, + ExperienceCertificateTemplateSeeder::class, + ]); + } + } +} diff --git a/database/seeders/DefaultCompanySeeder.php b/database/seeders/DefaultCompanySeeder.php new file mode 100644 index 000000000..871ea5211 --- /dev/null +++ b/database/seeders/DefaultCompanySeeder.php @@ -0,0 +1,160 @@ + 'TechCorp Solutions', + 'email' => 'admin@techcorp.com', + 'lang' => 'en', + ], + [ + 'name' => 'Digital Innovations Ltd', + 'email' => 'admin@digitalinnovations.com', + 'lang' => 'en', + ], + [ + 'name' => 'Global Systems Inc', + 'email' => 'admin@globalsystems.com', + 'lang' => 'en', + ], + [ + 'name' => 'Nexus Technologies', + 'email' => 'admin@nexustech.com', + 'lang' => 'en', + ], + [ + 'name' => 'Quantum Dynamics', + 'email' => 'admin@quantumdynamics.com', + 'lang' => 'en', + ], + [ + 'name' => 'Apex Industries', + 'email' => 'admin@apexindustries.com', + 'lang' => 'en', + ], + [ + 'name' => 'Stellar Enterprises', + 'email' => 'admin@stellarenterprises.com', + 'lang' => 'en', + ], + [ + 'name' => 'Phoenix Corporation', + 'email' => 'admin@phoenixcorp.com', + 'lang' => 'en', + ], + [ + 'name' => 'Infinity Solutions', + 'email' => 'admin@infinitysolutions.com', + 'lang' => 'en', + ], + [ + 'name' => 'Vortex Systems', + 'email' => 'admin@vortexsystems.com', + 'lang' => 'en', + ], + [ + 'name' => 'Company', + 'email' => 'company@example.com', + 'lang' => 'en', + ] + ]; + + + + // Filter companies based on demo config + if (config('app.is_saas') == true) { + // Get all plans + $plans = Plan::all(); + if (config('app.is_demo') == true) { + $companiesToCreate = $defaultCompanies; + } else { + $companiesToCreate = array_filter($defaultCompanies, function ($company) { + return $company['email'] === 'company@example.com'; + }); + } + } else { + // Non-SaaS: Only one company + $companiesToCreate = [[ + 'name' => 'Company', + 'email' => 'company@example.com', + 'lang' => 'en', + ]]; + } + + + // Create default companies + if (config('app.is_saas') == true) { + foreach ($companiesToCreate as $companyData) { + // Skip if user already exists + if (User::where('email', $companyData['email'])->exists()) { + continue; + } + + // Create company user + $user = User::create([ + 'name' => $companyData['name'], + 'email' => $companyData['email'], + 'email_verified_at' => now(), + 'password' => Hash::make('password'), + 'type' => 'company', + 'lang' => $companyData['lang'], + 'plan_id' => config('app.is_demo') ? $plans->random()->id : $plans->first()->id, + 'referral_code' => rand(100000, 999999), + 'created_at' => now(), + 'created_by' => 1, + ]); + + // Assign company role + $user->assignRole('company'); + + // Create default settings + if (!Setting::where('user_id', $user->id)->exists()) { + copySettingsFromSuperAdmin($user->id); + } + } + } else { + foreach ($companiesToCreate as $companyData) { + // Skip if user already exists + if (User::where('email', $companyData['email'])->exists()) { + continue; + } + + // Create company user (no plan_id for non-SaaS) + $user = User::create([ + 'name' => $companyData['name'], + 'email' => $companyData['email'], + 'email_verified_at' => now(), + 'password' => Hash::make('password'), + 'type' => 'company', + 'lang' => $companyData['lang'], + 'created_at' => now(), + ]); + + // Assign company role + $user->assignRole('company'); + + // Create default settings + if (!Setting::where('user_id', $user->id)->exists()) { + copySettingsFromSuperAdmin($user->id); + } + } + } + + $this->command->info('Created ' . count($companiesToCreate) . ' default companies successfully!'); + } +} diff --git a/database/seeders/DefaultCompanyUserSeeder.php b/database/seeders/DefaultCompanyUserSeeder.php new file mode 100644 index 000000000..7ed489d6f --- /dev/null +++ b/database/seeders/DefaultCompanyUserSeeder.php @@ -0,0 +1,1091 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run CompanySeeder first.'); + + return; + } + + foreach ($companies as $company) { + $this->createRoles($company); + $this->createUsers($company, $faker); + } + } + + private function createRoles($company) + { + // Employee Role + $employeeRole = Role::firstOrCreate( + ['name' => 'employee', 'guard_name' => 'web', 'created_by' => $company->id], + ['label' => 'Employee', 'description' => 'Employee Role', 'created_by' => $company->id] + ); + $employeeRole->syncPermissions(Permission::whereIn('name', $this->getEmployeePermissions())->get()); + + // Manager Role + $managerRole = Role::firstOrCreate( + ['name' => 'manager', 'guard_name' => 'web', 'created_by' => $company->id], + ['label' => 'Manager', 'description' => 'Manager Role', 'created_by' => $company->id] + ); + $managerRole->syncPermissions(Permission::whereIn('name', $this->getManagerPermissions())->get()); + + // HR Role + $hrRole = Role::firstOrCreate( + ['name' => 'hr', 'guard_name' => 'web', 'created_by' => $company->id], + ['label' => 'HR', 'description' => 'HR Role', 'created_by' => $company->id] + ); + $hrRole->syncPermissions(Permission::whereIn('name', $this->getHRPermissions())->get()); + } + + private function createUsers($company, $faker) + { + $isDemo = config('app.is_demo'); + $isSaas = isSaas(); + + if ($isSaas && $isDemo) { + // SaaS + Demo: 2 managers, 2 HR, 10 employees + $roles = [ + ['role' => 'manager', 'count' => 2], + ['role' => 'hr', 'count' => 2], + ['role' => 'employee', 'count' => 10], + ]; + } elseif ($isSaas && ! $isDemo) { + // SaaS + Non-Demo: 1 manager, 1 HR, 0 employees + $roles = [ + ['role' => 'manager', 'count' => 1], + ['role' => 'hr', 'count' => 1], + ['role' => 'employee', 'count' => 0], + ]; + } elseif (! $isSaas && $isDemo) { + // Non-SaaS + Demo: 2 managers, 2 HR, 10 employees + $roles = [ + ['role' => 'manager', 'count' => 2], + ['role' => 'hr', 'count' => 2], + ['role' => 'employee', 'count' => 10], + ]; + } else { + // Non-SaaS + Non-Demo: 1 manager, 1 HR, 0 employees + $roles = [ + ['role' => 'manager', 'count' => 1], + ['role' => 'hr', 'count' => 1], + ['role' => 'employee', 'count' => 0], + ]; + } + + foreach ($roles as $roleData) { + for ($i = 1; $i <= $roleData['count']; $i++) { + $userData = $this->getUserData($roleData['role'], $faker, $isDemo, $i); + + $user = User::firstOrCreate( + ['email' => $userData['email']], + [ + 'name' => $userData['name'], + 'type' => $roleData['role'], + 'password' => Hash::make('password'), + 'status' => 'active', + 'created_by' => $company->id, + ] + ); + + $user->assignRole(Role::where('name', $roleData['role'])->where('created_by', $company->id)->first()); + } + } + } + + private function getUserData($roleType, $faker, $isDemo, $index) + { + // If index > 1, use faker data to avoid duplicates + if ($index > 1) { + return ['name' => $faker->name, 'email' => $faker->unique()->safeEmail]; + } + + switch ($roleType) { + case 'manager': + return ['name' => 'Manager', 'email' => 'manager@example.com']; + case 'hr': + return ['name' => 'HR', 'email' => 'hr@example.com']; + case 'employee': + return ['name' => 'Employee', 'email' => 'employee@example.com']; + default: + return ['name' => $faker->name, 'email' => $faker->unique()->safeEmail]; + } + } + + private function getEmployeePermissions(): array + { + return [ + 'manage-dashboard', + 'view-dashboard', + + 'manage-calendar', + 'view-calendar', + 'manage-analytics', + + // Media Permissions + 'manage-media', + 'manage-any-media', + 'create-media', + 'edit-media', + 'delete-media', + 'view-media', + 'download-media', + + // Media Directory + 'manage-media-directories', + 'manage-any-media-directories', + 'create-media-directories', + 'edit-media-directories', + 'delete-media-directories', + + // Employee permissions + 'manage-employees', + 'manage-own-employees', + 'view-employees', + + // Award permissions + 'manage-awards', + 'manage-own-awards', + 'view-awards', + + // Promotion permissions + 'manage-promotions', + 'manage-own-promotions', + 'view-promotions', + + // Resignation permissions + 'manage-resignations', + 'view-resignations', + 'manage-own-resignations', + 'create-resignations', + 'edit-resignations', + 'delete-resignations', + + // Termination permissions + 'manage-terminations', + 'manage-own-terminations', + 'view-terminations', + + // Warning permissions + 'manage-warnings', + 'manage-own-warnings', + 'view-warnings', + + // Trip permissions + 'manage-trips', + 'manage-own-trips', + 'view-trips', + + // Complaint permissions + 'manage-complaints', + 'manage-own-complaints', + 'view-complaints', + 'create-complaints', + 'edit-complaints', + 'delete-complaints', + + // Employee Transfer permissions + 'manage-employee-transfers', + 'manage-own-employee-transfers', + 'view-employee-transfers', + + // Holiday permissions + 'manage-holidays', + 'manage-any-holidays', + 'view-holidays', + + // Announcement permissions + 'manage-announcements', + 'manage-any-announcements', + 'view-announcements', + + // Asset Type permissions + 'manage-asset-types', + 'manage-any-asset-types', + 'view-asset-types', + + // Asset permissions + 'manage-assets', + 'view-assets', + + // Training Program permissions + 'manage-training-programs', + 'manage-any-training-programs', + 'view-training-programs', + + // Training Session permissions + 'manage-training-sessions', + 'manage-own-training-sessions', + 'view-training-sessions', + 'manage-attendance', + + // Employee Training permissions + 'manage-employee-trainings', + 'manage-own-employee-trainings', + 'view-employee-trainings', + 'assign-trainings', + 'manage-assessments', + 'record-assessment-results', + + // Performance Indicators + 'manage-performance-indicators', + 'manage-own-performance-indicators', + 'view-performance-indicators', + + // Employee Goals + 'manage-employee-goals', + 'manage-own-employee-goals', + 'view-employee-goals', + + // Review Cycles + 'manage-review-cycles', + 'manage-own-review-cycles', + 'view-review-cycles', + + // Employee Reviews + 'manage-employee-reviews', + 'manage-own-employee-reviews', + 'view-employee-reviews', + + // Job Requisitions management + 'manage-job-requisitions', + 'manage- -job-requisitions', + 'view-job-requisitions', + + // Job Locations management + 'manage-job-locations', + 'manage-any-job-locations', + 'view-job-locations', + + // Job Postings management + 'manage-job-postings', + 'manage-any-job-postings', + 'view-job-postings', + + // Interview Rounds management + 'manage-interview-rounds', + 'manage-any-interview-rounds', + 'view-interview-rounds', + + // Interviews management + 'manage-interviews', + 'manage-own-interviews', + 'view-interviews', + + // Interview Feedback management + 'manage-interview-feedback', + 'manage-own-interview-feedback', + 'view-interview-feedback', + 'create-interview-feedback', + 'edit-interview-feedback', + 'delete-interview-feedback', + + // Candidate Assessments management + 'manage-candidate-assessments', + 'manage-own-candidate-assessments', + 'view-candidate-assessments', + 'create-candidate-assessments', + 'edit-candidate-assessments', + 'delete-candidate-assessments', + + // Candidate Onboarding management + 'manage-candidate-onboarding', + 'manage-own-candidate-onboarding', + 'manage-candidate-onboarding-status', + 'view-candidate-onboarding', + + // Meetings management + 'manage-meetings', + 'manage-own-meetings', + 'view-meetings', + + // Meeting Attendees management + 'manage-meeting-attendees', + 'manage-any-meeting-attendees', + 'view-meeting-attendees', + 'edit-meeting-attendees', + + // Meeting Minutes management + 'manage-meeting-minutes', + 'manage-own-meeting-minutes', + 'view-meeting-minutes', + + // Action Items management + 'manage-action-items', + 'manage-own-action-items', + 'view-action-items', + + // Employee Contracts management + 'manage-employee-contracts', + 'manage-own-employee-contracts', + 'view-employee-contracts', + + // Contract Renewals management + 'manage-contract-renewals', + 'view-contract-renewals', + + // HR Documents management + 'manage-hr-documents', + 'manage-any-hr-documents', + 'view-hr-documents', + + // Document Acknowledgments management + 'manage-document-acknowledgments', + 'manage-own-document-acknowledgments', + 'view-document-acknowledgments', + + // Leave Policies management + 'manage-leave-policies', + 'manage-any-leave-policies', + + // Leave Applications management + 'manage-leave-applications', + 'manage-own-leave-applications', + 'view-leave-applications', + 'create-leave-applications', + 'edit-leave-applications', + 'delete-leave-applications', + + // Leave Balances management + 'manage-leave-balances', + 'manage-own-leave-balances', + 'view-leave-balances', + + // Shifts management + 'manage-shifts', + 'manage-any-shifts', + 'view-shifts', + + // Attendance Policies management + 'manage-attendance-policies', + 'manage-any-attendance-policies', + 'view-attendance-policies', + + // Attendance Records management + 'manage-attendance-records', + 'manage-own-attendance-records', + 'view-attendance-records', + 'create-attendance-records', + 'edit-attendance-records', + 'delete-attendance-records', + 'clock-in-out', + + // Attendance Regularizations management + 'manage-attendance-regularizations', + 'manage-own-attendance-regularizations', + 'view-attendance-regularizations', + 'create-attendance-regularizations', + 'edit-attendance-regularizations', + 'delete-attendance-regularizations', + + // Time Entries management + 'manage-time-entries', + 'manage-own-time-entries', + 'view-time-entries', + 'create-time-entries', + 'edit-time-entries', + + // Employee Salaries management + 'manage-employee-salaries', + 'manage-own-employee-salaries', + 'view-employee-salaries', + + // Payslips management + 'manage-payslips', + 'manage-own-payslips', + 'download-payslips', + + // career page + 'manage-career-page', + + // Download Certificate permission + 'download-joining-letter', + 'download-experience-certificate', + 'download-noc-certificate', + ]; + } + + private function getManagerPermissions(): array + { + return [ + 'manage-dashboard', + 'view-dashboard', + + 'manage-calendar', + 'view-calendar', + 'manage-analytics', + + // Media Permissions + 'manage-media', + 'manage-any-media', + 'create-media', + 'edit-media', + 'delete-media', + 'view-media', + 'download-media', + + // Media Directory + 'manage-media-directories', + 'manage-any-media-directories', + 'create-media-directories', + 'edit-media-directories', + 'delete-media-directories', + + // Branch Permissions + 'manage-branches', + 'manage-any-branches', + 'view-branches', + 'create-branches', + 'edit-branches', + 'delete-branches', + 'toggle-status-branches', + + // Department Permissions + 'manage-departments', + 'manage-any-departments', + 'view-departments', + 'create-departments', + 'edit-departments', + 'delete-departments', + 'toggle-status-departments', + + // Designation Permissions + 'manage-designations', + 'manage-any-designations', + 'view-designations', + 'create-designations', + 'edit-designations', + 'delete-designations', + 'toggle-status-designations', + + // Document Type Permissions + 'manage-document-types', + 'manage-any-document-types', + 'view-document-types', + 'create-document-types', + 'edit-document-types', + 'delete-document-types', + + // Employee permissions + 'manage-employees', + 'manage-any-employees', + 'view-employees', + 'create-employees', + 'edit-employees', + + // Award Type management + 'manage-award-types', + 'manage-any-award-types', + 'view-award-types', + 'create-award-types', + 'edit-award-types', + 'delete-award-types', + + // Award Type management + 'manage-award-types', + 'manage-any-award-types', + 'view-award-types', + 'create-award-types', + 'edit-award-types', + 'delete-award-types', + + // Award management + 'manage-awards', + 'manage-any-awards', + 'view-awards', + 'create-awards', + 'edit-awards', + 'delete-awards', + + // Promotion management + 'manage-promotions', + 'manage-any-promotions', + 'view-promotions', + 'create-promotions', + 'edit-promotions', + 'delete-promotions', + 'approve-promotions', + 'reject-promotions', + + // Resignation management + 'manage-resignations', + 'manage-any-resignations', + 'view-resignations', + 'create-resignations', + 'edit-resignations', + 'delete-resignations', + 'approve-resignations', + 'reject-resignations', + + // Termination management + 'manage-terminations', + 'manage-any-terminations', + 'view-terminations', + 'create-terminations', + 'edit-terminations', + 'delete-terminations', + 'approve-terminations', + 'reject-terminations', + + // Warning management + 'manage-warnings', + 'manage-any-warnings', + 'view-warnings', + 'create-warnings', + 'edit-warnings', + 'delete-warnings', + 'approve-warnings', + 'acknowledge-warnings', + + // Trip management + 'manage-trips', + 'manage-any-trips', + 'view-trips', + 'create-trips', + 'edit-trips', + 'delete-trips', + 'approve-trips', + 'manage-trip-expenses', + 'approve-trip-expenses', + + // Complaint management + 'manage-complaints', + 'manage-any-complaints', + 'view-complaints', + 'create-complaints', + 'edit-complaints', + 'delete-complaints', + 'assign-complaints', + 'resolve-complaints', + + // Employee Transfer management + 'manage-employee-transfers', + 'manage-any-employee-transfers', + 'view-employee-transfers', + 'create-employee-transfers', + 'edit-employee-transfers', + 'delete-employee-transfers', + 'approve-employee-transfers', + 'reject-employee-transfers', + + // Holiday management + 'manage-holidays', + 'manage-any-holidays', + 'view-holidays', + 'create-holidays', + 'edit-holidays', + 'delete-holidays', + + // Announcement management + 'manage-announcements', + 'manage-any-announcements', + 'view-announcements', + 'create-announcements', + 'edit-announcements', + 'delete-announcements', + + // Asset Type management + 'manage-asset-types', + 'manage-any-asset-types', + 'view-asset-types', + 'create-asset-types', + 'edit-asset-types', + 'delete-asset-types', + + // Asset management + 'manage-assets', + 'manage-any-assets', + 'view-assets', + 'create-assets', + 'edit-assets', + 'delete-assets', + 'assign-assets', + 'manage-asset-maintenance', + + // Training Type management + 'manage-training-types', + 'manage-any-training-types', + 'view-training-types', + 'create-training-types', + 'edit-training-types', + 'delete-training-types', + + // Training Program management + 'manage-training-programs', + 'manage-any-training-programs', + 'view-training-programs', + 'create-training-programs', + 'edit-training-programs', + 'delete-training-programs', + + // Training Session management + 'manage-training-sessions', + 'manage-any-training-sessions', + 'view-training-sessions', + 'create-training-sessions', + 'edit-training-sessions', + 'delete-training-sessions', + 'manage-attendance', + + // Employee Training management + 'manage-employee-trainings', + 'manage-any-employee-trainings', + 'view-employee-trainings', + 'create-employee-trainings', + 'edit-employee-trainings', + 'delete-employee-trainings', + 'assign-trainings', + 'manage-assessments', + 'record-assessment-results', + + // Performance Indicator Category management + 'manage-performance-indicator-categories', + 'manage-any-performance-indicator-categories', + 'view-performance-indicator-categories', + 'create-performance-indicator-categories', + 'edit-performance-indicator-categories', + 'delete-performance-indicator-categories', + + // Performance Indicators management + 'manage-performance-indicators', + 'manage-any-performance-indicators', + 'view-performance-indicators', + 'create-performance-indicators', + 'edit-performance-indicators', + 'delete-performance-indicators', + + // Goal Types + 'manage-goal-types', + 'manage-any-goal-types', + 'view-goal-types', + 'create-goal-types', + 'edit-goal-types', + 'delete-goal-types', + + // Employee Goals + 'manage-employee-goals', + 'manage-any-employee-goals', + 'view-employee-goals', + 'create-employee-goals', + 'edit-employee-goals', + 'delete-employee-goals', + + // Review Cycles + 'manage-review-cycles', + 'manage-any-review-cycles', + 'view-review-cycles', + 'create-review-cycles', + 'edit-review-cycles', + 'delete-review-cycles', + + // Employee Reviews + 'manage-employee-reviews', + 'manage-any-employee-reviews', + 'view-employee-reviews', + 'create-employee-reviews', + 'edit-employee-reviews', + 'delete-employee-reviews', + + // Job Categories + 'manage-job-categories', + 'manage-any-job-categories', + 'view-job-categories', + 'create-job-categories', + 'edit-job-categories', + 'delete-job-categories', + + // Job Requisitions + 'manage-job-requisitions', + 'manage-any-job-requisitions', + 'view-job-requisitions', + 'create-job-requisitions', + 'edit-job-requisitions', + 'delete-job-requisitions', + 'approve-job-requisitions', + + // Job Types + 'manage-job-types', + 'manage-any-job-types', + 'view-job-types', + 'create-job-types', + 'edit-job-types', + 'delete-job-types', + + // Job Locations + 'manage-job-locations', + 'manage-any-job-locations', + 'view-job-locations', + 'create-job-locations', + 'edit-job-locations', + 'delete-job-locations', + + // Job Postings + 'manage-job-postings', + 'manage-any-job-postings', + 'view-job-postings', + 'create-job-postings', + 'edit-job-postings', + 'delete-job-postings', + 'publish-job-postings', + + // Candidate Sources + 'manage-candidate-sources', + 'manage-any-candidate-sources', + 'view-candidate-sources', + 'create-candidate-sources', + 'edit-candidate-sources', + 'delete-candidate-sources', + + // Candidates + 'manage-candidates', + 'manage-any-candidates', + 'view-candidates', + 'convert-to-employee', + // 'create-candidates', + 'edit-candidates', + 'delete-candidates', + + // Interview Types + 'manage-interview-types', + 'manage-any-interview-types', + 'view-interview-types', + 'create-interview-types', + 'edit-interview-types', + 'delete-interview-types', + + // Interview Rounds + 'manage-interview-rounds', + 'manage-any-interview-rounds', + 'view-interview-rounds', + 'create-interview-rounds', + 'edit-interview-rounds', + 'delete-interview-rounds', + + // Interviews + 'manage-interviews', + 'manage-any-interviews', + 'view-interviews', + 'create-interviews', + 'edit-interviews', + 'delete-interviews', + + // Interview Feedback + 'manage-interview-feedback', + 'manage-any-interview-feedback', + 'view-interview-feedback', + 'create-interview-feedback', + 'edit-interview-feedback', + 'delete-interview-feedback', + + // Candidate Assessments + 'manage-candidate-assessments', + 'manage-any-candidate-assessments', + 'view-candidate-assessments', + 'create-candidate-assessments', + 'edit-candidate-assessments', + 'delete-candidate-assessments', + + // Offer Templates + 'manage-offer-templates', + 'manage-any-offer-templates', + 'view-offer-templates', + 'create-offer-templates', + 'edit-offer-templates', + 'delete-offer-templates', + + // Offers + 'manage-offers', + 'manage-any-offers', + 'view-offers', + 'create-offers', + 'edit-offers', + 'delete-offers', + 'approve-offers', + + // Onboarding Checklists management + 'manage-onboarding-checklists', + 'manage-any-onboarding-checklists', + 'view-onboarding-checklists', + 'create-onboarding-checklists', + 'edit-onboarding-checklists', + 'delete-onboarding-checklists', + + // Checklist Items management + 'manage-checklist-items', + 'manage-any-checklist-items', + 'view-checklist-items', + 'create-checklist-items', + 'edit-checklist-items', + 'delete-checklist-items', + + // Candidate Onboarding management + 'manage-candidate-onboarding', + 'manage-any-candidate-onboarding', + 'manage-candidate-onboarding-status', + 'view-candidate-onboarding', + 'create-candidate-onboarding', + 'edit-candidate-onboarding', + 'delete-candidate-onboarding', + + // Meeting Types management + 'manage-meeting-types', + 'manage-any-meeting-types', + 'view-meeting-types', + 'create-meeting-types', + 'edit-meeting-types', + 'delete-meeting-types', + + // Meeting Rooms management + 'manage-meeting-rooms', + 'manage-any-meeting-rooms', + 'view-meeting-rooms', + 'create-meeting-rooms', + 'edit-meeting-rooms', + 'delete-meeting-rooms', + + // Meetings management + 'manage-meetings', + 'manage-any-meetings', + 'view-meetings', + 'create-meetings', + 'edit-meetings', + 'delete-meetings', + 'manage-meeting-status', + + // Meeting Attendees management + 'manage-meeting-attendees', + 'manage-any-meeting-attendees', + 'view-meeting-attendees', + 'create-meeting-attendees', + 'edit-meeting-attendees', + 'delete-meeting-attendees', + 'manage-meeting-rsvp-status', + 'manage-meeting-attendance', + + // Meeting Minutes management + 'manage-meeting-minutes', + 'manage-any-meeting-minutes', + 'view-meeting-minutes', + 'create-meeting-minutes', + 'edit-meeting-minutes', + 'delete-meeting-minutes', + + // Action Items management + 'manage-action-items', + 'manage-any-action-items', + 'view-action-items', + 'create-action-items', + 'edit-action-items', + 'delete-action-items', + + // Contract Types management + 'manage-contract-types', + 'manage-any-contract-types', + 'view-contract-types', + 'create-contract-types', + 'edit-contract-types', + 'delete-contract-types', + + // Employee Contracts management + 'manage-employee-contracts', + 'manage-any-employee-contracts', + 'view-employee-contracts', + 'create-employee-contracts', + 'edit-employee-contracts', + 'delete-employee-contracts', + 'approve-employee-contracts', + 'reject-employee-contracts', + + // Contract Renewals management + 'manage-contract-renewals', + 'manage-any-contract-renewals', + 'view-contract-renewals', + 'create-contract-renewals', + 'edit-contract-renewals', + 'delete-contract-renewals', + 'approve-contract-renewals', + 'reject-contract-renewals', + + // Contract Templates management + 'manage-contract-templates', + 'manage-any-contract-templates', + 'view-contract-templates', + 'create-contract-templates', + 'edit-contract-templates', + 'delete-contract-templates', + + // Document Categories management + 'manage-document-categories', + 'manage-any-document-categories', + 'view-document-categories', + 'create-document-categories', + 'edit-document-categories', + 'delete-document-categories', + + // HR Documents management + 'manage-hr-documents', + 'manage-any-hr-documents', + 'view-hr-documents', + 'create-hr-documents', + 'edit-hr-documents', + 'delete-hr-documents', + + // Document Acknowledgments management + 'manage-document-acknowledgments', + 'manage-any-document-acknowledgments', + 'view-document-acknowledgments', + 'create-document-acknowledgments', + 'edit-document-acknowledgments', + 'delete-document-acknowledgments', + 'acknowledge-document-acknowledgments', + + // Document Templates management + 'manage-document-templates', + 'manage-any-document-templates', + 'view-document-templates', + 'create-document-templates', + 'edit-document-templates', + 'delete-document-templates', + + // Leave Types management + 'manage-leave-types', + 'manage-any-leave-types', + 'view-leave-types', + 'create-leave-types', + 'edit-leave-types', + 'delete-leave-types', + + // Leave Policies management + 'manage-leave-policies', + 'manage-any-leave-policies', + 'view-leave-policies', + 'create-leave-policies', + 'edit-leave-policies', + 'delete-leave-policies', + + // Leave Applications management + 'manage-leave-applications', + 'manage-any-leave-applications', + 'view-leave-applications', + 'create-leave-applications', + 'edit-leave-applications', + 'delete-leave-applications', + 'approve-leave-applications', + 'reject-leave-applications', + + // Leave Balances management + 'manage-leave-balances', + 'manage-any-leave-balances', + 'view-leave-balances', + 'create-leave-balances', + 'edit-leave-balances', + 'delete-leave-balances', + 'adjust-leave-balances', + + // Shifts management + 'manage-shifts', + 'manage-any-shifts', + 'view-shifts', + 'create-shifts', + 'edit-shifts', + 'delete-shifts', + + // Attendance Policies management + 'manage-attendance-policies', + 'manage-any-attendance-policies', + 'view-attendance-policies', + 'create-attendance-policies', + 'edit-attendance-policies', + 'delete-attendance-policies', + + // Attendance Records management + 'manage-attendance-records', + 'manage-any-attendance-records', + 'view-attendance-records', + 'create-attendance-records', + 'edit-attendance-records', + 'delete-attendance-records', + 'clock-in-out', + + // Attendance Regularizations management + 'manage-attendance-regularizations', + 'manage-any-attendance-regularizations', + 'view-attendance-regularizations', + 'create-attendance-regularizations', + 'edit-attendance-regularizations', + 'delete-attendance-regularizations', + 'approve-attendance-regularizations', + 'reject-attendance-regularizations', + + // Time Entries management + 'manage-time-entries', + 'manage-any-time-entries', + 'view-time-entries', + 'create-time-entries', + 'edit-time-entries', + 'delete-time-entries', + 'approve-time-entries', + 'reject-time-entries', + + // Salary Components management + 'manage-salary-components', + 'manage-any-salary-components', + 'view-salary-components', + 'create-salary-components', + 'edit-salary-components', + 'delete-salary-components', + + // Employee Salaries management + 'manage-employee-salaries', + 'manage-any-employee-salaries', + 'view-employee-salaries', + 'create-employee-salaries', + 'edit-employee-salaries', + 'delete-employee-salaries', + + // Payroll Runs management + 'manage-payroll-runs', + 'manage-any-payroll-runs', + 'view-payroll-runs', + 'create-payroll-runs', + 'edit-payroll-runs', + 'delete-payroll-runs', + 'process-payroll-runs', + + // Payslips management + 'manage-payslips', + 'manage-any-payslips', + 'view-payslips', + 'create-payslips', + 'download-payslips', + 'send-payslips', + + // career page + 'manage-career-page', + + // Download Certificate permission + 'download-joining-letter', + 'download-experience-certificate', + 'download-noc-certificate', + ]; + } + + private function getHRPermissions(): array + { + return $this->getManagerPermissions(); + } +} diff --git a/database/seeders/DefaultSuperAdminSeeder.php b/database/seeders/DefaultSuperAdminSeeder.php new file mode 100644 index 000000000..dda2f5851 --- /dev/null +++ b/database/seeders/DefaultSuperAdminSeeder.php @@ -0,0 +1,38 @@ + 'superadmin@example.com'], + [ + 'name' => 'Super Admin', + 'email_verified_at' => now(), + 'password' => Hash::make('password'), + 'type' => 'superadmin', + 'lang' => 'en' + ] + ); + + // Assign super admin role + $superAdmin->assignRole('superadmin'); + + // Create default settings for superadmin if not exists + if (!Setting::where('user_id', $superAdmin->id)->exists()) { + createDefaultSettings($superAdmin->id); + } + } + } +} diff --git a/database/seeders/DepartmentPermissionSeeder.php b/database/seeders/DepartmentPermissionSeeder.php new file mode 100644 index 000000000..f1ea17d48 --- /dev/null +++ b/database/seeders/DepartmentPermissionSeeder.php @@ -0,0 +1,33 @@ + $permission, + 'guard_name' => 'web', + 'module' => 'department', + 'label' => ucfirst(str_replace('-', ' ', $permission)), + 'description' => 'Ability to ' . str_replace('-', ' ', $permission), + ]); + } + } +} \ No newline at end of file diff --git a/database/seeders/DepartmentSeeder.php b/database/seeders/DepartmentSeeder.php new file mode 100644 index 000000000..6696f4c10 --- /dev/null +++ b/database/seeders/DepartmentSeeder.php @@ -0,0 +1,83 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Department names with descriptions + $departments = [ + ['name' => 'Human Resources', 'description' => 'Manages employee relations, recruitment, training, benefits administration, and organizational development'], + ['name' => 'Information Technology', 'description' => 'Responsible for managing IT infrastructure, software development, system maintenance, and technical support'], + ['name' => 'Finance & Accounting', 'description' => 'Handles financial planning, budgeting, accounting, financial reporting, and compliance with financial regulations'], + ['name' => 'Marketing', 'description' => 'Develops marketing strategies, manages brand promotion, digital marketing campaigns, and market research'], + ['name' => 'Sales', 'description' => 'Focuses on revenue generation, client acquisition, customer relationship management, and sales target achievement'], + ['name' => 'Operations', 'description' => 'Oversees daily business operations, process optimization, quality control, and operational efficiency'], + ['name' => 'Customer Service', 'description' => 'Provides customer support, handles inquiries and complaints, and ensures customer satisfaction and retention'], + ['name' => 'Research & Development', 'description' => 'Conducts research, develops new products and services, innovation management, and technology advancement'], + ['name' => 'Legal', 'description' => 'Manages legal compliance, contract negotiations, risk management, and provides legal counsel to the organization'], + ['name' => 'Administration', 'description' => 'Handles administrative functions, office management, documentation, and general administrative support services'] + ]; + + foreach ($companies as $company) { + // Get all branches for this company + $branches = Branch::where('created_by', $company->id)->get(); + + if ($branches->isEmpty()) { + $this->command->warn('No branches found for company: ' . $company->name . '. Please run BranchSeeder first.'); + continue; + } + + foreach ($branches as $branch) { + // Create 5-8 departments for each branch + $departmentCount = rand(2,3); + + for ($i = 0; $i < $departmentCount; $i++) { + $department = $departments[$i]; + $departmentName = $department['name']; + $departmentDescription = $department['description']; + + // Check if department already exists for this branch + if (Department::where('name', $departmentName)->where('branch_id', $branch->id)->exists()) { + continue; + } + + try { + Department::create([ + 'name' => $departmentName, + 'branch_id' => $branch->id, + 'description' => $departmentDescription, + 'status' => 'active', + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create department: ' . $departmentName . ' for branch: ' . $branch->name); + continue; + } + } + } + } + + $this->command->info('Department seeder completed successfully!'); + } +} diff --git a/database/seeders/DesignationSeeder.php b/database/seeders/DesignationSeeder.php new file mode 100644 index 000000000..c4388a9fe --- /dev/null +++ b/database/seeders/DesignationSeeder.php @@ -0,0 +1,138 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Designation names and descriptions by department type + $designationsByDepartment = [ + 'Human Resources' => [ + ['name' => 'HR Manager', 'description' => 'Oversees all HR functions including recruitment, employee relations, and policy implementation'], + ['name' => 'HR Executive', 'description' => 'Handles day-to-day HR operations, employee onboarding, and maintains employee records'], + ['name' => 'Recruiter', 'description' => 'Responsible for talent acquisition, conducting interviews, and managing recruitment processes'], + ['name' => 'HR Assistant', 'description' => 'Provides administrative support to HR team and assists in various HR activities'] + ], + 'Information Technology' => [ + ['name' => 'IT Manager', 'description' => 'Manages IT infrastructure, oversees technical team, and ensures system security and performance'], + ['name' => 'Software Developer', 'description' => 'Designs, develops, and maintains software applications and systems'], + ['name' => 'System Administrator', 'description' => 'Maintains and configures computer systems, networks, and servers'], + ['name' => 'Technical Lead', 'description' => 'Leads technical projects, mentors developers, and ensures code quality standards'], + ['name' => 'QA Engineer', 'description' => 'Tests software applications, identifies bugs, and ensures quality standards are met'] + ], + 'Finance & Accounting' => [ + ['name' => 'Finance Manager', 'description' => 'Oversees financial planning, budgeting, and financial reporting activities'], + ['name' => 'Accountant', 'description' => 'Manages financial records, prepares financial statements, and ensures compliance'], + ['name' => 'Financial Analyst', 'description' => 'Analyzes financial data, prepares reports, and provides insights for decision making'], + ['name' => 'Accounts Executive', 'description' => 'Handles accounts payable/receivable, invoice processing, and vendor management'] + ], + 'Marketing' => [ + ['name' => 'Marketing Manager', 'description' => 'Develops marketing strategies, manages campaigns, and oversees brand promotion'], + ['name' => 'Marketing Executive', 'description' => 'Executes marketing campaigns, manages social media, and coordinates promotional activities'], + ['name' => 'Digital Marketing Specialist', 'description' => 'Manages online marketing campaigns, SEO/SEM, and digital advertising strategies'], + ['name' => 'Content Writer', 'description' => 'Creates engaging content for websites, blogs, marketing materials, and social media'] + ], + 'Sales' => [ + ['name' => 'Sales Manager', 'description' => 'Leads sales team, develops sales strategies, and manages client relationships'], + ['name' => 'Sales Executive', 'description' => 'Generates leads, conducts sales presentations, and closes deals with potential clients'], + ['name' => 'Sales Representative', 'description' => 'Represents company products/services, maintains customer relationships, and achieves sales targets'], + ['name' => 'Business Development Executive', 'description' => 'Identifies new business opportunities, develops partnerships, and expands market reach'] + ], + 'Operations' => [ + ['name' => 'Operations Manager', 'description' => 'Oversees daily operations, ensures efficiency, and manages operational processes'], + ['name' => 'Operations Executive', 'description' => 'Coordinates operational activities, monitors performance, and implements process improvements'], + ['name' => 'Process Analyst', 'description' => 'Analyzes business processes, identifies inefficiencies, and recommends optimization solutions'], + ['name' => 'Operations Coordinator', 'description' => 'Coordinates between departments, schedules activities, and ensures smooth operations'] + ], + 'Customer Service' => [ + ['name' => 'Customer Service Manager', 'description' => 'Manages customer service team, handles escalations, and ensures customer satisfaction'], + ['name' => 'Customer Support Executive', 'description' => 'Provides customer support, resolves queries, and maintains positive customer relationships'], + ['name' => 'Call Center Agent', 'description' => 'Handles inbound/outbound calls, provides information, and assists customers with their needs'] + ], + 'Research & Development' => [ + ['name' => 'R&D Manager', 'description' => 'Leads research initiatives, manages R&D projects, and drives innovation strategies'], + ['name' => 'Research Analyst', 'description' => 'Conducts market research, analyzes data, and provides insights for product development'], + ['name' => 'Product Developer', 'description' => 'Develops new products, improves existing offerings, and ensures market readiness'], + ['name' => 'Innovation Specialist', 'description' => 'Identifies emerging trends, evaluates new technologies, and drives innovation initiatives'] + ], + 'Legal' => [ + ['name' => 'Legal Manager', 'description' => 'Manages legal affairs, oversees contracts, and ensures regulatory compliance'], + ['name' => 'Legal Advisor', 'description' => 'Provides legal counsel, reviews agreements, and advises on legal matters'], + ['name' => 'Compliance Officer', 'description' => 'Ensures company compliance with laws and regulations, manages risk assessments'] + ], + 'Administration' => [ + ['name' => 'Admin Manager', 'description' => 'Oversees administrative functions, manages office operations, and ensures smooth workflow'], + ['name' => 'Administrative Assistant', 'description' => 'Provides administrative support, manages schedules, and handles office correspondence'], + ['name' => 'Office Coordinator', 'description' => 'Coordinates office activities, manages supplies, and ensures efficient office operations'] + ] + ]; + + foreach ($companies as $company) { + // Get all departments for this company + $departments = Department::where('created_by', $company->id)->get(); + + if ($departments->isEmpty()) { + $this->command->warn('No departments found for company: ' . $company->name . '. Please run DepartmentSeeder first.'); + continue; + } + + foreach ($departments as $department) { + // Get designations for this department type + $designations = $designationsByDepartment[$department->name] ?? [ + ['name' => 'Manager', 'description' => 'Manages department operations and oversees team performance'], + ['name' => 'Executive', 'description' => 'Executes departmental tasks and supports management activities'], + ['name' => 'Assistant', 'description' => 'Provides administrative support and assists in daily operations'] + ]; + + // Create 1-3 designations for each department + $designationCount = rand(1, min(3, count($designations))); + + for ($i = 0; $i < $designationCount; $i++) { + $designation = $designations[$i]; + $designationName = $designation['name']; + $designationDescription = $designation['description']; + + // Check if designation already exists for this department + if (Designation::where('name', $designationName)->where('department_id', $department->id)->exists()) { + continue; + } + + try { + Designation::create([ + 'name' => $designationName, + 'department_id' => $department->id, + 'description' => $designationDescription, + 'status' => 'active', + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create designation: ' . $designationName . ' for department: ' . $department->name); + continue; + } + } + } + } + + $this->command->info('Designation seeder completed successfully!'); + } +} diff --git a/database/seeders/DocumentAcknowledgmentSeeder.php b/database/seeders/DocumentAcknowledgmentSeeder.php new file mode 100644 index 000000000..dd39e8131 --- /dev/null +++ b/database/seeders/DocumentAcknowledgmentSeeder.php @@ -0,0 +1,119 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed acknowledgment statuses for consistent data + $acknowledgmentStatuses = ['Acknowledged', 'Acknowledged', 'Pending', 'Acknowledged', 'Overdue', 'Acknowledged', 'Pending', 'Acknowledged']; + + foreach ($companies as $company) { + // Get HR documents that require acknowledgment for this company + $hrDocuments = HrDocument::where('created_by', $company->id) + ->where('requires_acknowledgment', true) + ->where('status', 'Published') + ->get(); + + if ($hrDocuments->isEmpty()) { + $this->command->warn('No HR documents requiring acknowledgment found for company: ' . $company->name . '. Please run HrDocumentSeeder first.'); + continue; + } + + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get HR users for assignment + $hrUsers = User::whereIn('type', ['hr', 'manager']) + ->where('created_by', $company->id) + ->get(); + + if ($hrUsers->isEmpty()) { + $this->command->warn('No HR users found for company: ' . $company->name); + continue; + } + + // Create acknowledgments for each document and employee combination + foreach ($hrDocuments as $docIndex => $document) { + // Assign to first 8 employees + $selectedEmployees = $employees->take(8); + + foreach ($selectedEmployees as $empIndex => $employee) { + // Check if acknowledgment already exists + if (DocumentAcknowledgment::where('document_id', $document->id) + ->where('user_id', $employee->id) + ->exists() + ) { + continue; + } + + $status = $acknowledgmentStatuses[$empIndex % 8]; + $assignedBy = $hrUsers->first(); + + $assignedAt = date('Y-m-d H:i:s', strtotime('-' . ($docIndex + $empIndex + 1) . ' days')); + $dueDate = date('Y-m-d', strtotime($assignedAt . ' +7 days')); + + $acknowledgedAt = null; + $acknowledgmentNote = null; + $ipAddress = null; + $userAgent = null; + + if ($status === 'Acknowledged') { + $acknowledgedAt = date('Y-m-d H:i:s', strtotime($assignedAt . ' +' . ($empIndex + 1) . ' days')); + $acknowledgmentNote = 'I have read and understood the document contents and agree to comply with the policies outlined.'; + $ipAddress = '192.168.1.' . (100 + $empIndex); + $userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'; + } elseif ($status === 'Overdue') { + // Due date has passed but not acknowledged + $dueDate = date('Y-m-d', strtotime('-2 days')); + } + + try { + DocumentAcknowledgment::create([ + 'document_id' => $document->id, + 'user_id' => $employee->id, + 'status' => $status, + 'acknowledged_at' => $acknowledgedAt, + 'due_date' => $dueDate, + 'acknowledgment_note' => $acknowledgmentNote, + 'ip_address' => $ipAddress, + 'user_agent' => $userAgent, + 'assigned_by' => $assignedBy->id, + 'assigned_at' => $assignedAt, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create document acknowledgment for document: ' . $document->title . ' and employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('DocumentAcknowledgment seeder completed successfully!'); + } +} diff --git a/database/seeders/DocumentCategorySeeder.php b/database/seeders/DocumentCategorySeeder.php new file mode 100644 index 000000000..3cd4095e1 --- /dev/null +++ b/database/seeders/DocumentCategorySeeder.php @@ -0,0 +1,145 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed document categories for consistent data + $documentCategories = [ + [ + 'name' => 'Identity Documents', + 'description' => 'Personal identification documents including passport, national ID, driving license', + 'color' => '#3B82F6', + 'icon' => 'IdCard', + 'sort_order' => 1, + 'is_mandatory' => true, + 'status' => 'active' + ], + [ + 'name' => 'Educational Certificates', + 'description' => 'Academic qualifications, degrees, diplomas, and professional certifications', + 'color' => '#10b77f', + 'icon' => 'GraduationCap', + 'sort_order' => 2, + 'is_mandatory' => true, + 'status' => 'active' + ], + [ + 'name' => 'Employment Documents', + 'description' => 'Employment contracts, offer letters, job descriptions, and work agreements', + 'color' => '#F59E0B', + 'icon' => 'Briefcase', + 'sort_order' => 3, + 'is_mandatory' => true, + 'status' => 'active' + ], + [ + 'name' => 'Financial Documents', + 'description' => 'Bank statements, salary slips, tax documents, and financial records', + 'color' => '#EF4444', + 'icon' => 'DollarSign', + 'sort_order' => 4, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Medical Records', + 'description' => 'Health certificates, medical reports, fitness certificates, and insurance documents', + 'color' => '#8B5CF6', + 'icon' => 'Heart', + 'sort_order' => 5, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Legal Documents', + 'description' => 'Legal agreements, compliance certificates, background verification reports', + 'color' => '#6B7280', + 'icon' => 'Scale', + 'sort_order' => 6, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Training Certificates', + 'description' => 'Professional training certificates, skill development programs, workshop completions', + 'color' => '#06B6D4', + 'icon' => 'Award', + 'sort_order' => 7, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Performance Records', + 'description' => 'Performance appraisals, feedback reports, achievement certificates, and reviews', + 'color' => '#84CC16', + 'icon' => 'TrendingUp', + 'sort_order' => 8, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Personal Documents', + 'description' => 'Personal information, emergency contacts, family details, and personal references', + 'color' => '#F97316', + 'icon' => 'User', + 'sort_order' => 9, + 'is_mandatory' => true, + 'status' => 'active' + ], + [ + 'name' => 'Compliance Documents', + 'description' => 'Regulatory compliance documents, policy acknowledgments, and statutory requirements', + 'color' => '#DC2626', + 'icon' => 'Shield', + 'sort_order' => 10, + 'is_mandatory' => true, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($documentCategories as $categoryData) { + // Check if document category already exists for this company + if (DocumentCategory::where('name', $categoryData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + DocumentCategory::create([ + 'name' => $categoryData['name'], + 'description' => $categoryData['description'], + 'color' => $categoryData['color'], + 'icon' => $categoryData['icon'], + 'sort_order' => $categoryData['sort_order'], + 'is_mandatory' => $categoryData['is_mandatory'], + 'status' => $categoryData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create document category: ' . $categoryData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('DocumentCategory seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/DocumentTemplateSeeder.php b/database/seeders/DocumentTemplateSeeder.php new file mode 100644 index 000000000..6e349f0d0 --- /dev/null +++ b/database/seeders/DocumentTemplateSeeder.php @@ -0,0 +1,322 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed document templates for consistent data + $documentTemplates = [ + [ + 'name' => 'Employment Offer Letter', + 'description' => 'Standard template for employment offer letters with position details and compensation', + 'category' => 'Employment Documents', + 'template_content' => 'Dear {{candidate_name}}, + +We are pleased to offer you the position of {{job_title}} at {{company_name}}. + +Position Details: +- Job Title: {{job_title}} +- Department: {{department}} +- Reporting Manager: {{manager_name}} +- Start Date: {{start_date}} +- Work Location: {{work_location}} + +Compensation Package: +- Annual Salary: {{annual_salary}} +- Benefits: {{benefits_package}} +- Probation Period: {{probation_period}} months + +Terms and Conditions: +- This offer is contingent upon successful completion of background verification +- Please confirm your acceptance by {{response_deadline}} +- Notice period: {{notice_period}} days + +We look forward to welcoming you to our team. + +Best regards, +{{hr_manager_name}} +Human Resources +{{company_name}}', + 'placeholders' => ['candidate_name', 'job_title', 'company_name', 'department', 'manager_name', 'start_date', 'work_location', 'annual_salary', 'benefits_package', 'probation_period', 'response_deadline', 'notice_period', 'hr_manager_name'], + 'default_values' => ['probation_period' => '6', 'notice_period' => '30', 'benefits_package' => 'Health Insurance, Provident Fund'], + 'is_default' => true, + 'file_format' => 'pdf', + 'status' => 'active' + ], + [ + 'name' => 'Experience Certificate', + 'description' => 'Template for employee experience certificates upon resignation or completion of service', + 'category' => 'Employment Documents', + 'template_content' => 'EXPERIENCE CERTIFICATE + +This is to certify that {{employee_name}} was employed with {{company_name}} from {{joining_date}} to {{leaving_date}}. + +Employment Details: +- Employee ID: {{employee_id}} +- Designation: {{designation}} +- Department: {{department}} +- Employment Type: {{employment_type}} + +During the tenure, {{employee_name}} worked with dedication and professionalism. {{he_she}} demonstrated excellent skills in {{key_skills}} and contributed significantly to {{achievements}}. + +{{employee_name}} is leaving the organization on {{leaving_date}} due to {{reason_for_leaving}}. + +We wish {{him_her}} all the best for future endeavors. + +Issued on: {{issue_date}} + +{{hr_manager_name}} +Human Resources Manager +{{company_name}} +{{company_address}}', + 'placeholders' => ['employee_name', 'company_name', 'joining_date', 'leaving_date', 'employee_id', 'designation', 'department', 'employment_type', 'he_she', 'key_skills', 'achievements', 'reason_for_leaving', 'him_her', 'issue_date', 'hr_manager_name', 'company_address'], + 'default_values' => ['employment_type' => 'Full-time', 'reason_for_leaving' => 'Personal reasons'], + 'is_default' => false, + 'file_format' => 'pdf', + 'status' => 'active' + ], + [ + 'name' => 'Salary Certificate', + 'description' => 'Template for salary certificates required for loan applications and official purposes', + 'category' => 'Financial Documents', + 'template_content' => 'SALARY CERTIFICATE + +To Whom It May Concern, + +This is to certify that {{employee_name}} is employed with {{company_name}} as {{designation}} in the {{department}} department. + +Employment Details: +- Employee ID: {{employee_id}} +- Date of Joining: {{joining_date}} +- Employment Status: {{employment_status}} + +Salary Details: +- Basic Salary: {{basic_salary}} per month +- Allowances: {{allowances}} per month +- Gross Salary: {{gross_salary}} per month +- Annual CTC: {{annual_ctc}} + +This certificate is issued for {{purpose}} as requested by the employee. + +Issued on: {{issue_date}} + +{{hr_manager_name}} +Human Resources Manager +{{company_name}} +{{company_seal}}', + 'placeholders' => ['employee_name', 'company_name', 'designation', 'department', 'employee_id', 'joining_date', 'employment_status', 'basic_salary', 'allowances', 'gross_salary', 'annual_ctc', 'purpose', 'issue_date', 'hr_manager_name', 'company_seal'], + 'default_values' => ['employment_status' => 'Permanent', 'purpose' => 'Official purposes'], + 'is_default' => false, + 'file_format' => 'pdf', + 'status' => 'active' + ], + [ + 'name' => 'Training Certificate', + 'description' => 'Template for training completion certificates and skill development programs', + 'category' => 'Training Certificates', + 'template_content' => 'CERTIFICATE OF COMPLETION + +This is to certify that + +{{participant_name}} + +has successfully completed the training program + +"{{training_title}}" + +Training Details: +- Duration: {{training_duration}} +- Training Period: {{start_date}} to {{end_date}} +- Training Mode: {{training_mode}} +- Trainer: {{trainer_name}} +- Training Hours: {{total_hours}} hours + +Topics Covered: +{{training_topics}} + +Performance: +- Assessment Score: {{assessment_score}}% +- Grade: {{grade}} +- Attendance: {{attendance_percentage}}% + +This certificate is awarded in recognition of successful completion of the training program. + +Issued on: {{issue_date}} + +{{trainer_signature}} +{{trainer_name}} +Training Coordinator + +{{hr_manager_signature}} +{{hr_manager_name}} +Human Resources Manager', + 'placeholders' => ['participant_name', 'training_title', 'training_duration', 'start_date', 'end_date', 'training_mode', 'trainer_name', 'total_hours', 'training_topics', 'assessment_score', 'grade', 'attendance_percentage', 'issue_date', 'trainer_signature', 'trainer_name', 'hr_manager_signature', 'hr_manager_name'], + 'default_values' => ['training_mode' => 'Online', 'grade' => 'Pass'], + 'is_default' => false, + 'file_format' => 'pdf', + 'status' => 'active' + ], + [ + 'name' => 'Performance Appraisal Form', + 'description' => 'Standard template for employee performance evaluation and appraisal documentation', + 'category' => 'Performance Records', + 'template_content' => 'PERFORMANCE APPRAISAL FORM + +Employee Information: +- Name: {{employee_name}} +- Employee ID: {{employee_id}} +- Designation: {{designation}} +- Department: {{department}} +- Appraisal Period: {{appraisal_period}} + +Performance Ratings: +1. Job Knowledge: {{job_knowledge_rating}}/5 +2. Quality of Work: {{quality_rating}}/5 +3. Productivity: {{productivity_rating}}/5 +4. Communication: {{communication_rating}}/5 +5. Teamwork: {{teamwork_rating}}/5 +6. Leadership: {{leadership_rating}}/5 + +Overall Performance: {{overall_rating}}/5 + +Key Achievements: +{{key_achievements}} + +Areas for Improvement: +{{improvement_areas}} + +Goals for Next Period: +{{future_goals}} + +Training Recommendations: +{{training_recommendations}} + +Employee Comments: +{{employee_comments}} + +Appraiser Comments: +{{appraiser_comments}} + +{{employee_signature}} +Employee Signature + +{{appraiser_signature}} +{{appraiser_name}} +Reporting Manager + +Date: {{appraisal_date}}', + 'placeholders' => ['employee_name', 'employee_id', 'designation', 'department', 'appraisal_period', 'job_knowledge_rating', 'quality_rating', 'productivity_rating', 'communication_rating', 'teamwork_rating', 'leadership_rating', 'overall_rating', 'key_achievements', 'improvement_areas', 'future_goals', 'training_recommendations', 'employee_comments', 'appraiser_comments', 'employee_signature', 'appraiser_signature', 'appraiser_name', 'appraisal_date'], + 'default_values' => ['overall_rating' => '3'], + 'is_default' => false, + 'file_format' => 'pdf', + 'status' => 'active' + ], + [ + 'name' => 'Medical Fitness Certificate', + 'description' => 'Template for medical fitness certificates required for employment and health records', + 'category' => 'Medical Records', + 'template_content' => 'MEDICAL FITNESS CERTIFICATE + +Patient Information: +- Name: {{patient_name}} +- Age: {{age}} years +- Gender: {{gender}} +- Employee ID: {{employee_id}} +- Department: {{department}} + +Medical Examination Details: +- Examination Date: {{examination_date}} +- Examining Doctor: {{doctor_name}} +- Medical Registration No: {{doctor_registration}} + +Examination Results: +- Height: {{height}} cm +- Weight: {{weight}} kg +- Blood Pressure: {{blood_pressure}} +- Pulse Rate: {{pulse_rate}} bpm +- Vision: {{vision_status}} +- Hearing: {{hearing_status}} + +Medical History: +{{medical_history}} + +Current Health Status: +{{health_status}} + +Fitness Declaration: +Based on the medical examination, {{patient_name}} is declared MEDICALLY FIT for employment in the position of {{job_position}}. + +Valid Until: {{validity_date}} + +{{doctor_signature}} +Dr. {{doctor_name}} +{{doctor_qualification}} +Medical Officer', + 'placeholders' => ['patient_name', 'age', 'gender', 'employee_id', 'department', 'examination_date', 'doctor_name', 'doctor_registration', 'height', 'weight', 'blood_pressure', 'pulse_rate', 'vision_status', 'hearing_status', 'medical_history', 'health_status', 'job_position', 'validity_date', 'doctor_signature', 'doctor_qualification'], + 'default_values' => ['vision_status' => 'Normal', 'hearing_status' => 'Normal', 'health_status' => 'Good'], + 'is_default' => false, + 'file_format' => 'pdf', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + // Get document categories for this company + $documentCategories = DocumentCategory::where('created_by', $company->id)->get(); + + if ($documentCategories->isEmpty()) { + $this->command->warn('No document categories found for company: ' . $company->name . '. Please run DocumentCategorySeeder first.'); + continue; + } + + foreach ($documentTemplates as $templateData) { + // Find matching category + $category = $documentCategories->where('name', $templateData['category'])->first(); + if (!$category) $category = $documentCategories->first(); + + // Check if template already exists for this company + if (DocumentTemplate::where('name', $templateData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + DocumentTemplate::create([ + 'name' => $templateData['name'], + 'description' => $templateData['description'], + 'category_id' => $category->id, + 'template_content' => $templateData['template_content'], + 'placeholders' => $templateData['placeholders'], + 'default_values' => $templateData['default_values'], + 'is_default' => $templateData['is_default'], + 'file_format' => $templateData['file_format'], + 'status' => $templateData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create document template: ' . $templateData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('DocumentTemplate seeder completed successfully!'); + } +} diff --git a/database/seeders/DocumentTypeSeeder.php b/database/seeders/DocumentTypeSeeder.php new file mode 100644 index 000000000..c2423a34b --- /dev/null +++ b/database/seeders/DocumentTypeSeeder.php @@ -0,0 +1,67 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Document types with realistic data + $documentTypes = [ + ['name' => 'Identity Proof', 'is_required' => true, 'description' => 'Government issued identity document such as passport, driver license, or national ID'], + ['name' => 'Address Proof', 'is_required' => true, 'description' => 'Document verifying current residential address like utility bill or bank statement'], + ['name' => 'Educational Certificates', 'is_required' => true, 'description' => 'Academic certificates, degrees, diplomas, and transcripts'], + ['name' => 'Experience Letters', 'is_required' => false, 'description' => 'Previous employment experience and recommendation letters from former employers'], + ['name' => 'Medical Certificate', 'is_required' => false, 'description' => 'Health fitness certificate and medical examination reports'], + ['name' => 'Bank Account Details', 'is_required' => true, 'description' => 'Bank account information for salary processing and financial transactions'], + ['name' => 'Tax Documents', 'is_required' => false, 'description' => 'Tax identification numbers, PAN card, and tax-related documentation'], + ['name' => 'Emergency Contact', 'is_required' => true, 'description' => 'Emergency contact information and relationship details'], + ['name' => 'Passport Size Photo', 'is_required' => true, 'description' => 'Recent passport size photographs for employee identification'], + ['name' => 'Offer Letter', 'is_required' => false, 'description' => 'Job offer letter and employment terms documentation'], + ['name' => 'Non-Disclosure Agreement', 'is_required' => false, 'description' => 'Confidentiality and non-disclosure agreement signed by employee'] + ]; + + foreach ($companies as $company) { + // Create 8-10 document types for each company + $documentCount = rand(4,6); + + for ($i = 0; $i < $documentCount; $i++) { + $documentType = $documentTypes[$i]; + + // Check if document type already exists for this company + if (DocumentType::where('name', $documentType['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + DocumentType::create([ + 'name' => $documentType['name'], + 'is_required' => $documentType['is_required'], + 'description' => $documentType['description'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create document type: ' . $documentType['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('DocumentType seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/EmailTemplateSeeder.php b/database/seeders/EmailTemplateSeeder.php new file mode 100644 index 000000000..ace781b06 --- /dev/null +++ b/database/seeders/EmailTemplateSeeder.php @@ -0,0 +1,1174 @@ +get(); + if($getExtraEmailTemplates->isNotEmpty()){ + foreach($getExtraEmailTemplates as $template){ + $template->delete(); + } + } + + $languages = json_decode(file_get_contents(resource_path('lang/language.json')), true); + $langCodes = collect($languages)->pluck('code')->toArray(); + $fromName = isSaas() ? 'HRM SaaS' : 'HRM'; + + + $templates = [ + [ + 'name' => 'User Created', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Welcome to {app_name} - Account Created', + 'content' => '

Hello {user_name},

Your account has been successfully created on {app_name}.

Login Details:

  • Website: {app_url}
  • Email: {user_email}
  • Password: {user_password}
  • Account Type: {user_type}

Please keep this information secure and change your password after first login.

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Bienvenido a {app_name} - Cuenta Creada', + 'content' => '

Hola {user_name},

Su cuenta ha sido creada exitosamente en {app_name}.

Detalles de acceso:

  • Sitio web: {app_url}
  • Email: {user_email}
  • Contraseña: {user_password}
  • Tipo de cuenta: {user_type}

Por favor mantenga esta información segura y cambie su contraseña después del primer inicio de sesión.

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'مرحباً بك في {app_name} - تم إنشاء الحساب', + 'content' => '

مرحباً {user_name}،

تم إنشاء حسابك بنجاح على {app_name}.

تفاصيل تسجيل الدخول:

  • الموقع: {app_url}
  • البريد الإلكتروني: {user_email}
  • كلمة المرور: {user_password}
  • نوع الحساب: {user_type}

يرجى الحفاظ على هذه المعلومات آمنة وتغيير كلمة المرور بعد أول تسجيل دخول.

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Velkommen til {app_name} - Konto Oprettet', + 'content' => '

Hej {user_name},

Din konto er blevet oprettet på {app_name}.

Login detaljer:

  • Hjemmeside: {app_url}
  • Email: {user_email}
  • Adgangskode: {user_password}
  • Kontotype: {user_type}

Hold venligst disse oplysninger sikre og skift din adgangskode efter første login.

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Willkommen bei {app_name} - Konto Erstellt', + 'content' => '

Hallo {user_name},

Ihr Konto wurde erfolgreich auf {app_name} erstellt.

Anmeldedaten:

  • Website: {app_url}
  • E-Mail: {user_email}
  • Passwort: {user_password}
  • Kontotyp: {user_type}

Bitte bewahren Sie diese Informationen sicher auf und ändern Sie Ihr Passwort nach der ersten Anmeldung.

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Bienvenue sur {app_name} - Compte Créé', + 'content' => '

Bonjour {user_name},

Votre compte a été créé avec succès sur {app_name}.

Détails de connexion:

  • Site web: {app_url}
  • Email: {user_email}
  • Mot de passe: {user_password}
  • Type de compte: {user_type}

Veuillez garder ces informations en sécurité et changer votre mot de passe après la première connexion.

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'ברוכים הבאים ל-{app_name} - חשבון נוצר', + 'content' => '

שלום {user_name},

החשבון שלך נוצר בהצלחה ב-{app_name}.

פרטי התחברות:

  • אתר: {app_url}
  • אימייל: {user_email}
  • סיסמה: {user_password}
  • סוג חשבון: {user_type}

אנא שמור על מידע זה מאובטח ושנה את הסיסמה שלך לאחר הכניסה הראשונה.

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Benvenuto su {app_name} - Account Creato', + 'content' => '

Ciao {user_name},

Il tuo account è stato creato con successo su {app_name}.

Dettagli di accesso:

  • Sito web: {app_url}
  • Email: {user_email}
  • Password: {user_password}
  • Tipo di account: {user_type}

Si prega di mantenere queste informazioni al sicuro e cambiare la password dopo il primo accesso.

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '{app_name}へようこそ - アカウント作成完了', + 'content' => '

こんにちは {user_name}様、

{app_name}でアカウントが正常に作成されました。

ログイン情報:

  • ウェブサイト: {app_url}
  • メール: {user_email}
  • パスワード: {user_password}
  • アカウントタイプ: {user_type}

この情報を安全に保管し、初回ログイン後にパスワードを変更してください。

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Welkom bij {app_name} - Account Aangemaakt', + 'content' => '

Hallo {user_name},

Uw account is succesvol aangemaakt op {app_name}.

Inloggegevens:

  • Website: {app_url}
  • Email: {user_email}
  • Wachtwoord: {user_password}
  • Accounttype: {user_type}

Bewaar deze informatie veilig en wijzig uw wachtwoord na de eerste login.

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Witamy w {app_name} - Konto Utworzone', + 'content' => '

Witaj {user_name},

Twoje konto zostało pomyślnie utworzone w {app_name}.

Dane logowania:

  • Strona: {app_url}
  • Email: {user_email}
  • Hasło: {user_password}
  • Typ konta: {user_type}

Prosimy o bezpieczne przechowywanie tych informacji i zmianę hasła po pierwszym logowaniu.

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Bem-vindo ao {app_name} - Conta Criada', + 'content' => '

Olá {user_name},

Sua conta foi criada com sucesso no {app_name}.

Detalhes de login:

  • Website: {app_url}
  • Email: {user_email}
  • Senha: {user_password}
  • Tipo de conta: {user_type}

Por favor, mantenha essas informações seguras e altere sua senha após o primeiro login.

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Bem-vindo ao {app_name} - Conta Criada', + 'content' => '

Olá {user_name},

Sua conta foi criada com sucesso no {app_name}.

Detalhes de login:

  • Website: {app_url}
  • Email: {user_email}
  • Senha: {user_password}
  • Tipo de conta: {user_type}

Por favor, mantenha essas informações seguras e altere sua senha após o primeiro login.

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Добро пожаловать в {app_name} - Аккаунт Создан', + 'content' => '

Здравствуйте {user_name},

Ваш аккаунт успешно создан в {app_name}.

Данные для входа:

  • Сайт: {app_url}
  • Email: {user_email}
  • Пароль: {user_password}
  • Тип аккаунта: {user_type}

Пожалуйста, храните эту информацию в безопасности и измените пароль после первого входа.

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => '{app_name} Hoş Geldiniz - Hesap Oluşturuldu', + 'content' => '

Merhaba {user_name},

Hesabınız {app_name} üzerinde başarıyla oluşturuldu.

Giriş Bilgileri:

  • Website: {app_url}
  • Email: {user_email}
  • Şifre: {user_password}
  • Hesap Türü: {user_type}

Lütfen bu bilgileri güvenli tutun ve ilk girişten sonra şifrenizi değiştirin.

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '欢迎来到 {app_name} - 账户已创建', + 'content' => '

您好 {user_name}

您的账户已在 {app_name} 成功创建。

登录详情:

  • 网站:{app_url}
  • 邮箱:{user_email}
  • 密码:{user_password}
  • 账户类型:{user_type}

请妥善保管这些信息,并在首次登录后更改密码。

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'Employee Created', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Welcome to {app_name} - Employee Account Created', + 'content' => '

Hello {employee_name},

Your employee account has been successfully created on {app_name}.

Login Details:

  • Website: {app_url}
  • Email: {employee_email}
  • Password: {employee_password}
  • Employee ID: {employee_id}
  • Department: {department_name}
  • Designation: {designation_name}
  • Joining Date: {joining_date}

Please keep this information secure and change your password after first login.

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Bienvenido a {app_name} - Cuenta de Empleado Creada', + 'content' => '

Hola {employee_name},

Su cuenta de empleado ha sido creada exitosamente en {app_name}.

Detalles de acceso:

  • Sitio web: {app_url}
  • Email: {employee_email}
  • Contraseña: {employee_password}
  • ID de Empleado: {employee_id}
  • Departamento: {department_name}
  • Designación: {designation_name}
  • Fecha de Ingreso: {joining_date}

Por favor mantenga esta información segura y cambie su contraseña después del primer inicio de sesión.

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'مرحباً بك في {app_name} - تم إنشاء حساب الموظف', + 'content' => '

مرحباً {employee_name}،

تم إنشاء حساب الموظف الخاص بك بنجاح على {app_name}.

تفاصيل تسجيل الدخول:

  • الموقع: {app_url}
  • البريد الإلكتروني: {employee_email}
  • كلمة المرور: {employee_password}
  • رقم الموظف: {employee_id}
  • القسم: {department_name}
  • المسمى الوظيفي: {designation_name}
  • تاريخ الانضمام: {joining_date}

يرجى الحفاظ على هذه المعلومات آمنة وتغيير كلمة المرور بعد أول تسجيل دخول.

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Velkommen til {app_name} - Medarbejderkonto Oprettet', + 'content' => '

Hej {employee_name},

Din medarbejderkonto er blevet oprettet på {app_name}.

Login detaljer:

  • Hjemmeside: {app_url}
  • Email: {employee_email}
  • Adgangskode: {employee_password}
  • Medarbejder ID: {employee_id}
  • Afdeling: {department_name}
  • Betegnelse: {designation_name}
  • Tiltrædelsesdato: {joining_date}

Hold venligst disse oplysninger sikre og skift din adgangskode efter første login.

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Willkommen bei {app_name} - Mitarbeiterkonto Erstellt', + 'content' => '

Hallo {employee_name},

Ihr Mitarbeiterkonto wurde erfolgreich auf {app_name} erstellt.

Anmeldedaten:

  • Website: {app_url}
  • E-Mail: {employee_email}
  • Passwort: {employee_password}
  • Mitarbeiter-ID: {employee_id}
  • Abteilung: {department_name}
  • Bezeichnung: {designation_name}
  • Eintrittsdatum: {joining_date}

Bitte bewahren Sie diese Informationen sicher auf und ändern Sie Ihr Passwort nach der ersten Anmeldung.

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Bienvenue sur {app_name} - Compte Employé Créé', + 'content' => '

Bonjour {employee_name},

Votre compte employé a été créé avec succès sur {app_name}.

Détails de connexion:

  • Site web: {app_url}
  • Email: {employee_email}
  • Mot de passe: {employee_password}
  • ID Employé: {employee_id}
  • Département: {department_name}
  • Désignation: {designation_name}
  • Date d\'adhésion: {joining_date}

Veuillez garder ces informations en sécurité et changer votre mot de passe après la première connexion.

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'ברוכים הבאים ל-{app_name} - חשבון עובד נוצר', + 'content' => '

שלום {employee_name},

חשבון העובד שלך נוצר בהצלחה ב-{app_name}.

פרטי התחברות:

  • אתר: {app_url}
  • אימייל: {employee_email}
  • סיסמה: {employee_password}
  • מזהה עובד: {employee_id}
  • מחלקה: {department_name}
  • תפקיד: {designation_name}
  • תאריך הצטרפות: {joining_date}

אנא שמור על מידע זה מאובטח ושנה את הסיסמה שלך לאחר הכניסה הראשונה.

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Benvenuto su {app_name} - Account Dipendente Creato', + 'content' => '

Ciao {employee_name},

Il tuo account dipendente è stato creato con successo su {app_name}.

Dettagli di accesso:

  • Sito web: {app_url}
  • Email: {employee_email}
  • Password: {employee_password}
  • ID Dipendente: {employee_id}
  • Dipartimento: {department_name}
  • Designazione: {designation_name}
  • Data di Assunzione: {joining_date}

Si prega di mantenere queste informazioni al sicuro e cambiare la password dopo il primo accesso.

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '{app_name}へようこそ - 従業員アカウント作成完了', + 'content' => '

こんにちは {employee_name}様、

{app_name}で従業員アカウントが正常に作成されました。

ログイン情報:

  • ウェブサイト: {app_url}
  • メール: {employee_email}
  • パスワード: {employee_password}
  • 従業員ID: {employee_id}
  • 部門: {department_name}
  • 役職: {designation_name}
  • 入社日: {joining_date}

この情報を安全に保管し、初回ログイン後にパスワードを変更してください。

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Welkom bij {app_name} - Werknemersaccount Aangemaakt', + 'content' => '

Hallo {employee_name},

Uw werknemersaccount is succesvol aangemaakt op {app_name}.

Inloggegevens:

  • Website: {app_url}
  • Email: {employee_email}
  • Wachtwoord: {employee_password}
  • Werknemer ID: {employee_id}
  • Afdeling: {department_name}
  • Aanduiding: {designation_name}
  • Indiensttreding: {joining_date}

Bewaar deze informatie veilig en wijzig uw wachtwoord na de eerste login.

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Witamy w {app_name} - Konto Pracownika Utworzone', + 'content' => '

Witaj {employee_name},

Twoje konto pracownika zostało pomyślnie utworzone w {app_name}.

Dane logowania:

  • Strona: {app_url}
  • Email: {employee_email}
  • Hasło: {employee_password}
  • ID Pracownika: {employee_id}
  • Dział: {department_name}
  • Stanowisko: {designation_name}
  • Data Zatrudnienia: {joining_date}

Prosimy o bezpieczne przechowywanie tych informacji i zmianę hasła po pierwszym logowaniu.

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Bem-vindo ao {app_name} - Conta de Funcionário Criada', + 'content' => '

Olá {employee_name},

Sua conta de funcionário foi criada com sucesso no {app_name}.

Detalhes de login:

  • Website: {app_url}
  • Email: {employee_email}
  • Senha: {employee_password}
  • ID do Funcionário: {employee_id}
  • Departamento: {department_name}
  • Designação: {designation_name}
  • Data de Admissão: {joining_date}

Por favor, mantenha essas informações seguras e altere sua senha após o primeiro login.

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Bem-vindo ao {app_name} - Conta de Funcionário Criada', + 'content' => '

Olá {employee_name},

Sua conta de funcionário foi criada com sucesso no {app_name}.

Detalhes de login:

  • Website: {app_url}
  • Email: {employee_email}
  • Senha: {employee_password}
  • ID do Funcionário: {employee_id}
  • Departamento: {department_name}
  • Designação: {designation_name}
  • Data de Admissão: {joining_date}

Por favor, mantenha essas informações seguras e altere sua senha após o primeiro login.

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Добро пожаловать в {app_name} - Аккаунт Сотрудника Создан', + 'content' => '

Здравствуйте {employee_name},

Ваш аккаунт сотрудника успешно создан в {app_name}.

Данные для входа:

  • Сайт: {app_url}
  • Email: {employee_email}
  • Пароль: {employee_password}
  • ID Сотрудника: {employee_id}
  • Отдел: {department_name}
  • Должность: {designation_name}
  • Дата Приема: {joining_date}

Пожалуйста, храните эту информацию в безопасности и измените пароль после первого входа.

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => '{app_name} Hoş Geldiniz - Çalışan Hesabı Oluşturuldu', + 'content' => '

Merhaba {employee_name},

Çalışan hesabınız {app_name} üzerinde başarıyla oluşturuldu.

Giriş Bilgileri:

  • Website: {app_url}
  • Email: {employee_email}
  • Şifre: {employee_password}
  • Çalışan ID: {employee_id}
  • Departman: {department_name}
  • Unvan: {designation_name}
  • İşe Başlama Tarihi: {joining_date}

Lütfen bu bilgileri güvenli tutun ve ilk girişten sonra şifrenizi değiştirin.

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '欢迎来到 {app_name} - 员工账户已创建', + 'content' => '

您好 {employee_name}

您的员工账户已在 {app_name} 成功创建。

登录详情:

  • 网站:{app_url}
  • 邮箱:{employee_email}
  • 密码:{employee_password}
  • 员工ID:{employee_id}
  • 部门:{department_name}
  • 职位:{designation_name}
  • 入职日期:{joining_date}

请妥善保管这些信息,并在首次登录后更改密码。

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'New Award', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Congratulations! You Have Received an Award - {award_type}', + 'content' => '

Dear {employee_name},

Congratulations! We are pleased to inform you that you have been awarded the {award_type}.

Award Details:

  • Award Type: {award_type}
  • Award Date: {award_date}
  • Description: {description}

Your dedication and hard work have been recognized and appreciated. Keep up the excellent work!

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => '¡Felicitaciones! Has Recibido un Premio - {award_type}', + 'content' => '

Estimado/a {employee_name},

¡Felicitaciones! Nos complace informarle que ha sido galardonado con {award_type}.

Detalles del Premio:

  • Tipo de Premio: {award_type}
  • Fecha del Premio: {award_date}
  • Descripción: {description}

Su dedicación y arduo trabajo han sido reconocidos y apreciados. ¡Siga con el excelente trabajo!

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'تهانينا! لقد حصلت على جائزة - {award_type}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

تهانينا! يسعدنا إبلاغك بأنك حصلت على {award_type}.

تفاصيل الجائزة:

  • نوع الجائزة: {award_type}
  • تاريخ الجائزة: {award_date}
  • الوصف: {description}

لقد تم الاعتراف بتفانيك وعملك الجاد وتقديره. استمر في العمل الممتاز!

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Tillykke! Du Har Modtaget en Pris - {award_type}', + 'content' => '

Kære {employee_name},

Tillykke! Vi er glade for at informere dig om, at du er blevet tildelt {award_type}.

Prisdetaljer:

  • Pristype: {award_type}
  • Prisdato: {award_date}
  • Beskrivelse: {description}

Din dedikation og hårde arbejde er blevet anerkendt og værdsat. Fortsæt det fremragende arbejde!

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Herzlichen Glückwunsch! Sie Haben eine Auszeichnung Erhalten - {award_type}', + 'content' => '

Liebe/r {employee_name},

Herzlichen Glückwunsch! Wir freuen uns, Ihnen mitteilen zu können, dass Sie mit {award_type} ausgezeichnet wurden.

Auszeichnungsdetails:

  • Auszeichnungstyp: {award_type}
  • Auszeichnungsdatum: {award_date}
  • Beschreibung: {description}

Ihr Engagement und Ihre harte Arbeit wurden anerkannt und geschätzt. Machen Sie weiter so!

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Félicitations! Vous Avez Reçu un Prix - {award_type}', + 'content' => '

Cher/Chère {employee_name},

Félicitations! Nous sommes heureux de vous informer que vous avez reçu le prix {award_type}.

Détails du Prix:

  • Type de Prix: {award_type}
  • Date du Prix: {award_date}
  • Description: {description}

Votre dévouement et votre travail acharné ont été reconnus et appréciés. Continuez votre excellent travail!

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'מזל טוב! קיבלת פרס - {award_type}', + 'content' => '

יקר/ה {employee_name},

מזל טוב! אנו שמחים להודיע לך שקיבלת את {award_type}.

פרטי הפרס:

  • סוג הפרס: {award_type}
  • תאריך הפרס: {award_date}
  • תיאור: {description}

המסירות והעבודה הקשה שלך הוכרו והוערכו. המשך בעבודה המצוינת!

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Congratulazioni! Hai Ricevuto un Premio - {award_type}', + 'content' => '

Caro/a {employee_name},

Congratulazioni! Siamo lieti di informarti che hai ricevuto il premio {award_type}.

Dettagli del Premio:

  • Tipo di Premio: {award_type}
  • Data del Premio: {award_date}
  • Descrizione: {description}

La tua dedizione e il tuo duro lavoro sono stati riconosciuti e apprezzati. Continua così!

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => 'おめでとうございます!賞を受賞されました - {award_type}', + 'content' => '

{employee_name}様、

おめでとうございます!{award_type}を受賞されたことをお知らせいたします。

賞の詳細:

  • 賞のタイプ: {award_type}
  • 授賞日: {award_date}
  • 説明: {description}

あなたの献身と努力が認められ、評価されました。素晴らしい仕事を続けてください!

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Gefeliciteerd! Je Hebt een Prijs Ontvangen - {award_type}', + 'content' => '

Beste {employee_name},

Gefeliciteerd! We zijn verheugd je te informeren dat je de {award_type} hebt ontvangen.

Prijsdetails:

  • Prijstype: {award_type}
  • Prijsdatum: {award_date}
  • Beschrijving: {description}

Je toewijding en harde werk zijn erkend en gewaardeerd. Ga zo door!

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Gratulacje! Otrzymałeś Nagrodę - {award_type}', + 'content' => '

Drogi/a {employee_name},

Gratulacje! Z przyjemnością informujemy, że otrzymałeś nagrodę {award_type}.

Szczegóły Nagrody:

  • Typ Nagrody: {award_type}
  • Data Nagrody: {award_date}
  • Opis: {description}

Twoje zaangażowanie i ciężka praca zostały docenione. Tak trzymaj!

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Parabéns! Você Recebeu um Prêmio - {award_type}', + 'content' => '

Caro/a {employee_name},

Parabéns! Temos o prazer de informar que você recebeu o prêmio {award_type}.

Detalhes do Prêmio:

  • Tipo de Prêmio: {award_type}
  • Data do Prêmio: {award_date}
  • Descrição: {description}

Sua dedicação e trabalho árduo foram reconhecidos e apreciados. Continue com o excelente trabalho!

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Parabéns! Você Recebeu um Prêmio - {award_type}', + 'content' => '

Caro/a {employee_name},

Parabéns! Temos o prazer de informar que você recebeu o prêmio {award_type}.

Detalhes do Prêmio:

  • Tipo de Prêmio: {award_type}
  • Data do Prêmio: {award_date}
  • Descrição: {description}

Sua dedicação e trabalho árduo foram reconhecidos e apreciados. Continue com o excelente trabalho!

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Поздравляем! Вы Получили Награду - {award_type}', + 'content' => '

Уважаемый/ая {employee_name},

Поздравляем! Мы рады сообщить вам, что вы получили награду {award_type}.

Детали Награды:

  • Тип Награды: {award_type}
  • Дата Награды: {award_date}
  • Описание: {description}

Ваша преданность и упорный труд были признаны и оценены. Продолжайте в том же духе!

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => 'Tebrikler! Bir Ödül Aldınız - {award_type}', + 'content' => '

Sayın {employee_name},

Tebrikler! {award_type} ödülünü aldığınızı bildirmekten mutluluk duyuyoruz.

Ödül Detayları:

  • Ödül Türü: {award_type}
  • Ödül Tarihi: {award_date}
  • Açıklama: {description}

Özveriniz ve sıkı çalışmanız takdir edildi. Mükemmel çalışmalarınıza devam edin!

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '恭喜!您获得了奖项 - {award_type}', + 'content' => '

尊敬的 {employee_name}

恭喜!我们很高兴地通知您,您已获得 {award_type} 奖项。

奖项详情:

  • 奖项类型:{award_type}
  • 颁奖日期:{award_date}
  • 描述:{description}

您的奉献和辛勤工作得到了认可和赞赏。继续保持出色的工作!

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'Employee Promotion', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Congratulations on Your Promotion - {designation_name}', + 'content' => '

Dear {employee_name},

We are delighted to inform you that you have been promoted to the position of {designation_name}.

Promotion Details:

  • Previous Designation: {previous_designation}
  • New Designation: {designation_name}
  • Promotion Date: {promotion_date}
  • Effective Date: {effective_date}
  • Reason: {reason}

This promotion is a recognition of your hard work, dedication, and outstanding contributions to the organization. We are confident that you will excel in your new role.

Congratulations once again!

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Felicitaciones por su Promoción - {designation_name}', + 'content' => '

Estimado/a {employee_name},

Nos complace informarle que ha sido promovido/a al puesto de {designation_name}.

Detalles de la Promoción:

  • Designación Anterior: {previous_designation}
  • Nueva Designación: {designation_name}
  • Fecha de Promoción: {promotion_date}
  • Fecha Efectiva: {effective_date}
  • Motivo: {reason}

Esta promoción es un reconocimiento a su arduo trabajo, dedicación y contribuciones sobresalientes a la organización. Estamos seguros de que sobresaldrá en su nuevo rol.

¡Felicitaciones una vez más!

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'تهانينا على ترقيتك - {designation_name}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

يسعدنا إبلاغك بأنك تمت ترقيتك إلى منصب {designation_name}.

تفاصيل الترقية:

  • المسمى الوظيفي السابق: {previous_designation}
  • المسمى الوظيفي الجديد: {designation_name}
  • تاريخ الترقية: {promotion_date}
  • تاريخ السريان: {effective_date}
  • السبب: {reason}

هذه الترقية هي اعتراف بعملك الجاد وتفانيك ومساهماتك المتميزة في المنظمة. نحن واثقون من أنك ستتفوق في دورك الجديد.

تهانينا مرة أخرى!

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Tillykke med Din Forfremmelse - {designation_name}', + 'content' => '

Kære {employee_name},

Vi er glade for at informere dig om, at du er blevet forfremmet til stillingen som {designation_name}.

Forfremmelsesdetaljer:

  • Tidligere Betegnelse: {previous_designation}
  • Ny Betegnelse: {designation_name}
  • Forfremmelsesdato: {promotion_date}
  • Ikrafttrædelsesdato: {effective_date}
  • Årsag: {reason}

Denne forfremmelse er en anerkendelse af dit hårde arbejde, dedikation og fremragende bidrag til organisationen. Vi er sikre på, at du vil udmærke dig i din nye rolle.

Tillykke endnu en gang!

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Herzlichen Glückwunsch zu Ihrer Beförderung - {designation_name}', + 'content' => '

Liebe/r {employee_name},

Wir freuen uns, Ihnen mitteilen zu können, dass Sie zur Position {designation_name} befördert wurden.

Beförderungsdetails:

  • Vorherige Bezeichnung: {previous_designation}
  • Neue Bezeichnung: {designation_name}
  • Beförderungsdatum: {promotion_date}
  • Gültigkeitsdatum: {effective_date}
  • Grund: {reason}

Diese Beförderung ist eine Anerkennung Ihrer harten Arbeit, Ihres Engagements und Ihrer herausragenden Beiträge zur Organisation. Wir sind zuversichtlich, dass Sie in Ihrer neuen Rolle hervorragende Leistungen erbringen werden.

Nochmals herzlichen Glückwunsch!

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Félicitations pour Votre Promotion - {designation_name}', + 'content' => '

Cher/Chère {employee_name},

Nous sommes ravis de vous informer que vous avez été promu(e) au poste de {designation_name}.

Détails de la Promotion:

  • Désignation Précédente: {previous_designation}
  • Nouvelle Désignation: {designation_name}
  • Date de Promotion: {promotion_date}
  • Date d\'Effet: {effective_date}
  • Raison: {reason}

Cette promotion est une reconnaissance de votre travail acharné, de votre dévouement et de vos contributions exceptionnelles à l\'organisation. Nous sommes convaincus que vous excellerez dans votre nouveau rôle.

Félicitations encore une fois!

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'מזל טוב על הקידום שלך - {designation_name}', + 'content' => '

יקר/ה {employee_name},

אנו שמחים להודיע לך שקודמת לתפקיד {designation_name}.

פרטי הקידום:

  • תפקיד קודם: {previous_designation}
  • תפקיד חדש: {designation_name}
  • תאריך קידום: {promotion_date}
  • תאריך תוקף: {effective_date}
  • סיבה: {reason}

קידום זה הוא הכרה בעבודה הקשה שלך, במסירות ובתרומות המצוינות שלך לארגון. אנו בטוחים שתצטיין בתפקידך החדש.

מזל טוב שוב!

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Congratulazioni per la Tua Promozione - {designation_name}', + 'content' => '

Caro/a {employee_name},

Siamo lieti di informarti che sei stato/a promosso/a alla posizione di {designation_name}.

Dettagli della Promozione:

  • Designazione Precedente: {previous_designation}
  • Nuova Designazione: {designation_name}
  • Data di Promozione: {promotion_date}
  • Data Effettiva: {effective_date}
  • Motivo: {reason}

Questa promozione è un riconoscimento del tuo duro lavoro, dedizione e contributi eccezionali all\'organizzazione. Siamo sicuri che eccellerai nel tuo nuovo ruolo.

Congratulazioni ancora!

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '昇進おめでとうございます - {designation_name}', + 'content' => '

{employee_name}様、

{designation_name}の役職に昇進されたことをお知らせいたします。

昇進の詳細:

  • 以前の役職: {previous_designation}
  • 新しい役職: {designation_name}
  • 昇進日: {promotion_date}
  • 発効日: {effective_date}
  • 理由: {reason}

この昇進は、あなたの勤勉さ、献身、そして組織への優れた貢献の認識です。新しい役割で優れた成果を上げられることを確信しています。

改めておめでとうございます!

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Gefeliciteerd met Je Promotie - {designation_name}', + 'content' => '

Beste {employee_name},

We zijn verheugd je te informeren dat je bent gepromoveerd tot de functie van {designation_name}.

Promotiedetails:

  • Vorige Aanduiding: {previous_designation}
  • Nieuwe Aanduiding: {designation_name}
  • Promotiedatum: {promotion_date}
  • Ingangsdatum: {effective_date}
  • Reden: {reason}

Deze promotie is een erkenning van je harde werk, toewijding en uitstekende bijdragen aan de organisatie. We zijn ervan overtuigd dat je zult uitblinken in je nieuwe rol.

Nogmaals gefeliciteerd!

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Gratulacje z Okazji Awansu - {designation_name}', + 'content' => '

Drogi/a {employee_name},

Z przyjemnością informujemy, że zostałeś/aś awansowany/a na stanowisko {designation_name}.

Szczegóły Awansu:

  • Poprzednie Stanowisko: {previous_designation}
  • Nowe Stanowisko: {designation_name}
  • Data Awansu: {promotion_date}
  • Data Obowiązywania: {effective_date}
  • Powód: {reason}

Ten awans jest uznaniem Twojej ciężkiej pracy, zaangażowania i wybitnych wkładów w organizację. Jesteśmy przekonani, że będziesz się wyróżniać w swojej nowej roli.

Gratulacje jeszcze raz!

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Parabéns pela Sua Promoção - {designation_name}', + 'content' => '

Caro/a {employee_name},

Temos o prazer de informar que você foi promovido/a para o cargo de {designation_name}.

Detalhes da Promoção:

  • Designação Anterior: {previous_designation}
  • Nova Designação: {designation_name}
  • Data da Promoção: {promotion_date}
  • Data Efetiva: {effective_date}
  • Motivo: {reason}

Esta promoção é um reconhecimento do seu trabalho árduo, dedicação e contribuições excepcionais para a organização. Estamos confiantes de que você se destacará em seu novo papel.

Parabéns mais uma vez!

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Parabéns pela Sua Promoção - {designation_name}', + 'content' => '

Caro/a {employee_name},

Temos o prazer de informar que você foi promovido/a para o cargo de {designation_name}.

Detalhes da Promoção:

  • Designação Anterior: {previous_designation}
  • Nova Designação: {designation_name}
  • Data da Promoção: {promotion_date}
  • Data Efetiva: {effective_date}
  • Motivo: {reason}

Esta promoção é um reconhecimento do seu trabalho árduo, dedicação e contribuições excepcionais para a organização. Estamos confiantes de que você se destacará em seu novo papel.

Parabéns mais uma vez!

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Поздравляем с Повышением - {designation_name}', + 'content' => '

Уважаемый/ая {employee_name},

Мы рады сообщить вам, что вы были повышены до должности {designation_name}.

Детали Повышения:

  • Предыдущая Должность: {previous_designation}
  • Новая Должность: {designation_name}
  • Дата Повышения: {promotion_date}
  • Дата Вступления в Силу: {effective_date}
  • Причина: {reason}

Это повышение является признанием вашего упорного труда, преданности и выдающегося вклада в организацию. Мы уверены, что вы преуспеете в своей новой роли.

Поздравляем еще раз!

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => 'Terfi Ettiğiniz İçin Tebrikler - {designation_name}', + 'content' => '

Sayın {employee_name},

{designation_name} pozisyonuna terfi ettiğinizi bildirmekten mutluluk duyuyoruz.

Terfi Detayları:

  • Önceki Unvan: {previous_designation}
  • Yeni Unvan: {designation_name}
  • Terfi Tarihi: {promotion_date}
  • Geçerlilik Tarihi: {effective_date}
  • Sebep: {reason}

Bu terfi, sıkı çalışmanızın, özverinizin ve organizasyona olağanüstü katkılarınızın bir takdiridir. Yeni rolünüzde başarılı olacağınızdan eminiz.

Bir kez daha tebrikler!

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '恭喜您获得晋升 - {designation_name}', + 'content' => '

尊敬的 {employee_name}

我们很高兴地通知您,您已被晋升为 {designation_name} 职位。

晋升详情:

  • 以前职位:{previous_designation}
  • 新职位:{designation_name}
  • 晋升日期:{promotion_date}
  • 生效日期:{effective_date}
  • 原因:{reason}

此次晋升是对您辛勤工作、奉献精神和对组织杰出贡献的认可。我们相信您将在新角色中表现出色。

再次祝贺!

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'Employee Resignation', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Resignation Letter - {employee_name}', + 'content' => '

Dear Sir/Madam,

I am writing to formally notify you of my resignation from my position at {app_name}, effective {resignation_date}.

Resignation Details:

  • Resignation Date: {resignation_date}
  • Reason: {reason}

I would like to thank you for the opportunities and experiences I have gained during my time with the organization. I am committed to ensuring a smooth transition of my responsibilities.

Thank you for your understanding.

Sincerely,
{employee_name}

', + ], + 'es' => [ + 'subject' => 'Carta de Renuncia - {employee_name}', + 'content' => '

Estimado Señor/Señora,

Le escribo para notificarle formalmente mi renuncia a mi puesto en {app_name}, efectiva el {resignation_date}.

Detalles de la Renuncia:

  • Fecha de Renuncia: {resignation_date}
  • Motivo: {reason}

Me gustaría agradecerle por las oportunidades y experiencias que he obtenido durante mi tiempo en la organización. Estoy comprometido a garantizar una transición fluida de mis responsabilidades.

Gracias por su comprensión.

Atentamente,
{employee_name}

', + ], + 'ar' => [ + 'subject' => 'خطاب استقالة - {employee_name}', + 'content' => '

عزيزي السيد/السيدة،

أكتب إليكم لإبلاغكم رسميًا باستقالتي من منصبي في {app_name}، اعتبارًا من {resignation_date}.

تفاصيل الاستقالة:

  • تاريخ الاستقالة: {resignation_date}
  • السبب: {reason}

أود أن أشكركم على الفرص والخبرات التي اكتسبتها خلال فترة عملي في المنظمة. أنا ملتزم بضمان انتقال سلس لمسؤولياتي.

شكرًا لتفهمكم.

مع التقدير،
{employee_name}

', + ], + 'da' => [ + 'subject' => 'Opsigelse - {employee_name}', + 'content' => '

Kære Hr./Fru,

Jeg skriver for formelt at meddele dig om min opsigelse fra min stilling hos {app_name}, gældende fra {resignation_date}.

Opsigelsesdetaljer:

  • Opsigelsesdato: {resignation_date}
  • Årsag: {reason}

Jeg vil gerne takke dig for de muligheder og erfaringer, jeg har fået i min tid i organisationen. Jeg er forpligtet til at sikre en problemfri overgang af mine ansvarsområder.

Tak for din forståelse.

Med venlig hilsen,
{employee_name}

', + ], + 'de' => [ + 'subject' => 'Kündigungsschreiben - {employee_name}', + 'content' => '

Sehr geehrte Damen und Herren,

Ich schreibe Ihnen, um Sie formell über meine Kündigung meiner Position bei {app_name} zu informieren, wirksam ab {resignation_date}.

Kündigungsdetails:

  • Kündigungsdatum: {resignation_date}
  • Grund: {reason}

Ich möchte mich für die Möglichkeiten und Erfahrungen bedanken, die ich während meiner Zeit in der Organisation gesammelt habe. Ich bin bestrebt, einen reibungslosen Übergang meiner Verantwortlichkeiten zu gewährleisten.

Vielen Dank für Ihr Verständnis.

Mit freundlichen Grüßen,
{employee_name}

', + ], + 'fr' => [ + 'subject' => 'Lettre de Démission - {employee_name}', + 'content' => '

Madame, Monsieur,

Je vous écris pour vous informer formellement de ma démission de mon poste chez {app_name}, effective le {resignation_date}.

Détails de la Démission:

  • Date de Démission: {resignation_date}
  • Raison: {reason}

Je tiens à vous remercier pour les opportunités et les expériences que j\'ai acquises pendant mon temps au sein de l\'organisation. Je m\'engage à assurer une transition en douceur de mes responsabilités.

Merci de votre compréhension.

Cordialement,
{employee_name}

', + ], + 'he' => [ + 'subject' => 'מכתב התפטרות - {employee_name}', + 'content' => '

אדון/גברת נכבדים,

אני כותב כדי להודיע לכם רשמית על התפטרותי מתפקידי ב-{app_name}, בתוקף מ-{resignation_date}.

פרטי ההתפטרות:

  • תאריך התפטרות: {resignation_date}
  • סיבה: {reason}

אני רוצה להודות לכם על ההזדמנויות והחוויות שצברתי במהלך תקופתי בארגון. אני מחויב להבטיח מעבר חלק של האחריות שלי.

תודה על ההבנה.

בכבוד רב,
{employee_name}

', + ], + 'it' => [ + 'subject' => 'Lettera di Dimissioni - {employee_name}', + 'content' => '

Gentile Signore/Signora,

Le scrivo per notificarLe formalmente le mie dimissioni dalla mia posizione presso {app_name}, con effetto dal {resignation_date}.

Dettagli delle Dimissioni:

  • Data di Dimissioni: {resignation_date}
  • Motivo: {reason}

Vorrei ringraziarLa per le opportunità e le esperienze che ho acquisito durante il mio tempo nell\'organizzazione. Mi impegno a garantire una transizione fluida delle mie responsabilità.

Grazie per la comprensione.

Cordiali saluti,
{employee_name}

', + ], + 'ja' => [ + 'subject' => '退職届 - {employee_name}', + 'content' => '

拝啓

{app_name}での私の職を{resignation_date}付けで辞職することを正式にお知らせいたします。

退職の詳細:

  • 退職日: {resignation_date}
  • 理由: {reason}

組織での期間中に得た機会と経験に感謝いたします。私の責任のスムーズな引き継ぎを確実にすることをお約束します。

ご理解いただきありがとうございます。

敬具
{employee_name}

', + ], + 'nl' => [ + 'subject' => 'Ontslagbrief - {employee_name}', + 'content' => '

Geachte heer/mevrouw,

Ik schrijf u om u formeel op de hoogte te stellen van mijn ontslag uit mijn functie bij {app_name}, met ingang van {resignation_date}.

Ontslagdetails:

  • Ontslagdatum: {resignation_date}
  • Reden: {reason}

Ik wil u bedanken voor de kansen en ervaringen die ik heb opgedaan tijdens mijn tijd bij de organisatie. Ik ben toegewijd om een soepele overgang van mijn verantwoordelijkheden te waarborgen.

Dank u voor uw begrip.

Met vriendelijke groet,
{employee_name}

', + ], + 'pl' => [ + 'subject' => 'List Rezygnacyjny - {employee_name}', + 'content' => '

Szanowni Państwo,

Piszę, aby formalnie powiadomić Państwa o mojej rezygnacji z mojego stanowiska w {app_name}, ze skutkiem od {resignation_date}.

Szczegóły Rezygnacji:

  • Data Rezygnacji: {resignation_date}
  • Powód: {reason}

Chciałbym podziękować za możliwości i doświadczenia, które zdobyłem podczas mojego czasu w organizacji. Jestem zaangażowany w zapewnienie płynnego przejścia moich obowiązków.

Dziękuję za zrozumienie.

Z poważaniem,
{employee_name}

', + ], + 'pt' => [ + 'subject' => 'Carta de Demissão - {employee_name}', + 'content' => '

Prezado(a) Senhor(a),

Escrevo para notificá-lo formalmente da minha demissão do meu cargo na {app_name}, com efeito a partir de {resignation_date}.

Detalhes da Demissão:

  • Data de Demissão: {resignation_date}
  • Motivo: {reason}

Gostaria de agradecer pelas oportunidades e experiências que obtive durante meu tempo na organização. Estou comprometido em garantir uma transição suave das minhas responsabilidades.

Obrigado pela compreensão.

Atenciosamente,
{employee_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Carta de Demissão - {employee_name}', + 'content' => '

Prezado(a) Senhor(a),

Escrevo para notificá-lo formalmente da minha demissão do meu cargo na {app_name}, com efeito a partir de {resignation_date}.

Detalhes da Demissão:

  • Data de Demissão: {resignation_date}
  • Motivo: {reason}

Gostaria de agradecer pelas oportunidades e experiências que obtive durante meu tempo na organização. Estou comprometido em garantir uma transição suave das minhas responsabilidades.

Obrigado pela compreensão.

Atenciosamente,
{employee_name}

', + ], + 'ru' => [ + 'subject' => 'Заявление об Увольнении - {employee_name}', + 'content' => '

Уважаемый господин/госпожа,

Пишу, чтобы официально уведомить вас о моем увольнении с должности в {app_name}, вступающем в силу с {resignation_date}.

Детали Увольнения:

  • Дата Увольнения: {resignation_date}
  • Причина: {reason}

Я хотел бы поблагодарить вас за возможности и опыт, которые я получил за время работы в организации. Я обязуюсь обеспечить плавную передачу моих обязанностей.

Спасибо за понимание.

С уважением,
{employee_name}

', + ], + 'tr' => [ + 'subject' => 'İstifa Mektubu - {employee_name}', + 'content' => '

Sayın Yetkili,

{app_name} bünyesindeki pozisyonumdan {resignation_date} tarihinden itibaren geçerli olmak üzere istifa ettiğimi resmi olarak bildirmek için yazıyorum.

İstifa Detayları:

  • İstifa Tarihi: {resignation_date}
  • Sebep: {reason}

Organizasyonda geçirdiğim süre boyunca elde ettiğim fırsatlar ve deneyimler için teşekkür etmek isterim. Sorumluluklarımın sorunsuz bir şekilde devredilmesini sağlamaya kararlıyım.

Anlayışınız için teşekkür ederim.

Saygılarımla,
{employee_name}

', + ], + 'zh' => [ + 'subject' => '辞职信 - {employee_name}', + 'content' => '

尊敬的先生/女士,

我写信正式通知您,我将从{resignation_date}起辞去在{app_name}的职位。

辞职详情:

  • 辞职日期:{resignation_date}
  • 原因:{reason}

我要感谢在组织工作期间获得的机会和经验。我承诺确保我的职责顺利交接。

感谢您的理解。

此致敬礼,
{employee_name}

', + ], + ], + ], + [ + 'name' => 'Employee Termination', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Employment Termination Notice - {employee_name}', + 'content' => '

Dear {employee_name},

We regret to inform you that your employment with {app_name} will be terminated effective {termination_date}.

Termination Details:

  • Termination Type: {termination_type}
  • Termination Date: {termination_date}
  • Notice Date: {notice_date}
  • Reason: {reason}

Please contact the HR department for further information regarding final settlement and exit procedures.

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Aviso de Terminación de Empleo - {employee_name}', + 'content' => '

Estimado/a {employee_name},

Lamentamos informarle que su empleo con {app_name} será terminado con efecto {termination_date}.

Detalles de la Terminación:

  • Tipo de Terminación: {termination_type}
  • Fecha de Terminación: {termination_date}
  • Fecha de Aviso: {notice_date}
  • Motivo: {reason}

Por favor, contacte al departamento de RRHH para obtener más información sobre la liquidación final y los procedimientos de salida.

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'إشعار إنهاء الخدمة - {employee_name}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

يؤسفنا إبلاغك بأن خدمتك مع {app_name} سيتم إنهاؤها اعتبارًا من {termination_date}.

تفاصيل الإنهاء:

  • نوع الإنهاء: {termination_type}
  • تاريخ الإنهاء: {termination_date}
  • تاريخ الإشعار: {notice_date}
  • السبب: {reason}

يرجى الاتصال بقسم الموارد البشرية للحصول على مزيد من المعلومات بخصوص التسوية النهائية وإجراءات الخروج.

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Meddelelse om Ophør af Ansættelse - {employee_name}', + 'content' => '

Kære {employee_name},

Vi beklager at informere dig om, at dit ansættelsesforhold med {app_name} vil blive ophørt med virkning fra {termination_date}.

Ophørsdetaljer:

  • Ophørstype: {termination_type}
  • Ophørsdato: {termination_date}
  • Meddelelsesdato: {notice_date}
  • Årsag: {reason}

Kontakt venligst HR-afdelingen for yderligere information om endelig afregning og udtrædelsesprocedurer.

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Mitteilung über Beendigung des Arbeitsverhältnisses - {employee_name}', + 'content' => '

Liebe/r {employee_name},

Wir bedauern, Ihnen mitteilen zu müssen, dass Ihr Arbeitsverhältnis mit {app_name} mit Wirkung zum {termination_date} beendet wird.

Beendigungsdetails:

  • Beendigungsart: {termination_type}
  • Beendigungsdatum: {termination_date}
  • Mitteilungsdatum: {notice_date}
  • Grund: {reason}

Bitte wenden Sie sich an die Personalabteilung für weitere Informationen zur Endabrechnung und zu den Austrittsverfahren.

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Avis de Fin de Contrat - {employee_name}', + 'content' => '

Cher/Chère {employee_name},

Nous regrettons de vous informer que votre emploi avec {app_name} prendra fin à compter du {termination_date}.

Détails de la Fin de Contrat:

  • Type de Fin de Contrat: {termination_type}
  • Date de Fin de Contrat: {termination_date}
  • Date de Notification: {notice_date}
  • Raison: {reason}

Veuillez contacter le département RH pour plus d\'informations concernant le règlement final et les procédures de sortie.

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'הודעת סיום עבודה - {employee_name}', + 'content' => '

יקר/ה {employee_name},

אנו מצטערים להודיע לך שהעסקתך עם {app_name} תסתיים בתוקף מ-{termination_date}.

פרטי סיום העבודה:

  • סוג סיום: {termination_type}
  • תאריך סיום: {termination_date}
  • תאריך הודעה: {notice_date}
  • סיבה: {reason}

אנא צור קשר עם מחלקת המשאבי אנוש למידע נוסף לגבי הסדר סופי ונהלי יציאה.

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Avviso di Cessazione del Rapporto di Lavoro - {employee_name}', + 'content' => '

Caro/a {employee_name},

Siamo spiacenti di informarti che il tuo rapporto di lavoro con {app_name} sarà cessato con effetto dal {termination_date}.

Dettagli della Cessazione:

  • Tipo di Cessazione: {termination_type}
  • Data di Cessazione: {termination_date}
  • Data di Notifica: {notice_date}
  • Motivo: {reason}

Si prega di contattare il dipartimento HR per ulteriori informazioni riguardo alla liquidazione finale e alle procedure di uscita.

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '雇用終了通知 - {employee_name}', + 'content' => '

{employee_name}様、

討しくも、{termination_date}付けで{app_name}との雇用関係が終了することをお知らせいたします。

終了の詳細:

  • 終了タイプ: {termination_type}
  • 終了日: {termination_date}
  • 通知日: {notice_date}
  • 理由: {reason}

最終精算および退職手続きに関する詳細については、HR部門にお問い合わせください。

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Kennisgeving van Beëindiging Dienstverband - {employee_name}', + 'content' => '

Beste {employee_name},

Het spijt ons je te moeten informeren dat je dienstverband met {app_name} met ingang van {termination_date} wordt beëindigd.

Beëindigingsdetails:

  • Type Beëindiging: {termination_type}
  • Beëindigingsdatum: {termination_date}
  • Kennisgevingsdatum: {notice_date}
  • Reden: {reason}

Neem contact op met de HR-afdeling voor meer informatie over de eindafrekening en vertrekprocedures.

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Zawiadomienie o Rozwiazaniu Umowy o Pracę - {employee_name}', + 'content' => '

Drogi/a {employee_name},

Z przykrością informujemy, że Twoje zatrudnienie w {app_name} zostanie rozwiązane z dniem {termination_date}.

Szczegóły Rozwiązania:

  • Typ Rozwiązania: {termination_type}
  • Data Rozwiązania: {termination_date}
  • Data Powiadomienia: {notice_date}
  • Powód: {reason}

Prosimy o kontakt z działem HR w celu uzyskania dalszych informacji dotyczących ostatecznego rozliczenia i procedur wyjścia.

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Aviso de Rescisão de Contrato - {employee_name}', + 'content' => '

Caro/a {employee_name},

Lamentamos informá-lo de que seu emprego com {app_name} será rescindido com efeito a partir de {termination_date}.

Detalhes da Rescisão:

  • Tipo de Rescisão: {termination_type}
  • Data de Rescisão: {termination_date}
  • Data de Aviso: {notice_date}
  • Motivo: {reason}

Por favor, entre em contato com o departamento de RH para mais informações sobre acerto final e procedimentos de saída.

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Aviso de Rescisão de Contrato - {employee_name}', + 'content' => '

Caro/a {employee_name},

Lamentamos informá-lo de que seu emprego com {app_name} será rescindido com efeito a partir de {termination_date}.

Detalhes da Rescisão:

  • Tipo de Rescisão: {termination_type}
  • Data de Rescisão: {termination_date}
  • Data de Aviso: {notice_date}
  • Motivo: {reason}

Por favor, entre em contato com o departamento de RH para mais informações sobre acerto final e procedimentos de saída.

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Уведомление о Расторжении Трудового Договора - {employee_name}', + 'content' => '

Уважаемый/ая {employee_name},

С сожалением сообщаем вам, что ваше трудоустройство в {app_name} будет прекращено с {termination_date}.

Детали Расторжения:

  • Тип Расторжения: {termination_type}
  • Дата Расторжения: {termination_date}
  • Дата Уведомления: {notice_date}
  • Причина: {reason}

Пожалуйста, свяжитесь с отделом кадров для получения дополнительной информации о окончательном расчете и процедурах увольнения.

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => 'İş Sonu Bildirimi - {employee_name}', + 'content' => '

Sayın {employee_name},

Üzgünlükle bildiririz ki, {app_name} ile olan iş sözleşmeniz {termination_date} tarihinden itibaren sona erecektir.

İş Sonu Detayları:

  • İş Sonu Türü: {termination_type}
  • İş Sonu Tarihi: {termination_date}
  • Bildirim Tarihi: {notice_date}
  • Sebep: {reason}

Lütfen nihai ödeme ve çıkış prosedürleri hakkında daha fazla bilgi için İK departmanı ile iletişime geçin.

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '雇用终止通知 - {employee_name}', + 'content' => '

尊敬的 {employee_name}

我们遗憾地通知您,您与{app_name}的雇用关系将从{termination_date}起终止。

终止详情:

  • 终止类型:{termination_type}
  • 终止日期:{termination_date}
  • 通知日期:{notice_date}
  • 原因:{reason}

请联系HR部门了解有关最终结算和离职程序的进一步信息。

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'Employee Warning', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Official Warning Notice - {subject}', + 'content' => '

Dear {employee_name},

This letter serves as an official {severity} warning regarding {subject}.

Warning Details:

  • Warning Type: {warning_type}
  • Severity: {severity}
  • Warning Date: {warning_date}
  • Subject: {subject}
  • Description: {description}

This warning is being issued due to concerns about your conduct/performance. We expect immediate improvement in this area.

Please acknowledge receipt of this warning and contact HR if you have any questions.

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Aviso de Advertencia Oficial - {subject}', + 'content' => '

Estimado/a {employee_name},

Esta carta sirve como una advertencia oficial de {severity} con respecto a {subject}.

Detalles de la Advertencia:

  • Tipo de Advertencia: {warning_type}
  • Severidad: {severity}
  • Fecha de Advertencia: {warning_date}
  • Asunto: {subject}
  • Descripción: {description}

Esta advertencia se emite debido a preocupaciones sobre su conducta/desempeño. Esperamos una mejora inmediata en esta área.

Por favor, confirme la recepción de esta advertencia y contacte a RRHH si tiene alguna pregunta.

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'إشعار تحذير رسمي - {subject}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

تعتبر هذه الرسالة تحذيرًا رسميًا من مستوى {severity} بخصوص {subject}.

تفاصيل التحذير:

  • نوع التحذير: {warning_type}
  • الخطورة: {severity}
  • تاريخ التحذير: {warning_date}
  • الموضوع: {subject}
  • الوصف: {description}

يتم إصدار هذا التحذير بسبب مخاوف بشأن سلوكك/أدائك. نتوقع تحسنًا فوريًا في هذا المجال.

يرجى تأكيد استلام هذا التحذير والاتصال بالموارد البشرية إذا كان لديك أي أسئلة.

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Officiel Advarsel - {subject}', + 'content' => '

Kære {employee_name},

Dette brev tjener som en officiel {severity} advarsel vedrørende {subject}.

Advarselsdetaljer:

  • Advarselstype: {warning_type}
  • Alvorlighed: {severity}
  • Advarselsdato: {warning_date}
  • Emne: {subject}
  • Beskrivelse: {description}

Denne advarsel udstedes på grund af bekymringer om din adfærd/præstation. Vi forventer øjeblikkelig forbedring på dette område.

Bekræft venligst modtagelsen af denne advarsel og kontakt HR, hvis du har spørgsmål.

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Offizielle Verwarnung - {subject}', + 'content' => '

Liebe/r {employee_name},

Dieses Schreiben dient als offizielle {severity} Verwarnung bezüglich {subject}.

Verwarnungsdetails:

  • Verwarnungstyp: {warning_type}
  • Schweregrad: {severity}
  • Verwarnungsdatum: {warning_date}
  • Betreff: {subject}
  • Beschreibung: {description}

Diese Verwarnung wird aufgrund von Bedenken bezüglich Ihres Verhaltens/Ihrer Leistung ausgesprochen. Wir erwarten eine sofortige Verbesserung in diesem Bereich.

Bitte bestätigen Sie den Erhalt dieser Verwarnung und wenden Sie sich bei Fragen an die Personalabteilung.

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Avertissement Officiel - {subject}', + 'content' => '

Cher/Chère {employee_name},

Cette lettre constitue un avertissement officiel de niveau {severity} concernant {subject}.

Détails de l\'Avertissement:

  • Type d\'Avertissement: {warning_type}
  • Gravité: {severity}
  • Date d\'Avertissement: {warning_date}
  • Sujet: {subject}
  • Description: {description}

Cet avertissement est émis en raison de préoccupations concernant votre conduite/performance. Nous attendons une amélioration immédiate dans ce domaine.

Veuillez accuser réception de cet avertissement et contacter les RH si vous avez des questions.

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'הודעת אזהרה רשמית - {subject}', + 'content' => '

יקר/ה {employee_name},

מכתב זה משמש כאזהרה רשמית ברמת {severity} בנוגע ל-{subject}.

פרטי האזהרה:

  • סוג אזהרה: {warning_type}
  • חומרה: {severity}
  • תאריך אזהרה: {warning_date}
  • נושא: {subject}
  • תיאור: {description}

אזהרה זו מונפקת עקב חששות לגבי התנהגותך/ביצועיך. אנו מצפים לשיפור מיידי בתחום זה.

אנא אשר קבלת אזהרה זו וצור קשר עם משאבי אנוש אם יש לך שאלות.

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Avviso di Ammonimento Ufficiale - {subject}', + 'content' => '

Caro/a {employee_name},

Questa lettera costituisce un ammonimento ufficiale di livello {severity} riguardante {subject}.

Dettagli dell\'Ammonimento:

  • Tipo di Ammonimento: {warning_type}
  • Gravità: {severity}
  • Data Ammonimento: {warning_date}
  • Oggetto: {subject}
  • Descrizione: {description}

Questo ammonimento viene emesso a causa di preoccupazioni riguardo alla tua condotta/prestazione. Ci aspettiamo un miglioramento immediato in quest\'area.

Si prega di confermare la ricezione di questo ammonimento e contattare l\'HR per eventuali domande.

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '正式警告通知 - {subject}', + 'content' => '

{employee_name}様、

この書簡は、{subject}に関する{severity}レベルの正式な警告として機能します。

警告の詳細:

  • 警告タイプ: {warning_type}
  • 重大度: {severity}
  • 警告日: {warning_date}
  • 件名: {subject}
  • 説明: {description}

この警告は、あなたの行動/パフォーマンスに関する懸念により発行されています。この分野での即座の改善を期待しています。

この警告の受領を確認し、質問がある場合はHRにお問い合わせください。

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Officiële Waarschuwing - {subject}', + 'content' => '

Beste {employee_name},

Deze brief dient als een officiële {severity} waarschuwing met betrekking tot {subject}.

Waarschuwingsdetails:

  • Waarschuwingstype: {warning_type}
  • Ernst: {severity}
  • Waarschuwingsdatum: {warning_date}
  • Onderwerp: {subject}
  • Beschrijving: {description}

Deze waarschuwing wordt afgegeven vanwege zorgen over je gedrag/prestaties. We verwachten onmiddellijke verbetering op dit gebied.

Bevestig de ontvangst van deze waarschuwing en neem contact op met HR als je vragen hebt.

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Oficjalne Ostrzeżenie - {subject}', + 'content' => '

Drogi/a {employee_name},

Ten list stanowi oficjalne ostrzeżenie na poziomie {severity} dotyczące {subject}.

Szczegóły Ostrzeżenia:

  • Typ Ostrzeżenia: {warning_type}
  • Powaga: {severity}
  • Data Ostrzeżenia: {warning_date}
  • Temat: {subject}
  • Opis: {description}

To ostrzeżenie jest wydawane z powodu obaw dotyczących Twojego zachowania/wydajności. Oczekujemy natychmiastowej poprawy w tym obszarze.

Prosimy o potwierdzenie odbioru tego ostrzeżenia i skontaktowanie się z HR w przypadku pytań.

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Aviso de Advertência Oficial - {subject}', + 'content' => '

Caro/a {employee_name},

Esta carta serve como uma advertência oficial de nível {severity} referente a {subject}.

Detalhes da Advertência:

  • Tipo de Advertência: {warning_type}
  • Severidade: {severity}
  • Data da Advertência: {warning_date}
  • Assunto: {subject}
  • Descrição: {description}

Esta advertência está sendo emitida devido a preocupações sobre sua conduta/desempenho. Esperamos melhoria imediata nesta área.

Por favor, confirme o recebimento desta advertência e entre em contato com o RH se tiver alguma dúvida.

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Aviso de Advertência Oficial - {subject}', + 'content' => '

Caro/a {employee_name},

Esta carta serve como uma advertência oficial de nível {severity} referente a {subject}.

Detalhes da Advertência:

  • Tipo de Advertência: {warning_type}
  • Severidade: {severity}
  • Data da Advertência: {warning_date}
  • Assunto: {subject}
  • Descrição: {description}

Esta advertência está sendo emitida devido a preocupações sobre sua conduta/desempenho. Esperamos melhoria imediata nesta área.

Por favor, confirme o recebimento desta advertência e entre em contato com o RH se tiver alguma dúvida.

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Официальное Предупреждение - {subject}', + 'content' => '

Уважаемый/ая {employee_name},

Это письмо служит официальным предупреждением уровня {severity} относительно {subject}.

Детали Предупреждения:

  • Тип Предупреждения: {warning_type}
  • Серьезность: {severity}
  • Дата Предупреждения: {warning_date}
  • Тема: {subject}
  • Описание: {description}

Это предупреждение выдается в связи с озабоченностью по поводу вашего поведения/производительности. Мы ожидаем немедленного улучшения в этой области.

Пожалуйста, подтвердите получение этого предупреждения и свяжитесь с отделом кадров, если у вас есть вопросы.

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => 'Resmi Uyarı Bildirimi - {subject}', + 'content' => '

Sayın {employee_name},

Bu mektup, {subject} ile ilgili {severity} seviyesinde resmi bir uyarı olarak hizmet etmektedir.

Uyarı Detayları:

  • Uyarı Türü: {warning_type}
  • Ciddiyet: {severity}
  • Uyarı Tarihi: {warning_date}
  • Konu: {subject}
  • Açıklama: {description}

Bu uyarı, davranışınız/performansınız hakkındaki endişeler nedeniyle verilmektedir. Bu alanda acil iyileşme bekliyoruz.

Lütfen bu uyarının alındığını onaylayın ve sorularınız varsa İK ile iletişime geçin.

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '正式警告通知 - {subject}', + 'content' => '

尊敬的 {employee_name}

此信函作为关于{subject}{severity}级别正式警告。

警告详情:

  • 警告类型:{warning_type}
  • 严重程度:{severity}
  • 警告日期:{warning_date}
  • 主题:{subject}
  • 描述:{description}

由于对您的行为/表现存在担忧,特此发出此警告。我们期望在这方面立即改进。

请确认收到此警告,如有任何问题,请联系HR部门。

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'Employee Trip', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Business Trip Assignment - {destination}', + 'content' => '

Dear {employee_name},

You have been assigned a business trip to {destination}.

Trip Details:

  • Purpose: {purpose}
  • Destination: {destination}
  • Start Date: {start_date}
  • End Date: {end_date}
  • Description: {description}

Please make necessary arrangements and contact HR for travel logistics and advance payment if required.

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Asignación de Viaje de Negocios - {destination}', + 'content' => '

Estimado/a {employee_name},

Se le ha asignado un viaje de negocios a {destination}.

Detalles del Viaje:

  • Propósito: {purpose}
  • Destino: {destination}
  • Fecha de Inicio: {start_date}
  • Fecha de Fin: {end_date}
  • Descripción: {description}

Por favor, realice los arreglos necesarios y contacte a RRHH para la logística de viaje y pago anticipado si es necesario.

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'تكليف برحلة عمل - {destination}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

تم تكليفك برحلة عمل إلى {destination}.

تفاصيل الرحلة:

  • الغرض: {purpose}
  • الوجهة: {destination}
  • تاريخ البدء: {start_date}
  • تاريخ الانتهاء: {end_date}
  • الوصف: {description}

يرجى إجراء الترتيبات اللازمة والاتصال بالموارد البشرية للحصول على لوجستيات السفر والدفع المسبق إذا لزم الأمر.

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Forretningsrejse Tildeling - {destination}', + 'content' => '

Kære {employee_name},

Du er blevet tildelt en forretningsrejse til {destination}.

Rejsedetaljer:

  • Formål: {purpose}
  • Destination: {destination}
  • Startdato: {start_date}
  • Slutdato: {end_date}
  • Beskrivelse: {description}

Foretag venligst de nødvendige arrangementer og kontakt HR for rejselogistik og forskudsbetaling, hvis det er nødvendigt.

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Geschäftsreise Zuweisung - {destination}', + 'content' => '

Liebe/r {employee_name},

Ihnen wurde eine Geschäftsreise nach {destination} zugewiesen.

Reisedetails:

  • Zweck: {purpose}
  • Ziel: {destination}
  • Startdatum: {start_date}
  • Enddatum: {end_date}
  • Beschreibung: {description}

Bitte treffen Sie die notwendigen Vorkehrungen und wenden Sie sich an die Personalabteilung für Reiselogistik und Vorauszahlung, falls erforderlich.

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Affectation de Voyage d\'Affaires - {destination}', + 'content' => '

Cher/Chère {employee_name},

Vous avez été affecté(e) à un voyage d\'affaires à {destination}.

Détails du Voyage:

  • Objectif: {purpose}
  • Destination: {destination}
  • Date de Début: {start_date}
  • Date de Fin: {end_date}
  • Description: {description}

Veuillez prendre les dispositions nécessaires et contacter les RH pour la logistique de voyage et le paiement anticipé si nécessaire.

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'הקצאת נסיעת עסקים - {destination}', + 'content' => '

יקר/ה {employee_name},

הוקצתה לך נסיעת עסקים ל-{destination}.

פרטי הנסיעה:

  • מטרה: {purpose}
  • יעד: {destination}
  • תאריך התחלה: {start_date}
  • תאריך סיום: {end_date}
  • תיאור: {description}

אנא בצע את ההסדרים הדרושים וצור קשר עם משאבי אנוש ללוגיסטיקת נסיעות ותשלום מקדמה במידת הצורך.

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Assegnazione Viaggio di Lavoro - {destination}', + 'content' => '

Caro/a {employee_name},

Ti è stato assegnato un viaggio di lavoro a {destination}.

Dettagli del Viaggio:

  • Scopo: {purpose}
  • Destinazione: {destination}
  • Data di Inizio: {start_date}
  • Data di Fine: {end_date}
  • Descrizione: {description}

Si prega di effettuare le disposizioni necessarie e contattare l\'HR per la logistica di viaggio e il pagamento anticipato se necessario.

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '出張割り当て - {destination}', + 'content' => '

{employee_name}様、

{destination}への出張が割り当てられました。

出張の詳細:

  • 目的: {purpose}
  • 目的地: {destination}
  • 開始日: {start_date}
  • 終了日: {end_date}
  • 説明: {description}

必要な手配を行い、旅行の手配や前払いが必要な場合はHRにお問い合わせください。

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Zakelijke Reis Toewijzing - {destination}', + 'content' => '

Beste {employee_name},

Je bent toegewezen aan een zakelijke reis naar {destination}.

Reisdetails:

  • Doel: {purpose}
  • Bestemming: {destination}
  • Startdatum: {start_date}
  • Einddatum: {end_date}
  • Beschrijving: {description}

Maak de nodige regelingen en neem contact op met HR voor reislogistiek en vooruitbetaling indien nodig.

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Przydzielenie Podróży Służbowej - {destination}', + 'content' => '

Drogi/a {employee_name},

Zostałeś/aś przydzielony/a do podróży służbowej do {destination}.

Szczegóły Podróży:

  • Cel: {purpose}
  • Miejsce Docelowe: {destination}
  • Data Rozpoczęcia: {start_date}
  • Data Zakończenia: {end_date}
  • Opis: {description}

Prosimy o dokonanie niezbędnych ustaleń i skontaktowanie się z HR w sprawie logistyki podróży i zaliczki, jeśli jest to wymagane.

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Atribuição de Viagem de Negócios - {destination}', + 'content' => '

Caro/a {employee_name},

Você foi designado para uma viagem de negócios para {destination}.

Detalhes da Viagem:

  • Propósito: {purpose}
  • Destino: {destination}
  • Data de Início: {start_date}
  • Data de Término: {end_date}
  • Descrição: {description}

Por favor, faça os arranjos necessários e entre em contato com o RH para logística de viagem e pagamento antecipado, se necessário.

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Atribuição de Viagem de Negócios - {destination}', + 'content' => '

Caro/a {employee_name},

Você foi designado para uma viagem de negócios para {destination}.

Detalhes da Viagem:

  • Propósito: {purpose}
  • Destino: {destination}
  • Data de Início: {start_date}
  • Data de Término: {end_date}
  • Descrição: {description}

Por favor, faça os arranjos necessários e entre em contato com o RH para logística de viagem e pagamento antecipado, se necessário.

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Назначение Командировки - {destination}', + 'content' => '

Уважаемый/ая {employee_name},

Вам назначена командировка в {destination}.

Детали Поездки:

  • Цель: {purpose}
  • Место Назначения: {destination}
  • Дата Начала: {start_date}
  • Дата Окончания: {end_date}
  • Описание: {description}

Пожалуйста, сделайте необходимые приготовления и свяжитесь с отделом кадров для организации поездки и авансового платежа, если требуется.

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => 'İş Seyahati Ataması - {destination}', + 'content' => '

Sayın {employee_name},

{destination} için bir iş seyahati atandınız.

Seyahat Detayları:

  • Amaç: {purpose}
  • Hedef: {destination}
  • Başlangıç Tarihi: {start_date}
  • Bitiş Tarihi: {end_date}
  • Açıklama: {description}

Lütfen gerekli düzenlemeleri yapın ve gerekirse seyahat lojistiği ve avans ödemesi için İK ile iletişime geçin.

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '商务出差分配 - {destination}', + 'content' => '

尊敬的 {employee_name}

您已被分配到{destination}的商务出差。

出差详情:

  • 目的:{purpose}
  • 目的地:{destination}
  • 开始日期:{start_date}
  • 结束日期:{end_date}
  • 描述:{description}

请做好必要的安排,如需旅行后勤和预付款,请联系HR部门。

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'Employee Complaint', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Employee Complaint Submission - {subject}', + 'content' => '

Dear HR Team,

This is to formally submit a complaint regarding {subject}.

Complaint Details:

  • Employee Name: {employee_name}
  • Complaint Type: {complaint_type}
  • Complaint Date: {complaint_date}
  • Subject: {subject}
  • Description: {description}

I request that this matter be investigated and addressed promptly. I am available to provide any additional information if needed.

Thank you for your attention to this matter.

Sincerely,
{employee_name}

', + ], + 'es' => [ + 'subject' => 'Presentación de Queja de Empleado - {subject}', + 'content' => '

Estimado Equipo de RRHH,

Por la presente presento formalmente una queja con respecto a {subject}.

Detalles de la Queja:

  • Nombre del Empleado: {employee_name}
  • Tipo de Queja: {complaint_type}
  • Fecha de Queja: {complaint_date}
  • Asunto: {subject}
  • Descripción: {description}

Solicito que este asunto sea investigado y abordado con prontitud. Estoy disponible para proporcionar cualquier información adicional si es necesario.

Gracias por su atención a este asunto.

Atentamente,
{employee_name}

', + ], + 'ar' => [ + 'subject' => 'تقديم شكوى موظف - {subject}', + 'content' => '

عزيزي فريق الموارد البشرية،

أتقدم بهذا رسميًا بشكوى بخصوص {subject}.

تفاصيل الشكوى:

  • اسم الموظف: {employee_name}
  • نوع الشكوى: {complaint_type}
  • تاريخ الشكوى: {complaint_date}
  • الموضوع: {subject}
  • الوصف: {description}

أطلب التحقيق في هذا الأمر ومعالجته على الفور. أنا متاح لتقديم أي معلومات إضافية إذا لزم الأمر.

شكرًا لاهتمامكم بهذا الأمر.

مع خالص التقدير،
{employee_name}

', + ], + 'da' => [ + 'subject' => 'Medarbejderklage Indsendelse - {subject}', + 'content' => '

Kære HR-team,

Dette er for formelt at indsende en klage vedrørende {subject}.

Klagedetaljer:

  • Medarbejdernavn: {employee_name}
  • Klagetype: {complaint_type}
  • Klagedato: {complaint_date}
  • Emne: {subject}
  • Beskrivelse: {description}

Jeg anmoder om, at denne sag undersøges og behandles hurtigt. Jeg er tilgængelig for at give yderligere oplysninger, hvis det er nødvendigt.

Tak for din opmærksomhed på denne sag.

Med venlig hilsen,
{employee_name}

', + ], + 'de' => [ + 'subject' => 'Mitarbeiterbeschwerde Einreichung - {subject}', + 'content' => '

Liebes HR-Team,

Hiermit reiche ich formell eine Beschwerde bezüglich {subject} ein.

Beschwerdedetails:

  • Mitarbeitername: {employee_name}
  • Beschwerdeart: {complaint_type}
  • Beschwerdedatum: {complaint_date}
  • Betreff: {subject}
  • Beschreibung: {description}

Ich bitte darum, dass diese Angelegenheit umgehend untersucht und bearbeitet wird. Ich stehe zur Verfügung, um bei Bedarf zusätzliche Informationen bereitzustellen.

Vielen Dank für Ihre Aufmerksamkeit in dieser Angelegenheit.

Mit freundlichen Grüßen,
{employee_name}

', + ], + 'fr' => [ + 'subject' => 'Soumission de Plainte d\'Employé - {subject}', + 'content' => '

Cher Équipe RH,

Je soumets formellement une plainte concernant {subject}.

Détails de la Plainte:

  • Nom de l\'Employé: {employee_name}
  • Type de Plainte: {complaint_type}
  • Date de Plainte: {complaint_date}
  • Sujet: {subject}
  • Description: {description}

Je demande que cette affaire soit enquêtée et traitée rapidement. Je suis disponible pour fournir toute information supplémentaire si nécessaire.

Merci pour votre attention à cette affaire.

Cordialement,
{employee_name}

', + ], + 'he' => [ + 'subject' => 'הגשת תלונת עובד - {subject}', + 'content' => '

צוות משאבי אנוש יקר,

זאת להגיש רשמית תלונה בנוגע ל-{subject}.

פרטי התלונה:

  • שם העובד: {employee_name}
  • סוג תלונה: {complaint_type}
  • תאריך תלונה: {complaint_date}
  • נושא: {subject}
  • תיאור: {description}

אני מבקש שעניין זה ייחקר ויטופל במהירות. אני זמין לספק כל מידע נוסף במידת הצורך.

תודה על תשומת הלב לעניין זה.

בכבוד רב,
{employee_name}

', + ], + 'it' => [ + 'subject' => 'Presentazione Reclamo Dipendente - {subject}', + 'content' => '

Caro Team HR,

Con la presente presento formalmente un reclamo riguardante {subject}.

Dettagli del Reclamo:

  • Nome Dipendente: {employee_name}
  • Tipo di Reclamo: {complaint_type}
  • Data Reclamo: {complaint_date}
  • Oggetto: {subject}
  • Descrizione: {description}

Richiedo che questa questione venga indagata e affrontata prontamente. Sono disponibile a fornire ulteriori informazioni se necessario.

Grazie per l\'attenzione a questa questione.

Cordiali saluti,
{employee_name}

', + ], + 'ja' => [ + 'subject' => '従業員苦情提出 - {subject}', + 'content' => '

人事部御中、

{subject}に関する苦情を正式に提出いたします。

苦情の詳細:

  • 従業員名: {employee_name}
  • 苦情タイプ: {complaint_type}
  • 苦情日: {complaint_date}
  • 件名: {subject}
  • 説明: {description}

この件について速やかに調査し、対処していただくようお願いいたします。必要に応じて追加情報を提供する用意があります。

この件へのご配慮に感謝いたします。

敬具
{employee_name}

', + ], + 'nl' => [ + 'subject' => 'Indiening Werknemersklacht - {subject}', + 'content' => '

Beste HR-team,

Dit is om formeel een klacht in te dienen met betrekking tot {subject}.

Klachtdetails:

  • Werknemersnaam: {employee_name}
  • Klachttype: {complaint_type}
  • Klachtdatum: {complaint_date}
  • Onderwerp: {subject}
  • Beschrijving: {description}

Ik verzoek dat deze kwestie snel wordt onderzocht en aangepakt. Ik ben beschikbaar om indien nodig aanvullende informatie te verstrekken.

Dank u voor uw aandacht voor deze kwestie.

Met vriendelijke groet,
{employee_name}

', + ], + 'pl' => [ + 'subject' => 'Złożenie Skargi Pracownika - {subject}', + 'content' => '

Szanowny Zespół HR,

Niniejszym formalnie składam skargę dotyczącą {subject}.

Szczegóły Skargi:

  • Imię i Nazwisko Pracownika: {employee_name}
  • Typ Skargi: {complaint_type}
  • Data Skargi: {complaint_date}
  • Temat: {subject}
  • Opis: {description}

Proszę o zbadanie i niezwłoczne zajęcie się tą sprawą. Jestem dostępny, aby w razie potrzeby dostarczyć dodatkowe informacje.

Dziękuję za uwagę poświęconą tej sprawie.

Z poważaniem,
{employee_name}

', + ], + 'pt' => [ + 'subject' => 'Submissão de Reclamação de Funcionário - {subject}', + 'content' => '

Prezada Equipe de RH,

Venho formalmente apresentar uma reclamação referente a {subject}.

Detalhes da Reclamação:

  • Nome do Funcionário: {employee_name}
  • Tipo de Reclamação: {complaint_type}
  • Data da Reclamação: {complaint_date}
  • Assunto: {subject}
  • Descrição: {description}

Solicito que este assunto seja investigado e tratado prontamente. Estou disponível para fornecer qualquer informação adicional, se necessário.

Obrigado pela atenção a este assunto.

Atenciosamente,
{employee_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Submissão de Reclamação de Funcionário - {subject}', + 'content' => '

Prezada Equipe de RH,

Venho formalmente apresentar uma reclamação referente a {subject}.

Detalhes da Reclamação:

  • Nome do Funcionário: {employee_name}
  • Tipo de Reclamação: {complaint_type}
  • Data da Reclamação: {complaint_date}
  • Assunto: {subject}
  • Descrição: {description}

Solicito que este assunto seja investigado e tratado prontamente. Estou disponível para fornecer qualquer informação adicional, se necessário.

Obrigado pela atenção a este assunto.

Atenciosamente,
{employee_name}

', + ], + 'ru' => [ + 'subject' => 'Подача Жалобы Сотрудника - {subject}', + 'content' => '

Уважаемая команда HR,

Настоящим официально подаю жалобу относительно {subject}.

Детали Жалобы:

  • Имя Сотрудника: {employee_name}
  • Тип Жалобы: {complaint_type}
  • Дата Жалобы: {complaint_date}
  • Тема: {subject}
  • Описание: {description}

Прошу расследовать и оперативно рассмотреть этот вопрос. Я готов предоставить любую дополнительную информацию при необходимости.

Благодарю за внимание к этому вопросу.

С уважением,
{employee_name}

', + ], + 'tr' => [ + 'subject' => 'Çalışan Şikayeti Gönderimi - {subject}', + 'content' => '

Sayın İK Ekibi,

Bu, {subject} ile ilgili resmi olarak bir şikayet göndermek içindir.

Şikayet Detayları:

  • Çalışan Adı: {employee_name}
  • Şikayet Türü: {complaint_type}
  • Şikayet Tarihi: {complaint_date}
  • Konu: {subject}
  • Açıklama: {description}

Bu konunun araştırılmasını ve derhal ele alınmasını talep ediyorum. Gerekirse ek bilgi sağlamak için hazırım.

Bu konuya gösterdiğiniz ilgi için teşekkür ederim.

Saygılarımla,
{employee_name}

', + ], + 'zh' => [ + 'subject' => '员工投诉提交 - {subject}', + 'content' => '

尊敬的HR团队,

特此正式提交关于{subject}的投诉。

投诉详情:

  • 员工姓名:{employee_name}
  • 投诉类型:{complaint_type}
  • 投诉日期:{complaint_date}
  • 主题:{subject}
  • 描述:{description}

我请求对此事进行调查并及时处理。如有需要,我可以提供任何额外信息。

感谢您对此事的关注。

此致敬礼,
{employee_name}

', + ], + ], + ], + [ + 'name' => 'Employee Transfer', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Employee Transfer Notification - {employee_name}', + 'content' => '

Dear {employee_name},

We are writing to inform you that you have been transferred effective {effective_date}.

Transfer Details:

  • Transfer Date: {transfer_date}
  • Effective Date: {effective_date}
  • Department: {from_department_name} → {to_department_name}
  • Designation: {from_designation_name} → {to_designation_name}
  • Reason: {reason}

Please contact HR for further details regarding your transfer and any necessary arrangements.

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Notificación de Transferencia de Empleado - {employee_name}', + 'content' => '

Estimado/a {employee_name},

Le escribimos para informarle que ha sido transferido/a con efecto {effective_date}.

Detalles de la Transferencia:

  • Fecha de Transferencia: {transfer_date}
  • Fecha Efectiva: {effective_date}
  • Departamento: {from_department_name} → {to_department_name}
  • Designación: {from_designation_name} → {to_designation_name}
  • Motivo: {reason}

Por favor, contacte a RRHH para más detalles sobre su transferencia y cualquier arreglo necesario.

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'إشعار نقل موظف - {employee_name}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

نكتب إليك لإبلاغك بأنه تم نقلك اعتبارًا من {effective_date}.

تفاصيل النقل:

  • تاريخ النقل: {transfer_date}
  • تاريخ السريان: {effective_date}
  • القسم: {from_department_name} → {to_department_name}
  • المسمى الوظيفي: {from_designation_name} → {to_designation_name}
  • السبب: {reason}

يرجى الاتصال بالموارد البشرية لمزيد من التفاصيل حول نقلك وأي ترتيبات ضرورية.

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Medarbejderoverførsel Meddelelse - {employee_name}', + 'content' => '

Kære {employee_name},

Vi skriver for at informere dig om, at du er blevet overført med virkning fra {effective_date}.

Overførselsdetaljer:

  • Overførselsdato: {transfer_date}
  • Ikrafttrædelsesdato: {effective_date}
  • Afdeling: {from_department_name} → {to_department_name}
  • Betegnelse: {from_designation_name} → {to_designation_name}
  • Årsag: {reason}

Kontakt venligst HR for yderligere detaljer om din overførsel og eventuelle nødvendige arrangementer.

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Mitarbeiterversetzung Benachrichtigung - {employee_name}', + 'content' => '

Liebe/r {employee_name},

Wir schreiben Ihnen, um Sie darüber zu informieren, dass Sie mit Wirkung zum {effective_date} versetzt wurden.

Versetzungsdetails:

  • Versetzungsdatum: {transfer_date}
  • Gültigkeitsdatum: {effective_date}
  • Abteilung: {from_department_name} → {to_department_name}
  • Bezeichnung: {from_designation_name} → {to_designation_name}
  • Grund: {reason}

Bitte wenden Sie sich an die Personalabteilung für weitere Details zu Ihrer Versetzung und notwendigen Vorkehrungen.

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Notification de Transfert d\'Employé - {employee_name}', + 'content' => '

Cher/Chère {employee_name},

Nous vous écrivons pour vous informer que vous avez été transféré(e) à compter du {effective_date}.

Détails du Transfert:

  • Date de Transfert: {transfer_date}
  • Date d\'Effet: {effective_date}
  • Département: {from_department_name} → {to_department_name}
  • Désignation: {from_designation_name} → {to_designation_name}
  • Raison: {reason}

Veuillez contacter les RH pour plus de détails concernant votre transfert et les arrangements nécessaires.

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'הודעת העברת עובד - {employee_name}', + 'content' => '

יקר/ה {employee_name},

אנו כותבים להודיע לך שהועברת בתוקף מ-{effective_date}.

פרטי ההעברה:

  • תאריך העברה: {transfer_date}
  • תאריך תוקף: {effective_date}
  • מחלקה: {from_department_name} → {to_department_name}
  • תפקיד: {from_designation_name} → {to_designation_name}
  • סיבה: {reason}

אנא צור קשר עם משאבי אנוש לפרטים נוספים לגבי ההעברה שלך וכל הסדר נדרש.

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Notifica di Trasferimento Dipendente - {employee_name}', + 'content' => '

Caro/a {employee_name},

Ti scriviamo per informarti che sei stato/a trasferito/a con effetto dal {effective_date}.

Dettagli del Trasferimento:

  • Data di Trasferimento: {transfer_date}
  • Data Effettiva: {effective_date}
  • Dipartimento: {from_department_name} → {to_department_name}
  • Designazione: {from_designation_name} → {to_designation_name}
  • Motivo: {reason}

Si prega di contattare l\'HR per ulteriori dettagli riguardo al tuo trasferimento e qualsiasi disposizione necessaria.

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '従業員異動通知 - {employee_name}', + 'content' => '

{employee_name}様、

{effective_date}付けで異動となりましたことをお知らせいたします。

異動の詳細:

  • 異動日: {transfer_date}
  • 発効日: {effective_date}
  • 部門: {from_department_name} → {to_department_name}
  • 役職: {from_designation_name} → {to_designation_name}
  • 理由: {reason}

異動に関する詳細および必要な手配については、人事部にお問い合わせください。

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Werknemersoverplaatsing Kennisgeving - {employee_name}', + 'content' => '

Beste {employee_name},

We schrijven je om te informeren dat je met ingang van {effective_date} bent overgeplaatst.

Overplaatsingsdetails:

  • Overplaatsingsdatum: {transfer_date}
  • Ingangsdatum: {effective_date}
  • Afdeling: {from_department_name} → {to_department_name}
  • Aanduiding: {from_designation_name} → {to_designation_name}
  • Reden: {reason}

Neem contact op met HR voor meer details over je overplaatsing en eventuele noodzakelijke regelingen.

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Powiadomienie o Przeniesieniu Pracownika - {employee_name}', + 'content' => '

Drogi/a {employee_name},

Piszemy, aby poinformować Cię, że zostałeś/aś przeniesiony/a z dniem {effective_date}.

Szczegóły Przeniesienia:

  • Data Przeniesienia: {transfer_date}
  • Data Obowiązywania: {effective_date}
  • Dział: {from_department_name} → {to_department_name}
  • Stanowisko: {from_designation_name} → {to_designation_name}
  • Powód: {reason}

Prosimy o kontakt z HR w celu uzyskania dalszych szczegółów dotyczących Twojego przeniesienia i wszelkich niezbędnych ustaleń.

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Notificação de Transferência de Funcionário - {employee_name}', + 'content' => '

Caro/a {employee_name},

Escrevemos para informá-lo de que você foi transferido com efeito a partir de {effective_date}.

Detalhes da Transferência:

  • Data de Transferência: {transfer_date}
  • Data Efetiva: {effective_date}
  • Departamento: {from_department_name} → {to_department_name}
  • Designação: {from_designation_name} → {to_designation_name}
  • Motivo: {reason}

Por favor, entre em contato com o RH para mais detalhes sobre sua transferência e quaisquer arranjos necessários.

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Notificação de Transferência de Funcionário - {employee_name}', + 'content' => '

Caro/a {employee_name},

Escrevemos para informá-lo de que você foi transferido com efeito a partir de {effective_date}.

Detalhes da Transferência:

  • Data de Transferência: {transfer_date}
  • Data Efetiva: {effective_date}
  • Departamento: {from_department_name} → {to_department_name}
  • Designação: {from_designation_name} → {to_designation_name}
  • Motivo: {reason}

Por favor, entre em contato com o RH para mais detalhes sobre sua transferência e quaisquer arranjos necessários.

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Уведомление о Переводе Сотрудника - {employee_name}', + 'content' => '

Уважаемый/ая {employee_name},

Мы пишем, чтобы сообщить вам, что вы были переведены с {effective_date}.

Детали Перевода:

  • Дата Перевода: {transfer_date}
  • Дата Вступления в Силу: {effective_date}
  • Отдел: {from_department_name} → {to_department_name}
  • Должность: {from_designation_name} → {to_designation_name}
  • Причина: {reason}

Пожалуйста, свяжитесь с отделом кадров для получения дополнительной информации о вашем переводе и необходимых мероприятиях.

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => 'Çalışan Transferi Bildirimi - {employee_name}', + 'content' => '

Sayın {employee_name},

{effective_date} tarihinden itibaren geçerli olmak üzere transfer edildiğinizi bildirmek için yazıyoruz.

Transfer Detayları:

  • Transfer Tarihi: {transfer_date}
  • Geçerlilik Tarihi: {effective_date}
  • Departman: {from_department_name} → {to_department_name}
  • Unvan: {from_designation_name} → {to_designation_name}
  • Sebep: {reason}

Lütfen transferiniz ve gerekli düzenlemeler hakkında daha fazla bilgi için İK ile iletişime geçin.

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '员工调动通知 - {employee_name}', + 'content' => '

尊敬的 {employee_name}

我们写信通知您,您已被调动,生效日期为{effective_date}

调动详情:

  • 调动日期:{transfer_date}
  • 生效日期:{effective_date}
  • 部门:{from_department_name} → {to_department_name}
  • 职位:{from_designation_name} → {to_designation_name}
  • 原因:{reason}

请联系HR部门了解有关您调动的更多详情和任何必要的安排。

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'Employee Contract', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Employment Contract - {contract_number}', + 'content' => '

Dear {employee_name},

We are pleased to provide you with your employment contract details.

Contract Details:

  • Contract Number: {contract_number}
  • Contract Type: {contract_type}
  • Start Date: {start_date}
  • End Date: {end_date}
  • Basic Salary: {basic_salary}

Please review the contract carefully. If you have any questions or concerns, please contact HR.

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Contrato de Empleo - {contract_number}', + 'content' => '

Estimado/a {employee_name},

Nos complace proporcionarle los detalles de su contrato de empleo.

Detalles del Contrato:

  • Número de Contrato: {contract_number}
  • Tipo de Contrato: {contract_type}
  • Fecha de Inicio: {start_date}
  • Fecha de Fin: {end_date}
  • Salario Básico: {basic_salary}

Por favor, revise el contrato cuidadosamente. Si tiene alguna pregunta o inquietud, contacte a RRHH.

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'عقد العمل - {contract_number}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

يسعدنا تزويدك بتفاصيل عقد العمل الخاص بك.

تفاصيل العقد:

  • رقم العقد: {contract_number}
  • نوع العقد: {contract_type}
  • تاريخ البدء: {start_date}
  • تاريخ الانتهاء: {end_date}
  • الراتب الأساسي: {basic_salary}

يرجى مراجعة العقد بعناية. إذا كان لديك أي أسئلة أو مخاوف، يرجى الاتصال بالموارد البشرية.

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Ansættelseskontrakt - {contract_number}', + 'content' => '

Kære {employee_name},

Vi er glade for at give dig dine ansættelseskontraktdetaljer.

Kontraktdetaljer:

  • Kontraktnummer: {contract_number}
  • Kontrakttype: {contract_type}
  • Startdato: {start_date}
  • Slutdato: {end_date}
  • Grundløn: {basic_salary}

Gennemgå venligst kontrakten omhyggeligt. Hvis du har spørgsmål eller bekymringer, kontakt HR.

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Arbeitsvertrag - {contract_number}', + 'content' => '

Liebe/r {employee_name},

Wir freuen uns, Ihnen Ihre Arbeitsvertragsdetails zur Verfügung zu stellen.

Vertragsdetails:

  • Vertragsnummer: {contract_number}
  • Vertragsart: {contract_type}
  • Startdatum: {start_date}
  • Enddatum: {end_date}
  • Grundgehalt: {basic_salary}

Bitte prüfen Sie den Vertrag sorgfältig. Bei Fragen oder Bedenken wenden Sie sich bitte an die Personalabteilung.

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Contrat de Travail - {contract_number}', + 'content' => '

Cher/Chère {employee_name},

Nous sommes heureux de vous fournir les détails de votre contrat de travail.

Détails du Contrat:

  • Numéro de Contrat: {contract_number}
  • Type de Contrat: {contract_type}
  • Date de Début: {start_date}
  • Date de Fin: {end_date}
  • Salaire de Base: {basic_salary}

Veuillez examiner attentivement le contrat. Si vous avez des questions ou des préoccupations, veuillez contacter les RH.

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'חוזה עבודה - {contract_number}', + 'content' => '

יקר/ה {employee_name},

אנו שמחים לספק לך את פרטי חוזה העבודה שלך.

פרטי החוזה:

  • מספר חוזה: {contract_number}
  • סוג חוזה: {contract_type}
  • תאריך התחלה: {start_date}
  • תאריך סיום: {end_date}
  • משכורת בסיס: {basic_salary}

אנא עיין בחוזה בקפידה. אם יש לך שאלות או חששות, אנא צור קשר עם משאבי אנוש.

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Contratto di Lavoro - {contract_number}', + 'content' => '

Caro/a {employee_name},

Siamo lieti di fornirti i dettagli del tuo contratto di lavoro.

Dettagli del Contratto:

  • Numero Contratto: {contract_number}
  • Tipo di Contratto: {contract_type}
  • Data di Inizio: {start_date}
  • Data di Fine: {end_date}
  • Stipendio Base: {basic_salary}

Si prega di esaminare attentamente il contratto. Per domande o dubbi, contattare l\'HR.

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '雇用契約 - {contract_number}', + 'content' => '

{employee_name}様、

雇用契約の詳細をお知らせいたします。

契約の詳細:

  • 契約番号: {contract_number}
  • 契約タイプ: {contract_type}
  • 開始日: {start_date}
  • 終了日: {end_date}
  • 基本給: {basic_salary}

契約内容を注意深くご確認ください。ご質問やご不明な点がございましたら、人事部にお問い合わせください。

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Arbeidscontract - {contract_number}', + 'content' => '

Beste {employee_name},

We zijn verheugd je de details van je arbeidscontract te verstrekken.

Contractdetails:

  • Contractnummer: {contract_number}
  • Contracttype: {contract_type}
  • Startdatum: {start_date}
  • Einddatum: {end_date}
  • Basissalaris: {basic_salary}

Bekijk het contract zorgvuldig. Als je vragen of zorgen hebt, neem dan contact op met HR.

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Umowa o Pracę - {contract_number}', + 'content' => '

Drogi/a {employee_name},

Z przyjemnością przekazujemy szczegóły Twojej umowy o pracę.

Szczegóły Umowy:

  • Numer Umowy: {contract_number}
  • Typ Umowy: {contract_type}
  • Data Rozpoczęcia: {start_date}
  • Data Zakończenia: {end_date}
  • Wynagrodzenie Podstawowe: {basic_salary}

Prosimy o dokładne zapoznanie się z umową. W przypadku pytań lub wątpliwości prosimy o kontakt z HR.

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Contrato de Trabalho - {contract_number}', + 'content' => '

Caro/a {employee_name},

Temos o prazer de fornecer os detalhes do seu contrato de trabalho.

Detalhes do Contrato:

  • Número do Contrato: {contract_number}
  • Tipo de Contrato: {contract_type}
  • Data de Início: {start_date}
  • Data de Término: {end_date}
  • Salário Básico: {basic_salary}

Por favor, revise o contrato cuidadosamente. Se tiver alguma dúvida ou preocupação, entre em contato com o RH.

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Contrato de Trabalho - {contract_number}', + 'content' => '

Caro/a {employee_name},

Temos o prazer de fornecer os detalhes do seu contrato de trabalho.

Detalhes do Contrato:

  • Número do Contrato: {contract_number}
  • Tipo de Contrato: {contract_type}
  • Data de Início: {start_date}
  • Data de Término: {end_date}
  • Salário Básico: {basic_salary}

Por favor, revise o contrato cuidadosamente. Se tiver alguma dúvida ou preocupação, entre em contato com o RH.

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Трудовой Договор - {contract_number}', + 'content' => '

Уважаемый/ая {employee_name},

Мы рады предоставить вам детали вашего трудового договора.

Детали Договора:

  • Номер Договора: {contract_number}
  • Тип Договора: {contract_type}
  • Дата Начала: {start_date}
  • Дата Окончания: {end_date}
  • Базовая Зарплата: {basic_salary}

Пожалуйста, внимательно ознакомьтесь с договором. Если у вас есть вопросы или сомнения, свяжитесь с отделом кадров.

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => 'İş Sözleşmesi - {contract_number}', + 'content' => '

Sayın {employee_name},

İş sözleşmenizin detaylarını size sunmaktan mutluluk duyuyoruz.

Sözleşme Detayları:

  • Sözleşme Numarası: {contract_number}
  • Sözleşme Türü: {contract_type}
  • Başlangıç Tarihi: {start_date}
  • Bitiş Tarihi: {end_date}
  • Temel Maaş: {basic_salary}

Lütfen sözleşmeyi dikkatlice inceleyin. Herhangi bir sorunuz veya endişeniz varsa, İK ile iletişime geçin.

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '雇佣合同 - {contract_number}', + 'content' => '

尊敬的 {employee_name}

我们很高兴为您提供雇佣合同详情。

合同详情:

  • 合同编号:{contract_number}
  • 合同类型:{contract_type}
  • 开始日期:{start_date}
  • 结束日期:{end_date}
  • 基本工资:{basic_salary}

请仔细审阅合同。如有任何疑问或顾虑,请联系HR部门。

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'New Leave Request', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Leave Request Submitted - {employee_name}', + 'content' => '

Dear HR Team,

A new leave request has been submitted by {employee_name}.

Leave Request Details:

  • Employee Name: {employee_name}
  • Leave Type: {leave_type}
  • Start Date: {start_date}
  • End Date: {end_date}
  • Total Days: {total_days}
  • Reason: {reason}

Please review and take appropriate action on this leave request.

Best regards,
{app_name} System

', + ], + 'es' => [ + 'subject' => 'Solicitud de Permiso Enviada - {employee_name}', + 'content' => '

Estimado Equipo de RRHH,

Se ha enviado una nueva solicitud de permiso por {employee_name}.

Detalles de la Solicitud de Permiso:

  • Nombre del Empleado: {employee_name}
  • Tipo de Permiso: {leave_type}
  • Fecha de Inicio: {start_date}
  • Fecha de Fin: {end_date}
  • Total de Días: {total_days}
  • Motivo: {reason}

Por favor, revise y tome las medidas apropiadas sobre esta solicitud de permiso.

Saludos cordiales,
Sistema {app_name}

', + ], + 'ar' => [ + 'subject' => 'طلب إجازة مقدم - {employee_name}', + 'content' => '

عزيزي فريق الموارد البشرية،

تم تقديم طلب إجازة جديد من قبل {employee_name}.

تفاصيل طلب الإجازة:

  • اسم الموظف: {employee_name}
  • نوع الإجازة: {leave_type}
  • تاريخ البدء: {start_date}
  • تاريخ الانتهاء: {end_date}
  • إجمالي الأيام: {total_days}
  • السبب: {reason}

يرجى المراجعة واتخاذ الإجراء المناسب بشأن طلب الإجازة هذا.

مع أطيب التحيات،
نظام {app_name}

', + ], + 'da' => [ + 'subject' => 'Orlovsanmodning Indsendt - {employee_name}', + 'content' => '

Kære HR-team,

En ny orlovsanmodning er blevet indsendt af {employee_name}.

Orlovsanmodningsdetaljer:

  • Medarbejdernavn: {employee_name}
  • Orlovstype: {leave_type}
  • Startdato: {start_date}
  • Slutdato: {end_date}
  • Antal Dage: {total_days}
  • Årsag: {reason}

Gennemgå venligst og tag passende handling på denne orlovsanmodning.

Med venlig hilsen,
{app_name} System

', + ], + 'de' => [ + 'subject' => 'Urlaubsantrag Eingereicht - {employee_name}', + 'content' => '

Liebes HR-Team,

Ein neuer Urlaubsantrag wurde von {employee_name} eingereicht.

Urlaubsantragsdetails:

  • Mitarbeitername: {employee_name}
  • Urlaubsart: {leave_type}
  • Startdatum: {start_date}
  • Enddatum: {end_date}
  • Gesamttage: {total_days}
  • Grund: {reason}

Bitte prüfen Sie und ergreifen Sie geeignete Maßnahmen zu diesem Urlaubsantrag.

Mit freundlichen Grüßen,
{app_name} System

', + ], + 'fr' => [ + 'subject' => 'Demande de Congé Soumise - {employee_name}', + 'content' => '

Cher Équipe RH,

Une nouvelle demande de congé a été soumise par {employee_name}.

Détails de la Demande de Congé:

  • Nom de l\'Employé: {employee_name}
  • Type de Congé: {leave_type}
  • Date de Début: {start_date}
  • Date de Fin: {end_date}
  • Total de Jours: {total_days}
  • Raison: {reason}

Veuillez examiner et prendre les mesures appropriées concernant cette demande de congé.

Cordialement,
Système {app_name}

', + ], + 'he' => [ + 'subject' => 'בקשת חופשה הוגשה - {employee_name}', + 'content' => '

צוות משאבי אנוש יקר,

בקשת חופשה חדשה הוגשה על ידי {employee_name}.

פרטי בקשת החופשה:

  • שם העובד: {employee_name}
  • סוג חופשה: {leave_type}
  • תאריך התחלה: {start_date}
  • תאריך סיום: {end_date}
  • סך הכל ימים: {total_days}
  • סיבה: {reason}

אנא בדוק ונקוט בפעולה המתאימה לגבי בקשת חופשה זו.

בברכה,
מערכת {app_name}

', + ], + 'it' => [ + 'subject' => 'Richiesta di Permesso Inviata - {employee_name}', + 'content' => '

Caro Team HR,

Una nuova richiesta di permesso è stata inviata da {employee_name}.

Dettagli della Richiesta di Permesso:

  • Nome Dipendente: {employee_name}
  • Tipo di Permesso: {leave_type}
  • Data di Inizio: {start_date}
  • Data di Fine: {end_date}
  • Totale Giorni: {total_days}
  • Motivo: {reason}

Si prega di esaminare e prendere le misure appropriate su questa richiesta di permesso.

Cordiali saluti,
Sistema {app_name}

', + ], + 'ja' => [ + 'subject' => '休暇申請が提出されました - {employee_name}', + 'content' => '

人事部御中、

{employee_name}から新しい休暇申請が提出されました。

休暇申請の詳細:

  • 従業員名: {employee_name}
  • 休暇タイプ: {leave_type}
  • 開始日: {start_date}
  • 終了日: {end_date}
  • 合計日数: {total_days}
  • 理由: {reason}

この休暇申請を確認し、適切な対応をお願いいたします。

よろしくお願いいたします、
{app_name}システム

', + ], + 'nl' => [ + 'subject' => 'Verlofaanvraag Ingediend - {employee_name}', + 'content' => '

Beste HR-team,

Een nieuwe verlofaanvraag is ingediend door {employee_name}.

Verlofaanvraagdetails:

  • Werknemersnaam: {employee_name}
  • Verloftype: {leave_type}
  • Startdatum: {start_date}
  • Einddatum: {end_date}
  • Totaal Dagen: {total_days}
  • Reden: {reason}

Bekijk en neem passende actie op deze verlofaanvraag.

Met vriendelijke groet,
{app_name} Systeem

', + ], + 'pl' => [ + 'subject' => 'Wniosek Urlopowy Złożony - {employee_name}', + 'content' => '

Szanowny Zespół HR,

Nowy wniosek urlopowy został złożony przez {employee_name}.

Szczegóły Wniosku Urlopowego:

  • Imię i Nazwisko Pracownika: {employee_name}
  • Typ Urlopu: {leave_type}
  • Data Rozpoczęcia: {start_date}
  • Data Zakończenia: {end_date}
  • Łączna Liczba Dni: {total_days}
  • Powód: {reason}

Prosimy o przegląd i podjęcie odpowiednich działań w sprawie tego wniosku urlopowego.

Z poważaniem,
System {app_name}

', + ], + 'pt' => [ + 'subject' => 'Solicitação de Licença Enviada - {employee_name}', + 'content' => '

Prezada Equipe de RH,

Uma nova solicitação de licença foi enviada por {employee_name}.

Detalhes da Solicitação de Licença:

  • Nome do Funcionário: {employee_name}
  • Tipo de Licença: {leave_type}
  • Data de Início: {start_date}
  • Data de Término: {end_date}
  • Total de Dias: {total_days}
  • Motivo: {reason}

Por favor, revise e tome as medidas apropriadas sobre esta solicitação de licença.

Atenciosamente,
Sistema {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Solicitação de Licença Enviada - {employee_name}', + 'content' => '

Prezada Equipe de RH,

Uma nova solicitação de licença foi enviada por {employee_name}.

Detalhes da Solicitação de Licença:

  • Nome do Funcionário: {employee_name}
  • Tipo de Licença: {leave_type}
  • Data de Início: {start_date}
  • Data de Término: {end_date}
  • Total de Dias: {total_days}
  • Motivo: {reason}

Por favor, revise e tome as medidas apropriadas sobre esta solicitação de licença.

Atenciosamente,
Sistema {app_name}

', + ], + 'ru' => [ + 'subject' => 'Заявка на Отпуск Подана - {employee_name}', + 'content' => '

Уважаемая команда HR,

Новая заявка на отпуск была подана {employee_name}.

Детали Заявки на Отпуск:

  • Имя Сотрудника: {employee_name}
  • Тип Отпуска: {leave_type}
  • Дата Начала: {start_date}
  • Дата Окончания: {end_date}
  • Всего Дней: {total_days}
  • Причина: {reason}

Пожалуйста, рассмотрите и примите соответствующие меры по этой заявке на отпуск.

С уважением,
Система {app_name}

', + ], + 'tr' => [ + 'subject' => 'İzin Talebi Gönderildi - {employee_name}', + 'content' => '

Sayın İK Ekibi,

{employee_name} tarafından yeni bir izin talebi gönderildi.

İzin Talebi Detayları:

  • Çalışan Adı: {employee_name}
  • İzin Türü: {leave_type}
  • Başlangıç Tarihi: {start_date}
  • Bitiş Tarihi: {end_date}
  • Toplam Gün: {total_days}
  • Sebep: {reason}

Lütfen bu izin talebini inceleyin ve uygun işlemi yapın.

Saygılarımızla,
{app_name} Sistemi

', + ], + 'zh' => [ + 'subject' => '请假申请已提交 - {employee_name}', + 'content' => '

尊敬的HR团队,

{employee_name}提交了新的请假申请。

请假申请详情:

  • 员工姓名:{employee_name}
  • 请假类型:{leave_type}
  • 开始日期:{start_date}
  • 结束日期:{end_date}
  • 总天数:{total_days}
  • 原因:{reason}

请审核并对此请假申请采取适当措施。

此致,
{app_name} 系统

', + ], + ], + ], + [ + 'name' => 'Leave Request Status Change', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Leave Request {status} - {leave_type}', + 'content' => '

Dear {employee_name},

Your leave request has been {status} by the HR/Management.

Leave Request Details:

  • Leave Type: {leave_type}
  • Start Date: {start_date}
  • End Date: {end_date}
  • Total Days: {total_days}
  • Status: {status}
  • Reviewed By: {approved_by}
  • Reviewed Date: {approved_at}
  • Comments: {manager_comments}

If you have any questions regarding this decision, please contact the HR department.

Best regards,
{app_name} Team

', + ], + 'es' => [ + 'subject' => 'Solicitud de Permiso {status} - {leave_type}', + 'content' => '

Estimado/a {employee_name},

Su solicitud de permiso ha sido {status} por RRHH/Gerencia.

Detalles de la Solicitud:

  • Tipo de Permiso: {leave_type}
  • Fecha de Inicio: {start_date}
  • Fecha de Fin: {end_date}
  • Total de Días: {total_days}
  • Estado: {status}
  • Revisado Por: {approved_by}
  • Fecha de Revisión: {approved_at}
  • Comentarios: {manager_comments}

Si tiene alguna pregunta sobre esta decisión, contacte al departamento de RRHH.

Saludos cordiales,
Equipo de {app_name}

', + ], + 'ar' => [ + 'subject' => 'طلب الإجازة {status} - {leave_type}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

تم {status} طلب الإجازة الخاص بك من قبل الموارد البشرية/الإدارة.

تفاصيل طلب الإجازة:

  • نوع الإجازة: {leave_type}
  • تاريخ البدء: {start_date}
  • تاريخ الانتهاء: {end_date}
  • إجمالي الأيام: {total_days}
  • الحالة: {status}
  • تمت المراجعة بواسطة: {approved_by}
  • تاريخ المراجعة: {approved_at}
  • التعليقات: {manager_comments}

إذا كان لديك أي أسئلة بخصوص هذا القرار، يرجى الاتصال بقسم الموارد البشرية.

مع أطيب التحيات،
فريق {app_name}

', + ], + 'da' => [ + 'subject' => 'Orlovsanmodning {status} - {leave_type}', + 'content' => '

Kære {employee_name},

Din orlovsanmodning er blevet {status} af HR/Ledelsen.

Orlovsanmodningsdetaljer:

  • Orlovstype: {leave_type}
  • Startdato: {start_date}
  • Slutdato: {end_date}
  • Antal Dage: {total_days}
  • Status: {status}
  • Gennemgået Af: {approved_by}
  • Gennemgangsdato: {approved_at}
  • Kommentarer: {manager_comments}

Hvis du har spørgsmål til denne beslutning, kontakt HR-afdelingen.

Med venlig hilsen,
{app_name} Team

', + ], + 'de' => [ + 'subject' => 'Urlaubsantrag {status} - {leave_type}', + 'content' => '

Liebe/r {employee_name},

Ihr Urlaubsantrag wurde von der Personalabteilung/Geschäftsführung {status}.

Urlaubsantragsdetails:

  • Urlaubsart: {leave_type}
  • Startdatum: {start_date}
  • Enddatum: {end_date}
  • Gesamttage: {total_days}
  • Status: {status}
  • Überprüft Von: {approved_by}
  • Überprüfungsdatum: {approved_at}
  • Kommentare: {manager_comments}

Bei Fragen zu dieser Entscheidung wenden Sie sich bitte an die Personalabteilung.

Mit freundlichen Grüßen,
{app_name} Team

', + ], + 'fr' => [ + 'subject' => 'Demande de Congé {status} - {leave_type}', + 'content' => '

Cher/Chère {employee_name},

Votre demande de congé a été {status} par les RH/Direction.

Détails de la Demande:

  • Type de Congé: {leave_type}
  • Date de Début: {start_date}
  • Date de Fin: {end_date}
  • Total de Jours: {total_days}
  • Statut: {status}
  • Examiné Par: {approved_by}
  • Date d\'Examen: {approved_at}
  • Commentaires: {manager_comments}

Si vous avez des questions concernant cette décision, veuillez contacter le département RH.

Cordialement,
Équipe {app_name}

', + ], + 'he' => [ + 'subject' => 'בקשת חופשה {status} - {leave_type}', + 'content' => '

יקר/ה {employee_name},

בקשת החופשה שלך {status} על ידי משאבי אנוש/הנהלה.

פרטי בקשת החופשה:

  • סוג חופשה: {leave_type}
  • תאריך התחלה: {start_date}
  • תאריך סיום: {end_date}
  • סך הכל ימים: {total_days}
  • סטטוס: {status}
  • נבדק על ידי: {approved_by}
  • תאריך בדיקה: {approved_at}
  • הערות: {manager_comments}

אם יש לך שאלות לגבי החלטה זו, אנא צור קשר עם מחלקת משאבי אנוש.

בברכה,
צוות {app_name}

', + ], + 'it' => [ + 'subject' => 'Richiesta di Permesso {status} - {leave_type}', + 'content' => '

Caro/a {employee_name},

La tua richiesta di permesso è stata {status} dall\'HR/Direzione.

Dettagli della Richiesta:

  • Tipo di Permesso: {leave_type}
  • Data di Inizio: {start_date}
  • Data di Fine: {end_date}
  • Totale Giorni: {total_days}
  • Stato: {status}
  • Esaminato Da: {approved_by}
  • Data di Esame: {approved_at}
  • Commenti: {manager_comments}

Per domande su questa decisione, contattare il dipartimento HR.

Cordiali saluti,
Team {app_name}

', + ], + 'ja' => [ + 'subject' => '休暇申請{status} - {leave_type}', + 'content' => '

{employee_name}様、

あなたの休暇申請はHR/管理部門により{status}されました。

休暇申請の詳細:

  • 休暇タイプ: {leave_type}
  • 開始日: {start_date}
  • 終了日: {end_date}
  • 合計日数: {total_days}
  • ステータス: {status}
  • 承認者: {approved_by}
  • 承認日: {approved_at}
  • コメント: {manager_comments}

この決定について質問がある場合は、人事部にお問い合わせください。

よろしくお願いいたします、
{app_name}チーム

', + ], + 'nl' => [ + 'subject' => 'Verlofaanvraag {status} - {leave_type}', + 'content' => '

Beste {employee_name},

Je verlofaanvraag is {status} door HR/Management.

Verlofaanvraagdetails:

  • Verloftype: {leave_type}
  • Startdatum: {start_date}
  • Einddatum: {end_date}
  • Totaal Dagen: {total_days}
  • Status: {status}
  • Beoordeeld Door: {approved_by}
  • Beoordelingsdatum: {approved_at}
  • Opmerkingen: {manager_comments}

Voor vragen over deze beslissing, neem contact op met de HR-afdeling.

Met vriendelijke groet,
{app_name} Team

', + ], + 'pl' => [ + 'subject' => 'Wniosek Urlopowy {status} - {leave_type}', + 'content' => '

Drogi/a {employee_name},

Twój wniosek urlopowy został {status} przez HR/Zarząd.

Szczegóły Wniosku:

  • Typ Urlopu: {leave_type}
  • Data Rozpoczęcia: {start_date}
  • Data Zakończenia: {end_date}
  • Łączna Liczba Dni: {total_days}
  • Status: {status}
  • Sprawdzone Przez: {approved_by}
  • Data Sprawdzenia: {approved_at}
  • Komentarze: {manager_comments}

W przypadku pytań dotyczących tej decyzji, skontaktuj się z działem HR.

Z poważaniem,
Zespół {app_name}

', + ], + 'pt' => [ + 'subject' => 'Solicitação de Licença {status} - {leave_type}', + 'content' => '

Caro/a {employee_name},

Sua solicitação de licença foi {status} pelo RH/Gerência.

Detalhes da Solicitação:

  • Tipo de Licença: {leave_type}
  • Data de Início: {start_date}
  • Data de Término: {end_date}
  • Total de Dias: {total_days}
  • Status: {status}
  • Revisado Por: {approved_by}
  • Data de Revisão: {approved_at}
  • Comentários: {manager_comments}

Se tiver dúvidas sobre esta decisão, entre em contato com o departamento de RH.

Atenciosamente,
Equipe {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Solicitação de Licença {status} - {leave_type}', + 'content' => '

Caro/a {employee_name},

Sua solicitação de licença foi {status} pelo RH/Gerência.

Detalhes da Solicitação:

  • Tipo de Licença: {leave_type}
  • Data de Início: {start_date}
  • Data de Término: {end_date}
  • Total de Dias: {total_days}
  • Status: {status}
  • Revisado Por: {approved_by}
  • Data de Revisão: {approved_at}
  • Comentários: {manager_comments}

Se tiver dúvidas sobre esta decisão, entre em contato com o departamento de RH.

Atenciosamente,
Equipe {app_name}

', + ], + 'ru' => [ + 'subject' => 'Заявка на Отпуск {status} - {leave_type}', + 'content' => '

Уважаемый/ая {employee_name},

Ваша заявка на отпуск была {status} отделом кадров/руководством.

Детали Заявки:

  • Тип Отпуска: {leave_type}
  • Дата Начала: {start_date}
  • Дата Окончания: {end_date}
  • Всего Дней: {total_days}
  • Статус: {status}
  • Рассмотрено: {approved_by}
  • Дата Рассмотрения: {approved_at}
  • Комментарии: {manager_comments}

Если у вас есть вопросы по этому решению, свяжитесь с отделом кадров.

С уважением,
Команда {app_name}

', + ], + 'tr' => [ + 'subject' => 'İzin Talebi {status} - {leave_type}', + 'content' => '

Sayın {employee_name},

İzin talebiniz İK/Yönetim tarafından {status}.

İzin Talebi Detayları:

  • İzin Türü: {leave_type}
  • Başlangıç Tarihi: {start_date}
  • Bitiş Tarihi: {end_date}
  • Toplam Gün: {total_days}
  • Durum: {status}
  • İnceleyen: {approved_by}
  • İnceleme Tarihi: {approved_at}
  • Yorumlar: {manager_comments}

Bu karar hakkında sorularınız varsa, İK departmanı ile iletişime geçin.

Saygılarımızla,
{app_name} Ekibi

', + ], + 'zh' => [ + 'subject' => '请假申请{status} - {leave_type}', + 'content' => '

尊敬的 {employee_name}

您的请假申请已被HR/管理层{status}

请假申请详情:

  • 请假类型:{leave_type}
  • 开始日期:{start_date}
  • 结束日期:{end_date}
  • 总天数:{total_days}
  • 状态:{status}
  • 审核人:{approved_by}
  • 审核日期:{approved_at}
  • 备注:{manager_comments}

如对此决定有任何疑问,请联系HR部门。

此致,
{app_name} 团队

', + ], + ], + ], + [ + 'name' => 'Offer Create', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Job Offer - {position} at {app_name}', + 'content' => '

Dear {candidate_name},

We are pleased to extend an offer of employment for the position of {position} at {app_name}.

Offer Details:

  • Position: {position}
  • Department: {department_name}
  • Salary: {salary}
  • Start Date: {start_date}
  • Offer Date: {offer_date}
  • Expiration Date: {expiration_date}
  • Status: {status}

Please review this offer carefully and respond by {expiration_date}.

We look forward to welcoming you to our team!

Best regards,
{app_name} HR Team

', + ], + 'es' => [ + 'subject' => 'Oferta de Trabajo - {position} en {app_name}', + 'content' => '

Estimado/a {candidate_name},

Nos complace extender una oferta de empleo para el puesto de {position} en {app_name}.

Detalles de la Oferta:

  • Puesto: {position}
  • Departamento: {department_name}
  • Salario: {salary}
  • Fecha de Inicio: {start_date}
  • Fecha de Oferta: {offer_date}
  • Fecha de Vencimiento: {expiration_date}
  • Estado: {status}

Por favor, revise esta oferta cuidadosamente y responda antes del {expiration_date}.

¡Esperamos darle la bienvenida a nuestro equipo!

Saludos cordiales,
Equipo de RRHH de {app_name}

', + ], + 'ar' => [ + 'subject' => 'عرض عمل - {position} في {app_name}', + 'content' => '

عزيزي/عزيزتي {candidate_name}،

يسعدنا تقديم عرض عمل لمنصب {position} في {app_name}.

تفاصيل العرض:

  • المنصب: {position}
  • القسم: {department_name}
  • الراتب: {salary}
  • تاريخ البدء: {start_date}
  • تاريخ العرض: {offer_date}
  • تاريخ الانتهاء: {expiration_date}
  • الحالة: {status}

يرجى مراجعة هذا العرض بعناية والرد قبل {expiration_date}.

نتطلع للترحيب بك في فريقنا!

مع أطيب التحيات،
فريق الموارد البشرية في {app_name}

', + ], + 'da' => [ + 'subject' => 'Jobtilbud - {position} hos {app_name}', + 'content' => '

Kære {candidate_name},

Vi er glade for at tilbyde dig ansættelse som {position} hos {app_name}.

Tilbudsdetaljer:

  • Stilling: {position}
  • Afdeling: {department_name}
  • Løn: {salary}
  • Startdato: {start_date}
  • Tilbudsdato: {offer_date}
  • Udløbsdato: {expiration_date}
  • Status: {status}

Gennemgå venligst dette tilbud omhyggeligt og svar senest {expiration_date}.

Vi ser frem til at byde dig velkommen i vores team!

Med venlig hilsen,
{app_name} HR Team

', + ], + 'de' => [ + 'subject' => 'Stellenangebot - {position} bei {app_name}', + 'content' => '

Liebe/r {candidate_name},

Wir freuen uns, Ihnen ein Stellenangebot für die Position {position} bei {app_name} zu unterbreiten.

Angebotsdetails:

  • Position: {position}
  • Abteilung: {department_name}
  • Gehalt: {salary}
  • Startdatum: {start_date}
  • Angebotsdatum: {offer_date}
  • Ablaufdatum: {expiration_date}
  • Status: {status}

Bitte prüfen Sie dieses Angebot sorgfältig und antworten Sie bis zum {expiration_date}.

Wir freuen uns darauf, Sie in unserem Team willkommen zu heißen!

Mit freundlichen Grüßen,
{app_name} HR Team

', + ], + 'fr' => [ + 'subject' => 'Offre d\'Emploi - {position} chez {app_name}', + 'content' => '

Cher/Chère {candidate_name},

Nous sommes heureux de vous proposer un emploi pour le poste de {position} chez {app_name}.

Détails de l\'Offre:

  • Poste: {position}
  • Département: {department_name}
  • Salaire: {salary}
  • Date de Début: {start_date}
  • Date d\'Offre: {offer_date}
  • Date d\'Expiration: {expiration_date}
  • Statut: {status}

Veuillez examiner attentivement cette offre et répondre avant le {expiration_date}.

Nous sommes impatients de vous accueillir dans notre équipe!

Cordialement,
Équipe RH de {app_name}

', + ], + 'he' => [ + 'subject' => 'הצעת עבודה - {position} ב-{app_name}', + 'content' => '

יקר/ה {candidate_name},

אנו שמחים להציע לך משרה בתפקיד {position} ב-{app_name}.

פרטי ההצעה:

  • תפקיד: {position}
  • מחלקה: {department_name}
  • משכורת: {salary}
  • תאריך התחלה: {start_date}
  • תאריך הצעה: {offer_date}
  • תאריך תפוגה: {expiration_date}
  • סטטוס: {status}

אנא עיין בהצעה זו בקפידה והשב עד {expiration_date}.

אנו מצפים לקבל אותך לצוות שלנו!

בברכה,
צוות משאבי אנוש {app_name}

', + ], + 'it' => [ + 'subject' => 'Offerta di Lavoro - {position} presso {app_name}', + 'content' => '

Caro/a {candidate_name},

Siamo lieti di offrirti un impiego per la posizione di {position} presso {app_name}.

Dettagli dell\'Offerta:

  • Posizione: {position}
  • Dipartimento: {department_name}
  • Stipendio: {salary}
  • Data di Inizio: {start_date}
  • Data Offerta: {offer_date}
  • Data di Scadenza: {expiration_date}
  • Stato: {status}

Si prega di esaminare attentamente questa offerta e rispondere entro il {expiration_date}.

Non vediamo l\'ora di darti il benvenuto nel nostro team!

Cordiali saluti,
Team HR di {app_name}

', + ], + 'ja' => [ + 'subject' => '採用オファー - {app_name}の{position}', + 'content' => '

{candidate_name}様、

{app_name}の{position}職への採用オファーをお送りいたします。

オファー詳細:

  • 役職: {position}
  • 部門: {department_name}
  • 給与: {salary}
  • 入社日: {start_date}
  • オファー日: {offer_date}
  • 有効期限: {expiration_date}
  • ステータス: {status}

このオファーを注意深くご確認の上、{expiration_date}までにご返答をお願いいたします。

あなたをチームに迎えることを楽しみにしております!

よろしくお願いいたします、
{app_name} 人事チーム

', + ], + 'nl' => [ + 'subject' => 'Werkaanbod - {position} bij {app_name}', + 'content' => '

Beste {candidate_name},

We zijn verheugd je een werkaanbod te doen voor de functie van {position} bij {app_name}.

Aanboddetails:

  • Functie: {position}
  • Afdeling: {department_name}
  • Salaris: {salary}
  • Startdatum: {start_date}
  • Aanboddatum: {offer_date}
  • Vervaldatum: {expiration_date}
  • Status: {status}

Bekijk dit aanbod zorgvuldig en reageer uiterlijk {expiration_date}.

We kijken ernaar uit je te verwelkomen in ons team!

Met vriendelijke groet,
{app_name} HR Team

', + ], + 'pl' => [ + 'subject' => 'Oferta Pracy - {position} w {app_name}', + 'content' => '

Drogi/a {candidate_name},

Z przyjemnością składamy ofertę zatrudnienia na stanowisko {position} w {app_name}.

Szczegóły Oferty:

  • Stanowisko: {position}
  • Dział: {department_name}
  • Wynagrodzenie: {salary}
  • Data Rozpoczęcia: {start_date}
  • Data Oferty: {offer_date}
  • Data Wygaśnięcia: {expiration_date}
  • Status: {status}

Prosimy o dokładne zapoznanie się z tą ofertą i odpowiedź do {expiration_date}.

Cieszymy się na powitanie Cię w naszym zespole!

Z poważaniem,
Zespół HR {app_name}

', + ], + 'pt' => [ + 'subject' => 'Oferta de Emprego - {position} na {app_name}', + 'content' => '

Caro/a {candidate_name},

Temos o prazer de estender uma oferta de emprego para a posição de {position} na {app_name}.

Detalhes da Oferta:

  • Posição: {position}
  • Departamento: {department_name}
  • Salário: {salary}
  • Data de Início: {start_date}
  • Data da Oferta: {offer_date}
  • Data de Expiração: {expiration_date}
  • Status: {status}

Por favor, revise esta oferta cuidadosamente e responda até {expiration_date}.

Estamos ansiosos para recebê-lo em nossa equipe!

Atenciosamente,
Equipe de RH da {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Oferta de Emprego - {position} na {app_name}', + 'content' => '

Caro/a {candidate_name},

Temos o prazer de estender uma oferta de emprego para a posição de {position} na {app_name}.

Detalhes da Oferta:

  • Posição: {position}
  • Departamento: {department_name}
  • Salário: {salary}
  • Data de Início: {start_date}
  • Data da Oferta: {offer_date}
  • Data de Expiração: {expiration_date}
  • Status: {status}

Por favor, revise esta oferta cuidadosamente e responda até {expiration_date}.

Estamos ansiosos para recebê-lo em nossa equipe!

Atenciosamente,
Equipe de RH da {app_name}

', + ], + 'ru' => [ + 'subject' => 'Предложение о Работе - {position} в {app_name}', + 'content' => '

Уважаемый/ая {candidate_name},

Мы рады предложить вам работу на должность {position} в {app_name}.

Детали Предложения:

  • Должность: {position}
  • Отдел: {department_name}
  • Зарплата: {salary}
  • Дата Начала: {start_date}
  • Дата Предложения: {offer_date}
  • Срок Действия: {expiration_date}
  • Статус: {status}

Пожалуйста, внимательно ознакомьтесь с этим предложением и ответьте до {expiration_date}.

Мы с нетерпением ждем возможности приветствовать вас в нашей команде!

С уважением,
Команда HR {app_name}

', + ], + 'tr' => [ + 'subject' => 'İş Teklifi - {app_name}\'de {position}', + 'content' => '

Sayın {candidate_name},

{app_name}\'de {position} pozisyonu için size bir iş teklifi sunmaktan mutluluk duyuyoruz.

Teklif Detayları:

  • Pozisyon: {position}
  • Departman: {department_name}
  • Maaş: {salary}
  • Başlangıç Tarihi: {start_date}
  • Teklif Tarihi: {offer_date}
  • Son Geçerlilik Tarihi: {expiration_date}
  • Durum: {status}

Lütfen bu teklifi dikkatlice inceleyin ve {expiration_date} tarihine kadar yanıt verin.

Sizi ekibimizde görmek için sabırsızlanıyoruz!

Saygılarımızla,
{app_name} İK Ekibi

', + ], + 'zh' => [ + 'subject' => '工作邀请 - {app_name}的{position}', + 'content' => '

尊敬的 {candidate_name}

我们很高兴向您提供{app_name}的{position}职位的工作邀请。

邀请详情:

  • 职位:{position}
  • 部门:{department_name}
  • 薪资:{salary}
  • 入职日期:{start_date}
  • 邀请日期:{offer_date}
  • 截止日期:{expiration_date}
  • 状态:{status}

请仔细查看此邀请并在{expiration_date}之前回复。

我们期待欢迎您加入我们的团队!

此致,
{app_name} 人力资源团队

', + ], + ], + ], + [ + 'name' => 'Offer Status Update', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Offer Status Update - {position}', + 'content' => '

Dear {candidate_name},

We would like to inform you that the status of your job offer for the position of {position} has been updated by our HR team.

Offer Status Details:

  • Position: {position}
  • Department: {department_name}
  • Current Status: {status}
  • Offer Date: {offer_date}
  • Expiration Date: {expiration_date}

If you have any questions regarding this update, please contact our HR department.

Best regards,
{app_name} HR Team

', + ], + 'es' => [ + 'subject' => 'Actualización del Estado de la Oferta - {position}', + 'content' => '

Estimado/a {candidate_name},

Nos gustaría informarle que el estado de su oferta de trabajo para el puesto de {position} ha sido actualizado por nuestro equipo de RRHH.

Detalles del Estado de la Oferta:

  • Puesto: {position}
  • Departamento: {department_name}
  • Estado Actual: {status}
  • Fecha de Oferta: {offer_date}
  • Fecha de Vencimiento: {expiration_date}

Si tiene alguna pregunta sobre esta actualización, contacte a nuestro departamento de RRHH.

Saludos cordiales,
Equipo de RRHH de {app_name}

', + ], + 'ar' => [ + 'subject' => 'تحديث حالة العرض - {position}', + 'content' => '

عزيزي/عزيزتي {candidate_name}،

نود إبلاغك بأن حالة عرض العمل الخاص بك لمنصب {position} قد تم تحديثها من قبل فريق الموارد البشرية لدينا.

تفاصيل حالة العرض:

  • المنصب: {position}
  • القسم: {department_name}
  • الحالة الحالية: {status}
  • تاريخ العرض: {offer_date}
  • تاريخ الانتهاء: {expiration_date}

إذا كان لديك أي أسئلة بخصوص هذا التحديث، يرجى الاتصال بقسم الموارد البشرية.

مع أطيب التحيات،
فريق الموارد البشرية في {app_name}

', + ], + 'da' => [ + 'subject' => 'Opdatering af Tilbudsstatus - {position}', + 'content' => '

Kære {candidate_name},

Vi vil gerne informere dig om, at status på dit jobtilbud for stillingen som {position} er blevet opdateret af vores HR-team.

Tilbudsstatusdetaljer:

  • Stilling: {position}
  • Afdeling: {department_name}
  • Nuværende Status: {status}
  • Tilbudsdato: {offer_date}
  • Udløbsdato: {expiration_date}

Hvis du har spørgsmål til denne opdatering, kontakt vores HR-afdeling.

Med venlig hilsen,
{app_name} HR Team

', + ], + 'de' => [ + 'subject' => 'Aktualisierung des Angebotsstatus - {position}', + 'content' => '

Liebe/r {candidate_name},

Wir möchten Sie darüber informieren, dass der Status Ihres Stellenangebots für die Position {position} von unserem HR-Team aktualisiert wurde.

Angebotsstatusdetails:

  • Position: {position}
  • Abteilung: {department_name}
  • Aktueller Status: {status}
  • Angebotsdatum: {offer_date}
  • Ablaufdatum: {expiration_date}

Bei Fragen zu dieser Aktualisierung wenden Sie sich bitte an unsere Personalabteilung.

Mit freundlichen Grüßen,
{app_name} HR Team

', + ], + 'fr' => [ + 'subject' => 'Mise à Jour du Statut de l\'Offre - {position}', + 'content' => '

Cher/Chère {candidate_name},

Nous souhaitons vous informer que le statut de votre offre d\'emploi pour le poste de {position} a été mis à jour par notre équipe RH.

Détails du Statut de l\'Offre:

  • Poste: {position}
  • Département: {department_name}
  • Statut Actuel: {status}
  • Date d\'Offre: {offer_date}
  • Date d\'Expiration: {expiration_date}

Si vous avez des questions concernant cette mise à jour, veuillez contacter notre département RH.

Cordialement,
Équipe RH de {app_name}

', + ], + 'he' => [ + 'subject' => 'עדכון סטטוס הצעה - {position}', + 'content' => '

יקר/ה {candidate_name},

ברצוננו להודיע לך שהסטטוס של הצעת העבודה שלך לתפקיד {position} עודכן על ידי צוות משאבי האנוש שלנו.

פרטי סטטוס ההצעה:

  • תפקיד: {position}
  • מחלקה: {department_name}
  • סטטוס נוכחי: {status}
  • תאריך הצעה: {offer_date}
  • תאריך תפוגה: {expiration_date}

אם יש לך שאלות לגבי עדכון זה, אנא צור קשר עם מחלקת משאבי אנוש.

בברכה,
צוות משאבי אנוש {app_name}

', + ], + 'it' => [ + 'subject' => 'Aggiornamento Stato Offerta - {position}', + 'content' => '

Caro/a {candidate_name},

Desideriamo informarti che lo stato della tua offerta di lavoro per la posizione di {position} è stato aggiornato dal nostro team HR.

Dettagli dello Stato dell\'Offerta:

  • Posizione: {position}
  • Dipartimento: {department_name}
  • Stato Attuale: {status}
  • Data Offerta: {offer_date}
  • Data di Scadenza: {expiration_date}

Per domande su questo aggiornamento, contattare il nostro dipartimento HR.

Cordiali saluti,
Team HR di {app_name}

', + ], + 'ja' => [ + 'subject' => 'オファーステータス更新 - {position}', + 'content' => '

{candidate_name}様、

{position}職の採用オファーのステータスが人事チームにより更新されましたのでお知らせいたします。

オファーステータス詳細:

  • 役職: {position}
  • 部門: {department_name}
  • 現在のステータス: {status}
  • オファー日: {offer_date}
  • 有効期限: {expiration_date}

この更新に関してご質問がある場合は、人事部にお問い合わせください。

よろしくお願いいたします、
{app_name} 人事チーム

', + ], + 'nl' => [ + 'subject' => 'Update Aanbodstatus - {position}', + 'content' => '

Beste {candidate_name},

We willen je informeren dat de status van je werkaanbod voor de functie van {position} is bijgewerkt door ons HR-team.

Aanbodstatusdetails:

  • Functie: {position}
  • Afdeling: {department_name}
  • Huidige Status: {status}
  • Aanboddatum: {offer_date}
  • Vervaldatum: {expiration_date}

Voor vragen over deze update, neem contact op met onze HR-afdeling.

Met vriendelijke groet,
{app_name} HR Team

', + ], + 'pl' => [ + 'subject' => 'Aktualizacja Statusu Oferty - {position}', + 'content' => '

Drogi/a {candidate_name},

Chcielibyśmy poinformować, że status Twojej oferty pracy na stanowisko {position} został zaktualizowany przez nasz zespół HR.

Szczegóły Statusu Oferty:

  • Stanowisko: {position}
  • Dział: {department_name}
  • Aktualny Status: {status}
  • Data Oferty: {offer_date}
  • Data Wygaśnięcia: {expiration_date}

W przypadku pytań dotyczących tej aktualizacji, skontaktuj się z naszym działem HR.

Z poważaniem,
Zespół HR {app_name}

', + ], + 'pt' => [ + 'subject' => 'Atualização do Status da Oferta - {position}', + 'content' => '

Caro/a {candidate_name},

Gostaríamos de informá-lo de que o status de sua oferta de emprego para a posição de {position} foi atualizado por nossa equipe de RH.

Detalhes do Status da Oferta:

  • Posição: {position}
  • Departamento: {department_name}
  • Status Atual: {status}
  • Data da Oferta: {offer_date}
  • Data de Expiração: {expiration_date}

Se tiver dúvidas sobre esta atualização, entre em contato com nosso departamento de RH.

Atenciosamente,
Equipe de RH da {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Atualização do Status da Oferta - {position}', + 'content' => '

Caro/a {candidate_name},

Gostaríamos de informá-lo de que o status de sua oferta de emprego para a posição de {position} foi atualizado por nossa equipe de RH.

Detalhes do Status da Oferta:

  • Posição: {position}
  • Departamento: {department_name}
  • Status Atual: {status}
  • Data da Oferta: {offer_date}
  • Data de Expiração: {expiration_date}

Se tiver dúvidas sobre esta atualização, entre em contato com nosso departamento de RH.

Atenciosamente,
Equipe de RH da {app_name}

', + ], + 'ru' => [ + 'subject' => 'Обновление Статуса Предложения - {position}', + 'content' => '

Уважаемый/ая {candidate_name},

Мы хотим сообщить вам, что статус вашего предложения о работе на должность {position} был обновлен нашим отделом кадров.

Детали Статуса Предложения:

  • Должность: {position}
  • Отдел: {department_name}
  • Текущий Статус: {status}
  • Дата Предложения: {offer_date}
  • Срок Действия: {expiration_date}

Если у вас есть вопросы по этому обновлению, свяжитесь с нашим отделом кадров.

С уважением,
Команда HR {app_name}

', + ], + 'tr' => [ + 'subject' => 'Teklif Durumu Güncelleme - {position}', + 'content' => '

Sayın {candidate_name},

{position} pozisyonu için iş teklifinizin durumunun İK ekibimiz tarafından güncellendiğini bildirmek isteriz.

Teklif Durumu Detayları:

  • Pozisyon: {position}
  • Departman: {department_name}
  • Mevcut Durum: {status}
  • Teklif Tarihi: {offer_date}
  • Son Geçerlilik Tarihi: {expiration_date}

Bu güncelleme hakkında sorularınız varsa, İK departmanımızla iletişime geçin.

Saygılarımızla,
{app_name} İK Ekibi

', + ], + 'zh' => [ + 'subject' => '邀请状态更新 - {position}', + 'content' => '

尊敬的 {candidate_name}

我们想通知您,您的{position}职位工作邀请的状态已由我们的HR团队更新。

邀请状态详情:

  • 职位:{position}
  • 部门:{department_name}
  • 当前状态:{status}
  • 邀请日期:{offer_date}
  • 截止日期:{expiration_date}

如对此更新有任何疑问,请联系我们的HR部门。

此致,
{app_name} 人力资源团队

', + ], + ], + ], + [ + 'name' => 'Payslip Generate', + 'from' => $fromName, + 'translations' => [ + 'en' => [ + 'subject' => 'Payslip Generated - {pay_period_start} to {pay_period_end}', + 'content' => '

Dear {employee_name},

Your payslip for the period {pay_period_start} to {pay_period_end} has been generated and is now available.

Payslip Details:

  • Payslip Number: {payslip_number}
  • Pay Period: {pay_period_start} to {pay_period_end}
  • Pay Date: {pay_date}
  • Gross Pay: {gross_pay}
  • Net Pay: {net_pay}

You can download your payslip from the employee portal or contact HR for assistance.

Best regards,
{app_name} HR Team

', + ], + 'es' => [ + 'subject' => 'Nómina Generada - {pay_period_start} a {pay_period_end}', + 'content' => '

Estimado/a {employee_name},

Su nómina para el período {pay_period_start} a {pay_period_end} ha sido generada y ya está disponible.

Detalles de la Nómina:

  • Número de Nómina: {payslip_number}
  • Período de Pago: {pay_period_start} a {pay_period_end}
  • Fecha de Pago: {pay_date}
  • Salario Bruto: {gross_pay}
  • Salario Neto: {net_pay}

Puede descargar su nómina desde el portal de empleados o contactar a RRHH para asistencia.

Saludos cordiales,
Equipo de RRHH de {app_name}

', + ], + 'ar' => [ + 'subject' => 'تم إنشاء قسيمة الراتب - {pay_period_start} إلى {pay_period_end}', + 'content' => '

عزيزي/عزيزتي {employee_name}،

تم إنشاء قسيمة الراتب الخاصة بك للفترة من {pay_period_start} إلى {pay_period_end} وهي متاحة الآن.

تفاصيل قسيمة الراتب:

  • رقم القسيمة: {payslip_number}
  • فترة الدفع: {pay_period_start} إلى {pay_period_end}
  • تاريخ الدفع: {pay_date}
  • الراتب الإجمالي: {gross_pay}
  • صافي الراتب: {net_pay}

يمكنك تنزيل قسيمة الراتب من بوابة الموظفين أو الاتصال بالموارد البشرية للمساعدة.

مع أطيب التحيات،
فريق الموارد البشرية في {app_name}

', + ], + 'da' => [ + 'subject' => 'Lønseddel Genereret - {pay_period_start} til {pay_period_end}', + 'content' => '

Kære {employee_name},

Din lønseddel for perioden {pay_period_start} til {pay_period_end} er blevet genereret og er nu tilgængelig.

Lønseddeldetaljer:

  • Lønseddelnummer: {payslip_number}
  • Lønperiode: {pay_period_start} til {pay_period_end}
  • Lønningsdato: {pay_date}
  • Bruttoløn: {gross_pay}
  • Nettoløn: {net_pay}

Du kan downloade din lønseddel fra medarbejderportalen eller kontakte HR for hjælp.

Med venlig hilsen,
{app_name} HR Team

', + ], + 'de' => [ + 'subject' => 'Gehaltsabrechnung Erstellt - {pay_period_start} bis {pay_period_end}', + 'content' => '

Liebe/r {employee_name},

Ihre Gehaltsabrechnung für den Zeitraum {pay_period_start} bis {pay_period_end} wurde erstellt und ist jetzt verfügbar.

Gehaltsabrechnungsdetails:

  • Abrechnungsnummer: {payslip_number}
  • Abrechnungszeitraum: {pay_period_start} bis {pay_period_end}
  • Zahlungsdatum: {pay_date}
  • Bruttogehalt: {gross_pay}
  • Nettogehalt: {net_pay}

Sie können Ihre Gehaltsabrechnung über das Mitarbeiterportal herunterladen oder sich an die Personalabteilung wenden.

Mit freundlichen Grüßen,
{app_name} HR Team

', + ], + 'fr' => [ + 'subject' => 'Fiche de Paie Générée - {pay_period_start} à {pay_period_end}', + 'content' => '

Cher/Chère {employee_name},

Votre fiche de paie pour la période du {pay_period_start} au {pay_period_end} a été générée et est maintenant disponible.

Détails de la Fiche de Paie:

  • Numéro de Fiche: {payslip_number}
  • Période de Paie: {pay_period_start} à {pay_period_end}
  • Date de Paiement: {pay_date}
  • Salaire Brut: {gross_pay}
  • Salaire Net: {net_pay}

Vous pouvez télécharger votre fiche de paie depuis le portail employé ou contacter les RH pour assistance.

Cordialement,
Équipe RH de {app_name}

', + ], + 'he' => [ + 'subject' => 'תלוש שכר נוצר - {pay_period_start} עד {pay_period_end}', + 'content' => '

יקר/ה {employee_name},

תלוש השכר שלך לתקופה {pay_period_start} עד {pay_period_end} נוצר וזמין כעת.

פרטי תלוש השכר:

  • מספר תלוש: {payslip_number}
  • תקופת תשלום: {pay_period_start} עד {pay_period_end}
  • תאריך תשלום: {pay_date}
  • שכר ברוטו: {gross_pay}
  • שכר נטו: {net_pay}

ניתן להוריד את תלוש השכר מפורטל העובדים או ליצור קשר עם משאבי אנוש לסיוע.

בברכה,
צוות משאבי אנוש {app_name}

', + ], + 'it' => [ + 'subject' => 'Busta Paga Generata - {pay_period_start} a {pay_period_end}', + 'content' => '

Caro/a {employee_name},

La tua busta paga per il periodo dal {pay_period_start} al {pay_period_end} è stata generata ed è ora disponibile.

Dettagli della Busta Paga:

  • Numero Busta Paga: {payslip_number}
  • Periodo di Pagamento: {pay_period_start} a {pay_period_end}
  • Data di Pagamento: {pay_date}
  • Retribuzione Lorda: {gross_pay}
  • Retribuzione Netta: {net_pay}

Puoi scaricare la tua busta paga dal portale dipendenti o contattare l\'HR per assistenza.

Cordiali saluti,
Team HR di {app_name}

', + ], + 'ja' => [ + 'subject' => '給与明細書作成 - {pay_period_start}から{pay_period_end}', + 'content' => '

{employee_name}様、

{pay_period_start}から{pay_period_end}までの給与明細書が作成され、現在利用可能です。

給与明細書の詳細:

  • 明細書番号: {payslip_number}
  • 支給期間: {pay_period_start}から{pay_period_end}
  • 支給日: {pay_date}
  • 総支給額: {gross_pay}
  • 差引支給額: {net_pay}

従業員ポータルから給与明細書をダウンロードするか、人事部にお問い合わせください。

よろしくお願いいたします、
{app_name} 人事チーム

', + ], + 'nl' => [ + 'subject' => 'Loonstrook Gegenereerd - {pay_period_start} tot {pay_period_end}', + 'content' => '

Beste {employee_name},

Je loonstrook voor de periode {pay_period_start} tot {pay_period_end} is gegenereerd en is nu beschikbaar.

Loonstrookdetails:

  • Loonstrooknummer: {payslip_number}
  • Loonperiode: {pay_period_start} tot {pay_period_end}
  • Betaaldatum: {pay_date}
  • Brutoloon: {gross_pay}
  • Nettoloon: {net_pay}

Je kunt je loonstrook downloaden via het werknemersportaal of contact opnemen met HR voor hulp.

Met vriendelijke groet,
{app_name} HR Team

', + ], + 'pl' => [ + 'subject' => 'Pasek Wypłaty Wygenerowany - {pay_period_start} do {pay_period_end}', + 'content' => '

Drogi/a {employee_name},

Twój pasek wypłaty za okres od {pay_period_start} do {pay_period_end} został wygenerowany i jest teraz dostępny.

Szczegóły Paska Wypłaty:

  • Numer Paska: {payslip_number}
  • Okres Wypłaty: {pay_period_start} do {pay_period_end}
  • Data Wypłaty: {pay_date}
  • Wynagrodzenie Brutto: {gross_pay}
  • Wynagrodzenie Netto: {net_pay}

Możesz pobrać swój pasek wypłaty z portalu pracowniczego lub skontaktować się z HR w celu uzyskania pomocy.

Z poważaniem,
Zespół HR {app_name}

', + ], + 'pt' => [ + 'subject' => 'Holerite Gerado - {pay_period_start} a {pay_period_end}', + 'content' => '

Caro/a {employee_name},

Seu holerite para o período de {pay_period_start} a {pay_period_end} foi gerado e está disponível agora.

Detalhes do Holerite:

  • Número do Holerite: {payslip_number}
  • Período de Pagamento: {pay_period_start} a {pay_period_end}
  • Data de Pagamento: {pay_date}
  • Salário Bruto: {gross_pay}
  • Salário Líquido: {net_pay}

Você pode baixar seu holerite no portal do funcionário ou entrar em contato com o RH para assistência.

Atenciosamente,
Equipe de RH da {app_name}

', + ], + 'pt-BR' => [ + 'subject' => 'Holerite Gerado - {pay_period_start} a {pay_period_end}', + 'content' => '

Caro/a {employee_name},

Seu holerite para o período de {pay_period_start} a {pay_period_end} foi gerado e está disponível agora.

Detalhes do Holerite:

  • Número do Holerite: {payslip_number}
  • Período de Pagamento: {pay_period_start} a {pay_period_end}
  • Data de Pagamento: {pay_date}
  • Salário Bruto: {gross_pay}
  • Salário Líquido: {net_pay}

Você pode baixar seu holerite no portal do funcionário ou entrar em contato com o RH para assistência.

Atenciosamente,
Equipe de RH da {app_name}

', + ], + 'ru' => [ + 'subject' => 'Расчетный Лист Создан - {pay_period_start} до {pay_period_end}', + 'content' => '

Уважаемый/ая {employee_name},

Ваш расчетный лист за период с {pay_period_start} по {pay_period_end} был создан и теперь доступен.

Детали Расчетного Листа:

  • Номер Листа: {payslip_number}
  • Период Оплаты: {pay_period_start} до {pay_period_end}
  • Дата Выплаты: {pay_date}
  • Валовая Зарплата: {gross_pay}
  • Чистая Зарплата: {net_pay}

Вы можете скачать свой расчетный лист с портала сотрудников или связаться с отделом кадров для помощи.

С уважением,
Команда HR {app_name}

', + ], + 'tr' => [ + 'subject' => 'Maaş Bordrosu Oluşturuldu - {pay_period_start} - {pay_period_end}', + 'content' => '

Sayın {employee_name},

{pay_period_start} - {pay_period_end} dönemi için maaş bordronuz oluşturuldu ve artık kullanılabilir.

Bordro Detayları:

  • Bordro Numarası: {payslip_number}
  • Ödeme Dönemi: {pay_period_start} - {pay_period_end}
  • Ödeme Tarihi: {pay_date}
  • Brüt Maaş: {gross_pay}
  • Net Maaş: {net_pay}

Maaş bordronuzu çalışan portalından indirebilir veya yardım için İK ile iletişime geçebilirsiniz.

Saygılarımızla,
{app_name} İK Ekibi

', + ], + 'zh' => [ + 'subject' => '工资单已生成 - {pay_period_start}至{pay_period_end}', + 'content' => '

尊敬的 {employee_name}

{pay_period_start}{pay_period_end}期间的工资单已生成,现在可以查看。

工资单详情:

  • 工资单编号:{payslip_number}
  • 工资期间:{pay_period_start}至{pay_period_end}
  • 发薪日期:{pay_date}
  • 应发工资:{gross_pay}
  • 实发工资:{net_pay}

您可以从员工门户下载工资单或联系HR部门寻求帮助。

此致,
{app_name} 人力资源团队

', + ], + ], + ], + ]; + + foreach ($templates as $templateData) { + $template = EmailTemplate::create([ + 'name' => $templateData['name'], + 'from' => $templateData['from'], + 'user_id' => 1, + ]); + + foreach ($langCodes as $langCode) { + $translation = $templateData['translations'][$langCode] ?? $templateData['translations']['en']; + + EmailTemplateLang::create([ + 'parent_id' => $template->id, + 'lang' => $langCode, + 'subject' => $translation['subject'], + 'content' => $translation['content'], + ]); + } + + UserEmailTemplate::create([ + 'template_id' => $template->id, + 'user_id' => 1, + 'is_active' => true, + ]); + } + } +} diff --git a/database/seeders/EmployeeContractSeeder.php b/database/seeders/EmployeeContractSeeder.php new file mode 100644 index 000000000..8202136f8 --- /dev/null +++ b/database/seeders/EmployeeContractSeeder.php @@ -0,0 +1,121 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed contract data for consistent results + $contractData = [ + ['basic_salary' => 120000, 'allowances' => 42000, 'benefits' => 'Health Insurance', 'status' => 'Active', 'contract_type' => 'Permanent Full-time'], + ['basic_salary' => 85000, 'allowances' => 29500, 'benefits' => 'Provident Fund', 'status' => 'Active', 'contract_type' => 'Permanent Full-time'], + ['basic_salary' => 75000, 'allowances' => 26000, 'benefits' => 'Health Insurance', 'status' => 'Active', 'contract_type' => 'Fixed-term Contract'], + ['basic_salary' => 95000, 'allowances' => 33500, 'benefits' => 'Annual Bonus', 'status' => 'Active', 'contract_type' => 'Permanent Full-time'], + ['basic_salary' => 65000, 'allowances' => 22500, 'benefits' => 'Training Allowance', 'status' => 'Active', 'contract_type' => 'Fixed-term Contract'], + ['basic_salary' => 110000, 'allowances' => 38500, 'benefits' => 'Performance Bonus', 'status' => 'Active', 'contract_type' => 'Permanent Full-time'], + ['basic_salary' => 35000, 'allowances' => 5500, 'benefits' => 'Learning Stipend', 'status' => 'Active', 'contract_type' => 'Internship Contract'], + ['basic_salary' => 55000, 'allowances' => 19000, 'benefits' => 'Health Insurance', 'status' => 'Active', 'contract_type' => 'Part-time Contract'], + ['basic_salary' => 135000, 'allowances' => 57500, 'benefits' => 'Executive Health Plan', 'status' => 'Active', 'contract_type' => 'Permanent Full-time'], + ['basic_salary' => 88000, 'allowances' => 30800, 'benefits' => 'Remote Work Allowance', 'status' => 'Active', 'contract_type' => 'Permanent Full-time'] + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get contract types for this company + $contractTypes = ContractType::where('created_by', $company->id)->get(); + + if ($contractTypes->isEmpty()) { + $this->command->warn('No contract types found for company: ' . $company->name . '. Please run ContractTypeSeeder first.'); + continue; + } + + // Get managers/HR for approval + $approvers = User::whereIn('type', ['manager', 'hr']) + ->where('created_by', $company->id) + ->get(); + + $contractCounter = ($company->id - 1) * 100 + 1; + + // Create contracts for first 10 employees + $selectedEmployees = $employees->take(10); + + foreach ($selectedEmployees as $index => $employee) { + // Check if contract already exists for this employee + if (EmployeeContract::where('employee_id', $employee->id)->where('created_by', $company->id)->exists()) { + continue; + } + + $contract = $contractData[$index % 10]; + $contractNumber = 'EMP-' . $currentYear . '-' . str_pad($contractCounter, 4, '0', STR_PAD_LEFT); + + // Find matching contract type + $contractType = $contractTypes->where('name', $contract['contract_type'])->first(); + if (!$contractType) $contractType = $contractTypes->first(); + + // Select approver + $approver = $approvers->isNotEmpty() ? $approvers->first() : null; + + $startDate = date('Y-m-d', strtotime('-' . ($index + 30) . ' days')); + $endDate = $contractType->default_duration_months ? + date('Y-m-d', strtotime($startDate . ' +' . $contractType->default_duration_months . ' months')) : null; + + $termsConditions = 'Standard employment terms and conditions as per company policy. Employee is subject to ' . + $contractType->probation_period_months . ' months probation period and ' . + $contractType->notice_period_days . ' days notice period for termination.'; + + try { + EmployeeContract::create([ + 'contract_number' => $contractNumber, + 'employee_id' => $employee->id, + 'contract_type_id' => $contractType->id, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'basic_salary' => $contract['basic_salary'], + 'allowances' => json_encode(['total' => $contract['allowances']]), + 'benefits' => json_encode([$contract['benefits']]), + 'terms_conditions' => $termsConditions, + 'status' => $contract['status'], + 'approved_by' => $contract['status'] === 'Active' ? $approver?->id : null, + 'approved_at' => $contract['status'] === 'Active' ? now() : null, + 'created_by' => $company->id, + ]); + + $contractCounter++; + + } catch (\Exception $e) { + $this->command->error('Failed to create contract for employee: ' . $employee->name . ' in company: ' . $company->name); + $contractCounter++; + continue; + } + } + } + + $this->command->info('EmployeeContract seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/EmployeeGoalSeeder.php b/database/seeders/EmployeeGoalSeeder.php new file mode 100644 index 000000000..6279376ef --- /dev/null +++ b/database/seeders/EmployeeGoalSeeder.php @@ -0,0 +1,118 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed employee goals by goal type for consistent data + $goalsByType = [ + 'Performance Goals' => [ + ['title' => 'Increase Task Completion Rate', 'description' => 'Improve task completion rate from 85% to 95% by implementing better time management strategies', 'target' => '95% completion rate', 'progress' => 75, 'status' => 'in_progress'], + ['title' => 'Enhance Productivity Metrics', 'description' => 'Achieve 20% increase in daily productivity by optimizing workflow processes', 'target' => '20% productivity increase', 'progress' => 60, 'status' => 'in_progress'], + ['title' => 'Meet Quality Standards', 'description' => 'Maintain consistent quality standards with zero critical errors in deliverables', 'target' => 'Zero critical errors', 'progress' => 90, 'status' => 'in_progress'] + ], + 'Career Development Goals' => [ + ['title' => 'Obtain Professional Certification', 'description' => 'Complete industry-relevant certification to enhance professional credentials', 'target' => '1 certification', 'progress' => 40, 'status' => 'in_progress'], + ['title' => 'Develop Leadership Skills', 'description' => 'Participate in leadership development program and apply learned skills', 'target' => 'Complete leadership program', 'progress' => 30, 'status' => 'in_progress'], + ['title' => 'Expand Technical Expertise', 'description' => 'Master new technology stack relevant to current role and future opportunities', 'target' => 'Master 2 new technologies', 'progress' => 50, 'status' => 'in_progress'] + ], + 'Learning and Training Goals' => [ + ['title' => 'Complete Mandatory Training', 'description' => 'Finish all required compliance and safety training modules within deadline', 'target' => '100% training completion', 'progress' => 100, 'status' => 'completed'], + ['title' => 'Attend Professional Workshops', 'description' => 'Participate in at least 3 professional development workshops this year', 'target' => '3 workshops', 'progress' => 67, 'status' => 'in_progress'], + ['title' => 'Learn New Software Tools', 'description' => 'Gain proficiency in advanced features of job-related software applications', 'target' => 'Advanced proficiency', 'progress' => 45, 'status' => 'in_progress'] + ], + 'Project Goals' => [ + ['title' => 'Complete Project Alpha', 'description' => 'Successfully deliver Project Alpha within budget and timeline constraints', 'target' => 'On-time delivery', 'progress' => 80, 'status' => 'in_progress'], + ['title' => 'Implement Process Improvement', 'description' => 'Lead initiative to streamline departmental processes and reduce inefficiencies', 'target' => '25% efficiency gain', 'progress' => 55, 'status' => 'in_progress'], + ['title' => 'Launch New Product Feature', 'description' => 'Coordinate cross-functional team to launch new product feature successfully', 'target' => 'Successful launch', 'progress' => 35, 'status' => 'in_progress'] + ], + 'Sales and Revenue Goals' => [ + ['title' => 'Achieve Sales Quota', 'description' => 'Meet or exceed quarterly sales targets and contribute to revenue growth', 'target' => '110% of quota', 'progress' => 85, 'status' => 'in_progress'], + ['title' => 'Acquire New Clients', 'description' => 'Identify and onboard 15 new clients to expand customer base', 'target' => '15 new clients', 'progress' => 70, 'status' => 'in_progress'], + ['title' => 'Increase Customer Retention', 'description' => 'Improve customer retention rate through enhanced service and relationship management', 'target' => '95% retention rate', 'progress' => 88, 'status' => 'in_progress'] + ], + 'Quality Improvement Goals' => [ + ['title' => 'Reduce Error Rate', 'description' => 'Implement quality control measures to reduce work errors by 50%', 'target' => '50% error reduction', 'progress' => 65, 'status' => 'in_progress'], + ['title' => 'Enhance Review Process', 'description' => 'Establish comprehensive review process to ensure deliverable quality', 'target' => 'Implement review process', 'progress' => 100, 'status' => 'completed'], + ['title' => 'Achieve Quality Certification', 'description' => 'Work towards obtaining quality management certification for the department', 'target' => 'Quality certification', 'progress' => 25, 'status' => 'in_progress'] + ] + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get goal types for this company + $goalTypes = GoalType::where('created_by', $company->id)->get(); + + if ($goalTypes->isEmpty()) { + $this->command->warn('No goal types found for company: ' . $company->name . '. Please run GoalTypeSeeder first.'); + continue; + } + + // Create goals for first 3 employees (consistent data) + $selectedEmployees = $employees->take(3); + + foreach ($selectedEmployees as $index => $employee) { + foreach ($goalTypes as $goalType) { + $typeGoals = $goalsByType[$goalType->name] ?? []; + + // Create one goal per type for each employee + if (!empty($typeGoals)) { + $goalData = $typeGoals[$index % count($typeGoals)]; + + // Check if goal already exists + if (EmployeeGoal::where('title', $goalData['title'])->where('employee_id', $employee->id)->exists()) { + continue; + } + + try { + EmployeeGoal::create([ + 'employee_id' => $employee->id, + 'goal_type_id' => $goalType->id, + 'title' => $goalData['title'], + 'description' => $goalData['description'], + 'start_date' => $currentYear . '-01-01', + 'end_date' => $currentYear . '-12-31', + 'target' => $goalData['target'], + 'progress' => $goalData['progress'], + 'status' => $goalData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create goal: ' . $goalData['title'] . ' for employee: ' . $employee->name); + continue; + } + } + } + } + } + + $this->command->info('EmployeeGoal seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/EmployeeReviewSeeder.php b/database/seeders/EmployeeReviewSeeder.php new file mode 100644 index 000000000..24cc6a089 --- /dev/null +++ b/database/seeders/EmployeeReviewSeeder.php @@ -0,0 +1,144 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed review data for consistent results + $reviewStatuses = ['completed', 'completed', 'completed', 'completed', 'in_progress', 'in_progress', 'scheduled', 'scheduled']; + $overallRatings = [3.5, 4.0, 4.2, 3.8, 4.5]; + $reviewComments = [ + 'Employee demonstrates strong performance and meets expectations consistently', + 'Excellent work quality and dedication to achieving team objectives', + 'Shows good progress in skill development and professional growth', + 'Reliable team member with positive attitude and strong work ethic', + 'Outstanding performance with significant contributions to project success' + ]; + + // Fixed ratings for performance indicators + $indicatorRatings = [ + ['rating' => 4.0, 'comment' => 'Consistently meets performance standards with room for improvement'], + ['rating' => 4.5, 'comment' => 'Exceeds expectations and demonstrates strong competency'], + ['rating' => 3.5, 'comment' => 'Meets basic requirements but could benefit from additional development'], + ['rating' => 4.2, 'comment' => 'Strong performance with consistent quality output'], + ['rating' => 3.8, 'comment' => 'Good performance with occasional areas for enhancement'] + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get review cycles for this company + $reviewCycles = ReviewCycle::where('created_by', $company->id)->get(); + + if ($reviewCycles->isEmpty()) { + $this->command->warn('No review cycles found for company: ' . $company->name . '. Please run ReviewCycleSeeder first.'); + continue; + } + + // Get performance indicators for this company + $performanceIndicators = PerformanceIndicator::where('created_by', $company->id)->get(); + + if ($performanceIndicators->isEmpty()) { + $this->command->warn('No performance indicators found for company: ' . $company->name . '. Please run PerformanceIndicatorSeeder first.'); + continue; + } + + // Get managers/HR for reviewers + $reviewers = User::whereIn('type', ['manager', 'hr', 'employee'])->where('created_by', $company->id)->get(); + $reviewer = $reviewers->isNotEmpty() ? $reviewers->first() : $company; + + // Create reviews for first 3 employees (consistent data) + $selectedEmployees = $employees->take(4); + + foreach ($selectedEmployees as $empIndex => $employee) { + // Create 2 reviews per employee (different cycles) + $selectedCycles = $reviewCycles->take(2); + + foreach ($selectedCycles as $cycleIndex => $reviewCycle) { + $reviewIndex = ($empIndex * 2) + $cycleIndex; + $status = $reviewStatuses[$reviewIndex % 8]; + $overallRating = $status === 'completed' ? $overallRatings[$reviewIndex % 5] : null; + $completionDate = $status === 'completed' ? $currentYear . '-06-15' : null; + + try { + $employeeReview = EmployeeReview::create([ + 'employee_id' => $employee->id, + 'reviewer_id' => $reviewer->id, + 'review_cycle_id' => $reviewCycle->id, + 'review_date' => $currentYear . '-03-01', + 'completion_date' => $completionDate, + 'overall_rating' => $overallRating, + 'comments' => $status === 'completed' ? $reviewComments[$reviewIndex % 5] : null, + 'status' => $status, + 'created_by' => $company->id, + ]); + + // Create review ratings for completed reviews + if ($status === 'completed') { + $this->createReviewRatings($employeeReview, $performanceIndicators, $indicatorRatings); + } + } catch (\Exception $e) { + $this->command->error('Failed to create review for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('EmployeeReview seeder completed successfully!'); + } + + /** + * Create review ratings for performance indicators + */ + private function createReviewRatings($employeeReview, $performanceIndicators, $indicatorRatings) + { + // Create ratings for first 5 performance indicators + $selectedIndicators = $performanceIndicators->take(5); + + foreach ($selectedIndicators as $index => $indicator) { + $ratingData = $indicatorRatings[$index]; + + try { + DB::table('employee_review_ratings')->insert([ + 'employee_review_id' => $employeeReview->id, + 'performance_indicator_id' => $indicator->id, + 'rating' => $ratingData['rating'], + 'comments' => $ratingData['comment'], + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + continue; + } + } + } +} diff --git a/database/seeders/EmployeeSalarySeeder.php b/database/seeders/EmployeeSalarySeeder.php new file mode 100644 index 000000000..2e1a4d828 --- /dev/null +++ b/database/seeders/EmployeeSalarySeeder.php @@ -0,0 +1,127 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + continue; + } + + // Get salary components for this company + $salaryComponents = SalaryComponent::where('created_by', $company->id) + ->where('status', 'active') + ->get(); + + if ($salaryComponents->isEmpty()) { + continue; + } + + foreach ($employees as $index => $employee) { + // Fixed basic salary based on employee index for consistency + $basicSalaries = [30000, 35000, 40000, 45000, 15000, 55000, 60000, 20000, 70000, 75000]; + $basicSalary = $basicSalaries[$index % count($basicSalaries)]; + + // Select components based on employee index for consistency + $selectedComponents = $this->getComponentsForEmployee($salaryComponents, employeeIndex: $index); + + + try { + EmployeeSalary::updateOrCreate( + ['employee_id' => $employee->id], + [ + 'basic_salary' => $basicSalary, + 'components' => $selectedComponents, + 'is_active' => true, + 'calculation_status' => 'pending', + 'notes' => 'Salary setup for employee', + 'created_by' => $company->id, + ] + ); + } catch (\Exception $e) { + $this->command->error('Failed to create/update employee salary for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('EmployeeSalary seeder completed successfully!'); + } + + /** + * Get salary components for employee based on index for consistency + */ + private function getComponentsForEmployee($salaryComponents, $employeeIndex) + { + $earnings = $salaryComponents->where('type', 'earning'); + $deductions = $salaryComponents->where('type', 'deduction'); + + $selectedComponents = []; + + // Select earnings based on employee index + $earningsPattern = $employeeIndex % 3; + switch ($earningsPattern) { + case 0: // Basic package + $selectedComponents = array_merge( + $selectedComponents, + $earnings->whereIn('name', ['House Rent Allowance (HRA)', 'Transport Allowance'])->pluck('id')->toArray() + ); + break; + case 1: // Standard package + $selectedComponents = array_merge( + $selectedComponents, + $earnings->whereIn('name', ['House Rent Allowance (HRA)', 'Transport Allowance', 'Medical Allowance'])->pluck('id')->toArray() + ); + break; + case 2: // Premium package + $selectedComponents = array_merge( + $selectedComponents, + $earnings->whereIn('name', ['House Rent Allowance (HRA)', 'Transport Allowance', 'Medical Allowance', 'Dearness Allowance (DA)', 'Special Allowance'])->pluck('id')->toArray() + ); + break; + } + + // Select deductions based on employee index + $deductionsPattern = $employeeIndex % 2; + switch ($deductionsPattern) { + case 0: // Standard deductions + $selectedComponents = array_merge( + $selectedComponents, + $deductions->whereIn('name', ['Provident Fund (PF)', 'Professional Tax'])->pluck('id')->toArray() + ); + break; + case 1: // Full deductions + $selectedComponents = array_merge( + $selectedComponents, + $deductions->whereIn('name', ['Provident Fund (PF)', 'Employee State Insurance (ESI)', 'Professional Tax', 'Income Tax (TDS)'])->pluck('id')->toArray() + ); + break; + } + + return $selectedComponents; + } +} \ No newline at end of file diff --git a/database/seeders/EmployeeSeeder.php b/database/seeders/EmployeeSeeder.php new file mode 100644 index 000000000..be2575e81 --- /dev/null +++ b/database/seeders/EmployeeSeeder.php @@ -0,0 +1,151 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + foreach ($companies as $company) { + // Get all employee users for this company + $employeeUsers = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employeeUsers->isEmpty()) { + $this->command->warn('No employee users found for company: ' . $company->name . '. Please run DefaultCompanyUserSeeder first.'); + continue; + } + + // Get company branches, departments, and designations + $branches = Branch::where('created_by', $company->id)->get(); + $departments = Department::where('created_by', $company->id)->get(); + $designations = Designation::where('created_by', $company->id)->get(); + + foreach ($employeeUsers as $employeeUser) { + // Check if employee record already exists + if (Employee::where('user_id', $employeeUser->id)->exists()) { + continue; + } + + // Generate unique employee ID + do { + $employeeId = 'EMP' . str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT); + } while (Employee::where('employee_id', $employeeId)->exists()); + + // Select random branch, department, and designation + $branch = $branches->isNotEmpty() ? $branches->random() : null; + $department = $departments->isNotEmpty() ? $departments->random() : null; + $designation = $designations->isNotEmpty() ? $designations->random() : null; + + try { + Employee::create([ + // Basic Information + 'employee_id' => $employeeId, + 'phone' => $faker->phoneNumber, + 'date_of_birth' => $faker->dateTimeBetween('-60 years', '-22 years')->format('Y-m-d'), + 'gender' => $faker->randomElement(['male', 'female']), + + // Employment Details + 'branch_id' => $branch?->id, + 'department_id' => $department?->id, + 'designation_id' => $designation?->id, + 'shift_id' => null, // Will be set when shift seeder runs + 'attendance_policy_id' => null, // Will be set when attendance policy seeder runs + 'date_of_joining' => $faker->dateTimeBetween('-2 years', 'now')->format('Y-m-d'), + 'employment_type' => $faker->randomElement(['Full-time', 'Part-time', 'Contract', 'Temporary']), + + // Contact Information + 'address_line_1' => $faker->streetAddress, + 'address_line_2' => $faker->optional()->secondaryAddress, + 'city' => $faker->city, + 'state' => $faker->state, + 'country' => $faker->country, + 'postal_code' => $faker->postcode, + 'emergency_contact_name' => $faker->name, + 'emergency_contact_relationship' => $faker->randomElement(['Father', 'Mother', 'Spouse', 'Brother', 'Sister', 'Friend']), + 'emergency_contact_number' => $faker->phoneNumber, + + // Banking Information + 'bank_name' => $faker->randomElement(['State Bank', 'HDFC Bank', 'ICICI Bank', 'Axis Bank', 'Kotak Bank']), + 'account_holder_name' => $employeeUser->name, + 'account_number' => $faker->numerify('##########'), + 'bank_identifier_code' => $faker->regexify('[A-Z]{4}[0-9]{7}'), + 'bank_branch' => $faker->city . ' Branch', + 'tax_payer_id' => $faker->regexify('[A-Z]{5}[0-9]{4}[A-Z]{1}'), + + // System fields + 'user_id' => $employeeUser->id, + 'created_by' => $company->id, + ]); + + // Create employee documents + $this->createEmployeeDocuments($employeeUser, $company, $faker); + } catch (\Exception $e) { + $this->command->error('Failed to create employee record for user: ' . $employeeUser->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Employee seeder completed successfully!'); + } + + /** + * Create employee documents + */ + private function createEmployeeDocuments($employeeUser, $company, $faker) + { + // Get document types for this company + $documentTypes = DocumentType::where('created_by', $company->id)->get(); + + if ($documentTypes->isEmpty()) { + return; + } + + // Create 3-5 random documents for each employee + $documentCount = rand(3, min(5, $documentTypes->count())); + $selectedDocumentTypes = $documentTypes->random($documentCount); + + foreach ($selectedDocumentTypes as $documentType) { + try { + EmployeeDocument::create([ + 'employee_id' => $employeeUser->id, + 'document_type_id' => $documentType->id, + 'file_path' => randomImage(), + 'expiry_date' => $documentType->name === 'Identity Proof' || $documentType->name === 'Medical Certificate' + ? $faker->dateTimeBetween('now', '+5 years')->format('Y-m-d') + : null, + 'verification_status' => $faker->randomElement(['pending', 'verified', 'rejected']), + 'notes' => $faker->optional(0.3)->sentence(), + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + continue; + } + } + } +} diff --git a/database/seeders/EmployeeTrainingSeeder.php b/database/seeders/EmployeeTrainingSeeder.php new file mode 100644 index 000000000..128ff1004 --- /dev/null +++ b/database/seeders/EmployeeTrainingSeeder.php @@ -0,0 +1,207 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed training statuses and scores for consistent data + $trainingStatuses = ['assigned', 'in_progress', 'completed', 'completed', 'completed']; // 60% completed + $trainingScores = [85.5, 92.0, 78.5, 88.0, 95.5]; + $feedbacks = [ + 'Excellent training program with practical examples and hands-on exercises', + 'Very informative content, well-structured modules, and knowledgeable instructors', + 'Good training overall, could benefit from more interactive sessions', + 'Outstanding program that significantly improved my skills and knowledge', + 'Comprehensive training with relevant case studies and real-world applications' + ]; + + // Fixed assessment data by training program type + $assessmentsByProgram = [ + 'New Employee Orientation Program' => [ + ['name' => 'Company Policy Quiz', 'type' => 'quiz', 'passing_score' => 80.0, 'criteria' => 'Understanding of company policies and procedures'], + ['name' => 'Safety Procedures Assessment', 'type' => 'practical', 'passing_score' => 85.0, 'criteria' => 'Demonstration of safety protocols and emergency procedures'] + ], + 'Advanced Software Development' => [ + ['name' => 'Coding Skills Assessment', 'type' => 'practical', 'passing_score' => 75.0, 'criteria' => 'Programming proficiency and code quality'], + ['name' => 'Technical Presentation', 'type' => 'presentation', 'passing_score' => 70.0, 'criteria' => 'Technical knowledge presentation and communication skills'] + ], + 'Executive Leadership Program' => [ + ['name' => 'Leadership Case Study', 'type' => 'presentation', 'passing_score' => 80.0, 'criteria' => 'Strategic thinking and leadership decision-making'], + ['name' => 'Management Skills Evaluation', 'type' => 'practical', 'passing_score' => 85.0, 'criteria' => 'Team management and conflict resolution abilities'] + ], + 'Effective Business Communication' => [ + ['name' => 'Communication Skills Test', 'type' => 'quiz', 'passing_score' => 75.0, 'criteria' => 'Written and verbal communication proficiency'], + ['name' => 'Presentation Skills Demo', 'type' => 'presentation', 'passing_score' => 80.0, 'criteria' => 'Public speaking and presentation delivery skills'] + ], + 'Workplace Safety Certification' => [ + ['name' => 'Safety Knowledge Test', 'type' => 'quiz', 'passing_score' => 90.0, 'criteria' => 'Comprehensive understanding of safety regulations and procedures'], + ['name' => 'Emergency Response Drill', 'type' => 'practical', 'passing_score' => 95.0, 'criteria' => 'Proper execution of emergency response procedures'] + ], + 'Customer Experience Mastery' => [ + ['name' => 'Customer Service Scenarios', 'type' => 'practical', 'passing_score' => 80.0, 'criteria' => 'Customer interaction skills and problem-solving abilities'], + ['name' => 'Service Excellence Presentation', 'type' => 'presentation', 'passing_score' => 75.0, 'criteria' => 'Customer service best practices and improvement strategies'] + ] + ]; + + foreach ($companies as $company) { + // Get training programs for this company + $trainingPrograms = TrainingProgram::where('created_by', $company->id)->get(); + + if ($trainingPrograms->isEmpty()) { + $this->command->warn('No training programs found for company: ' . $company->name . '. Please run TrainingProgramSeeder first.'); + continue; + } + + // Get employees for this company + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get managers/HR for assignment + $assigners = User::whereIn('type', ['manager', 'hr','employee'])->where('created_by', $company->id)->get(); + $assigner = $assigners->isNotEmpty() ? $assigners->first() : $company; + + // Create assessments for training programs first + foreach ($trainingPrograms as $program) { + $programAssessments = $assessmentsByProgram[$program->name] ?? []; + + foreach ($programAssessments as $assessmentData) { + // Check if assessment already exists + if (DB::table('training_assessments')->where('name', $assessmentData['name'])->where('training_program_id', $program->id)->exists()) { + continue; + } + + try { + DB::table('training_assessments')->insert([ + 'training_program_id' => $program->id, + 'name' => $assessmentData['name'], + 'description' => 'Assessment for ' . $program->name . ' - ' . $assessmentData['name'], + 'type' => $assessmentData['type'], + 'passing_score' => $assessmentData['passing_score'], + 'criteria' => $assessmentData['criteria'], + 'created_by' => $company->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + continue; + } + } + } + + // Create employee trainings for first 3 employees and first 3 programs + $selectedEmployees = $employees->take(5); + $selectedPrograms = $trainingPrograms->take(5); + + foreach ($selectedEmployees as $empIndex => $employee) { + foreach ($selectedPrograms as $progIndex => $program) { + $trainingIndex = ($empIndex * 3) + $progIndex; + $status = $trainingStatuses[$trainingIndex % 5]; + $score = $status === 'completed' ? $trainingScores[$trainingIndex % 5] : null; + $isPassed = $score ? $score >= 70 : null; + + $assignedDate = $currentYear . '-' . str_pad($progIndex + 2, 2, '0', STR_PAD_LEFT) . '-01'; + $completionDate = $status === 'completed' ? + date('Y-m-d', strtotime($assignedDate . ' +30 days')) : null; + + try { + $employeeTraining = EmployeeTraining::create([ + 'employee_id' => $employee->id, + 'training_program_id' => $program->id, + 'status' => $status, + 'assigned_date' => $assignedDate, + 'completion_date' => $completionDate, + 'certification' => randomImage(), + 'score' => $score, + 'is_passed' => $isPassed, + 'feedback' => $status === 'completed' ? $feedbacks[$trainingIndex % 5] : null, + 'notes' => 'Training assigned for skill development and compliance requirements', + 'assigned_by' => $assigner->id, + 'created_by' => $assigner->id, + ]); + + // Create assessment results for completed trainings + if ($status === 'completed') { + $this->createAssessmentResults($employeeTraining, $program, $assigner, $trainingIndex); + } + + } catch (\Exception $e) { + $this->command->error('Failed to create employee training for: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('EmployeeTraining seeder completed successfully!'); + } + + /** + * Create assessment results for completed training + */ + private function createAssessmentResults($employeeTraining, $program, $assigner, $trainingIndex) + { + // Get assessments for this training program + $assessments = DB::table('training_assessments') + ->where('training_program_id', $program->id) + ->get(); + + // Fixed assessment scores + $assessmentScores = [ + [82.5, 88.0], // First set of assessment scores + [91.5, 85.5], // Second set + [76.0, 92.0], // Third set + [89.5, 87.0], // Fourth set + [94.0, 90.5] // Fifth set + ]; + + $scoreSet = $assessmentScores[$trainingIndex % 5]; + + foreach ($assessments as $index => $assessment) { + $score = $scoreSet[$index % 2]; + $isPassed = $score >= $assessment->passing_score; + + try { + DB::table('employee_assessment_results')->insert([ + 'employee_training_id' => $employeeTraining->id, + 'training_assessment_id' => $assessment->id, + 'score' => $score, + 'is_passed' => $isPassed, + 'feedback' => $isPassed ? + 'Excellent performance, demonstrates strong understanding of the subject matter' : + 'Good effort, but needs improvement in some areas to meet the required standards', + 'assessment_date' => $employeeTraining->completion_date, + 'assessed_by' => $assigner->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + continue; + } + } + } +} \ No newline at end of file diff --git a/database/seeders/EmployeeTransferSeeder.php b/database/seeders/EmployeeTransferSeeder.php new file mode 100644 index 000000000..c8b86b0a3 --- /dev/null +++ b/database/seeders/EmployeeTransferSeeder.php @@ -0,0 +1,130 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Transfer reasons + $transferReasons = [ + 'Business Expansion' => 'Employee transfer required to support business expansion and new market opportunities in different location.', + 'Skill Utilization' => 'Transfer to better utilize employee skills and expertise in department that requires specialized knowledge.', + 'Career Development' => 'Transfer provides employee with career growth opportunities and exposure to different business functions.', + 'Operational Requirements' => 'Transfer necessary to meet operational requirements and maintain adequate staffing levels across locations.', + 'Employee Request' => 'Transfer approved based on employee personal request for relocation due to family or personal circumstances.', + 'Project Assignment' => 'Employee assigned to specific project that requires presence at different branch or department location.', + 'Performance Improvement' => 'Transfer to environment better suited for employee development and performance improvement.', + 'Restructuring' => 'Transfer part of organizational restructuring to optimize resources and improve operational efficiency.', + 'Training Opportunity' => 'Transfer provides access to specialized training and development programs available at different location.', + 'Succession Planning' => 'Transfer supports succession planning initiatives and leadership development across organization.' + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get company resources + $branches = Branch::where('created_by', $company->id)->get(); + $departments = Department::where('created_by', $company->id)->get(); + $designations = Designation::where('created_by', $company->id)->get(); + + if ($branches->isEmpty() || $departments->isEmpty() || $designations->isEmpty()) { + $this->command->warn('Missing branches, departments, or designations for company: ' . $company->name); + continue; + } + + // Get managers/HR for approval + $approvers = User::whereIn('type', ['manager', 'hr']) + ->where('created_by', $company->id) + ->get(); + + // Create 3-7 transfers for this company + $transferCount = rand(3, 7); + + for ($i = 0; $i < $transferCount; $i++) { + $employee = $employees->take(5)->random(); + + // Get employee's current details from employee table + $employeeRecord = Employee::where('user_id', $employee->id)->first(); + + if (!$employeeRecord) { + continue; // Skip if no employee record found + } + + $fromBranch = $employeeRecord->branch_id ? Branch::find($employeeRecord->branch_id) : $branches->random(); + $fromDepartment = $employeeRecord->department_id ? Department::find($employeeRecord->department_id) : $departments->random(); + $fromDesignation = $employeeRecord->designation_id ? Designation::find($employeeRecord->designation_id) : $designations->random(); + + // Select different branch/department/designation for transfer + $toBranch = $branches->where('id', '!=', $fromBranch?->id)->random(); + $toDepartment = $departments->where('id', '!=', $fromDepartment?->id)->random(); + $toDesignation = $designations->where('id', '!=', $fromDesignation?->id)->random(); + + $transferDate = $faker->dateTimeBetween('-6 months', 'now'); + $effectiveDate = $faker->dateTimeBetween($transferDate, '+30 days'); + + $reasonKey = $faker->randomElement(array_keys($transferReasons)); + $reason = $transferReasons[$reasonKey]; + + $status = $faker->randomElement(['pending', 'approved', 'rejected']); + $approver = $approvers->isNotEmpty() ? $approvers->random() : null; + + try { + EmployeeTransfer::create([ + 'employee_id' => $employee->id, + 'from_branch_id' => $fromBranch?->id, + 'to_branch_id' => $toBranch?->id, + 'from_department_id' => $fromDepartment?->id, + 'to_department_id' => $toDepartment?->id, + 'from_designation_id' => $fromDesignation?->id, + 'to_designation_id' => $toDesignation?->id, + 'transfer_date' => $transferDate->format('Y-m-d'), + 'effective_date' => $effectiveDate->format('Y-m-d'), + 'reason' => $reason, + 'status' => $status, + 'documents' => randomImage(), + 'approved_by' => $status === 'approved' ? $approver?->id : null, + 'approved_at' => $status === 'approved' ? $faker->dateTimeBetween($transferDate, 'now') : null, + 'notes' => $faker->optional(0.6)->sentence(10), + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create transfer for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('EmployeeTransfer seeder completed successfully!'); + } +} diff --git a/database/seeders/ExperienceCertificateTemplateSeeder.php b/database/seeders/ExperienceCertificateTemplateSeeder.php new file mode 100644 index 000000000..8061e00c4 --- /dev/null +++ b/database/seeders/ExperienceCertificateTemplateSeeder.php @@ -0,0 +1,83 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $languages = [ + 'ar' => 'شهادة خبرة', + 'da' => 'Erfaringsbevis', + 'de' => 'Arbeitsbescheinigung', + 'en' => 'Experience Certificate', + 'es' => 'Certificado de Experiencia', + 'fr' => 'Certificat d\'Expérience', + 'he' => 'תעודת ניסיון', + 'it' => 'Certificato di Esperienza', + 'ja' => '経験証明書', + 'nl' => 'Ervaring Certificaat', + 'pl' => 'Świadectwo Doświadczenia', + 'pt' => 'Certificado de Experiência', + 'pt-BR' => 'Certificado de Experiência', + 'ru' => 'Справка о трудовом стаже', + 'tr' => 'Deneyim Belgesi', + 'zh' => '工作经验证明' + ]; + + $templates = [ + 'ar' => '

شهادة خبرة

التاريخ: {date}

إلى من يهمه الأمر،

نشهد بأن {employee_name} قد عمل لدى {company_name} بمنصب {designation} من تاريخ {joining_date} إلى {leaving_date}.

خلال فترة عمله، أظهر الموظف المذكور أداءً ممتازاً ومهارات مهنية عالية. لقد كان موظفاً مخلصاً ومسؤولاً.

نتمنى له التوفيق في مساعيه المستقبلية.

مع خالص التقدير،
قسم الموارد البشرية
{company_name}

', + 'da' => '

Erfaringsbevis

Dato: {date}

Til hvem det måtte vedkomme,

Dette er for at bekræfte, at {employee_name} var ansat hos {company_name} som {designation} fra {joining_date} til {leaving_date}.

I løbet af ansættelsesperioden viste den nævnte medarbejder fremragende præstation og høje professionelle færdigheder. Han/hun var en dedikeret og ansvarlig medarbejder.

Vi ønsker ham/hende held og lykke med fremtidige bestræbelser.

Med venlig hilsen,
HR-afdelingen
{company_name}

', + 'de' => '

Arbeitsbescheinigung

Datum: {date}

An wen es betrifft,

Hiermit wird bestätigt, dass {employee_name} bei {company_name} als {designation} vom {joining_date} bis {leaving_date} beschäftigt war.

Während der Beschäftigungszeit zeigte der genannte Mitarbeiter hervorragende Leistungen und hohe berufliche Fähigkeiten. Er/Sie war ein engagierter und verantwortungsvoller Mitarbeiter.

Wir wünschen ihm/ihr alles Gute für zukünftige Unternehmungen.

Mit freundlichen Grüßen,
Personalabteilung
{company_name}

', + 'en' => '

Experience Certificate

Date: {date}

To Whom It May Concern,

This is to certify that {employee_name} was employed with {company_name} as {designation} from {joining_date} to {leaving_date}.

During the period of employment, the above-mentioned employee demonstrated excellent performance and high professional skills. He/She was a dedicated and responsible employee.

We wish him/her all the best for future endeavors.

Sincerely,
HR Department
{company_name}

', + 'es' => '

Certificado de Experiencia

Fecha: {date}

A quien corresponda,

Por la presente certificamos que {employee_name} estuvo empleado en {company_name} como {designation} desde {joining_date} hasta {leaving_date}.

Durante el período de empleo, el empleado mencionado demostró un excelente desempeño y altas habilidades profesionales. Fue un empleado dedicado y responsable.

Le deseamos todo lo mejor para sus futuros proyectos.

Atentamente,
Departamento de RRHH
{company_name}

', + 'fr' => '

Certificat d\'Expérience

Date: {date}

À qui de droit,

Ceci certifie que {employee_name} était employé chez {company_name} en tant que {designation} du {joining_date} au {leaving_date}.

Pendant la période d\'emploi, l\'employé susmentionné a démontré d\'excellentes performances et de hautes compétences professionnelles. Il/Elle était un employé dévoué et responsable.

Nous lui souhaitons tout le meilleur pour ses projets futurs.

Cordialement,
Département RH
{company_name}

', + 'he' => '

תעודת ניסיון

תאריך: {date}

למי שזה נוגע,

זאת להעיד כי {employee_name} הועסק ב-{company_name} בתפקיד {designation} מ-{joining_date} עד {leaving_date}.

במהלך תקופת העבודה, העובד הנ"ל הפגין ביצועים מעולים וכישורים מקצועיים גבוהים. הוא/היא היה/הייתה עובד/ת מסור/ה ואחראי/ת.

אנו מאחלים לו/לה הצלחה בכל המאמצים העתידיים.

בכבוד רב,
מחלקת משאבי אנוש
{company_name}

', + 'it' => '

Certificato di Esperienza

Data: {date}

A chi di competenza,

Si certifica che {employee_name} è stato impiegato presso {company_name} come {designation} dal {joining_date} al {leaving_date}.

Durante il periodo di impiego, il suddetto dipendente ha dimostrato prestazioni eccellenti e alte competenze professionali. È stato un dipendente dedicato e responsabile.

Gli auguriamo tutto il meglio per i futuri progetti.

Cordiali saluti,
Dipartimento HR
{company_name}

', + 'ja' => '

経験証明書

日付: {date}

関係者各位

{employee_name}が{joining_date}から{leaving_date}まで{company_name}で{designation}として雇用されていたことを証明いたします。

雇用期間中、上記従業員は優秀な成績と高い専門技能を示しました。献身的で責任感のある従業員でした。

今後の活動における成功をお祈りいたします。

敬具
人事部
{company_name}

', + 'nl' => '

Ervaring Certificaat

Datum: {date}

Aan wie het betreft,

Hierbij wordt bevestigd dat {employee_name} werkzaam was bij {company_name} als {designation} van {joining_date} tot {leaving_date}.

Tijdens de dienstperiode toonde bovengenoemde werknemer uitstekende prestaties en hoge professionele vaardigheden. Hij/Zij was een toegewijde en verantwoordelijke werknemer.

Wij wensen hem/haar het beste toe voor toekomstige ondernemingen.

Met vriendelijke groet,
HR Afdeling
{company_name}

', + 'pl' => '

Świadectwo Doświadczenia

Data: {date}

Do kogo to dotyczy,

Niniejszym poświadczamy, że {employee_name} był zatrudniony w {company_name} na stanowisku {designation} od {joining_date} do {leaving_date}.

W okresie zatrudnienia wyżej wymieniony pracownik wykazał się doskonałymi wynikami i wysokimi umiejętnościami zawodowymi. Był oddanym i odpowiedzialnym pracownikiem.

Życzymy mu/jej powodzenia w przyszłych przedsięwzięciach.

Z poważaniem,
Dział HR
{company_name}

', + 'pt' => '

Certificado de Experiência

Data: {date}

A quem possa interessar,

Certificamos que {employee_name} esteve empregado na {company_name} como {designation} de {joining_date} a {leaving_date}.

Durante o período de emprego, o funcionário mencionado demonstrou excelente desempenho e altas habilidades profissionais. Foi um funcionário dedicado e responsável.

Desejamos-lhe tudo de bom para empreendimentos futuros.

Atenciosamente,
Departamento de RH
{company_name}

', + 'pt-BR' => '

Certificado de Experiência

Data: {date}

A quem possa interessar,

Certificamos que {employee_name} esteve empregado na {company_name} como {designation} de {joining_date} a {leaving_date}.

Durante o período de emprego, o funcionário mencionado demonstrou excelente desempenho e altas habilidades profissionais. Foi um funcionário dedicado e responsável.

Desejamos-lhe tudo de bom para empreendimentos futuros.

Atenciosamente,
Departamento de RH
{company_name}

', + 'ru' => '

Справка о трудовом стаже

Дата: {date}

Кого это касается,

Настоящим подтверждаем, что {employee_name} работал в {company_name} в должности {designation} с {joining_date} по {leaving_date}.

В период трудоустройства вышеупомянутый сотрудник продемонстрировал отличные результаты и высокие профессиональные навыки. Он/Она был/была преданным и ответственным сотрудником.

Желаем ему/ей всего наилучшего в будущих начинаниях.

С уважением,
Отдел кадров
{company_name}

', + 'tr' => '

Deneyim Belgesi

Tarih: {date}

İlgili Makama,

{employee_name} adlı kişinin {joining_date} tarihinden {leaving_date} tarihine kadar {company_name} şirketinde {designation} pozisyonunda çalıştığını onaylarız.

İstihdam süresi boyunca yukarıda belirtilen çalışan mükemmel performans ve yüksek mesleki beceriler sergilemiştir. Kendisi özverili ve sorumlu bir çalışandı.

Gelecekteki çalışmalarında kendisine başarılar dileriz.

Saygılarımızla,
İnsan Kaynakları Departmanı
{company_name}

', + 'zh' => '

工作经验证明

日期:{date}

致相关人员:

兹证明{employee_name}于{joining_date}至{leaving_date}期间在{company_name}担任{designation}职位。

在任职期间,上述员工表现出色,具备高水平的专业技能。他/她是一位敬业负责的员工。

祝愿他/她在未来的工作中一切顺利。

此致
人力资源部
{company_name}

' + ]; + + $variables = json_encode(['date', 'company_name', 'employee_name', 'designation', 'joining_date', 'leaving_date']); + + foreach ($companies as $company) { + foreach ($languages as $code => $title) { + try { + ExperienceCertificateTemplate::updateOrCreate( + [ + 'language' => $code, + 'created_by' => $company->id + ], + [ + 'content' => $templates[$code] ?? $templates['en'], + 'variables' => $variables + ] + ); + } catch (\Exception $e) { + $this->command->error('Failed to create Experience Certificate template for language: ' . $code . ' and company: ' . $company->name); + continue; + } + } + } + + $this->command->info('ExperienceCertificateTemplate seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/GoalTypeSeeder.php b/database/seeders/GoalTypeSeeder.php new file mode 100644 index 000000000..fa0380c63 --- /dev/null +++ b/database/seeders/GoalTypeSeeder.php @@ -0,0 +1,111 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed goal types for consistent data + $goalTypes = [ + [ + 'name' => 'Performance Goals', + 'description' => 'Goals focused on improving individual or team performance metrics, productivity, and efficiency in daily work activities', + 'status' => 'active' + ], + [ + 'name' => 'Career Development Goals', + 'description' => 'Goals aimed at professional growth, skill enhancement, and career advancement opportunities within the organization', + 'status' => 'active' + ], + [ + 'name' => 'Learning and Training Goals', + 'description' => 'Goals related to acquiring new knowledge, completing training programs, and developing competencies required for current or future roles', + 'status' => 'active' + ], + [ + 'name' => 'Project Goals', + 'description' => 'Specific objectives related to project completion, deliverables, milestones, and project success criteria', + 'status' => 'active' + ], + [ + 'name' => 'Sales and Revenue Goals', + 'description' => 'Targets focused on achieving sales quotas, revenue generation, customer acquisition, and business growth objectives', + 'status' => 'active' + ], + [ + 'name' => 'Quality Improvement Goals', + 'description' => 'Goals aimed at enhancing work quality, reducing errors, improving processes, and maintaining high standards of excellence', + 'status' => 'active' + ], + [ + 'name' => 'Customer Service Goals', + 'description' => 'Objectives focused on improving customer satisfaction, service quality, response times, and customer relationship management', + 'status' => 'active' + ], + [ + 'name' => 'Innovation Goals', + 'description' => 'Goals related to creative thinking, process innovation, new idea generation, and implementing innovative solutions', + 'status' => 'active' + ], + [ + 'name' => 'Team Collaboration Goals', + 'description' => 'Objectives aimed at improving teamwork, communication, cross-functional collaboration, and team effectiveness', + 'status' => 'active' + ], + [ + 'name' => 'Leadership Goals', + 'description' => 'Goals focused on developing leadership skills, mentoring abilities, decision-making capabilities, and team management competencies', + 'status' => 'active' + ], + [ + 'name' => 'Operational Excellence Goals', + 'description' => 'Objectives related to improving operational efficiency, streamlining processes, and optimizing resource utilization', + 'status' => 'active' + ], + [ + 'name' => 'Compliance and Safety Goals', + 'description' => 'Goals focused on maintaining regulatory compliance, workplace safety standards, and adherence to company policies and procedures', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($goalTypes as $goalTypeData) { + // Check if goal type already exists for this company + if (GoalType::where('name', $goalTypeData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + GoalType::create([ + 'name' => $goalTypeData['name'], + 'description' => $goalTypeData['description'], + 'status' => $goalTypeData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create goal type: ' . $goalTypeData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('GoalType seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/HolidaySeeder.php b/database/seeders/HolidaySeeder.php new file mode 100644 index 000000000..966a23c50 --- /dev/null +++ b/database/seeders/HolidaySeeder.php @@ -0,0 +1,187 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed holidays for consistent data + $holidays = [ + [ + 'name' => 'New Year\'s Day', + 'start_date' => $currentYear . '-01-01', + 'end_date' => null, + 'category' => 'National', + 'description' => 'Celebration of the beginning of the new calendar year', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Republic Day', + 'start_date' => $currentYear . '-01-26', + 'end_date' => null, + 'category' => 'National', + 'description' => 'Commemorates the adoption of the Constitution of India', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Independence Day', + 'start_date' => $currentYear . '-08-15', + 'end_date' => null, + 'category' => 'National', + 'description' => 'Celebrates the independence from British colonial rule', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Gandhi Jayanti', + 'start_date' => $currentYear . '-10-02', + 'end_date' => null, + 'category' => 'National', + 'description' => 'Birthday of Mahatma Gandhi, Father of the Nation', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Diwali', + 'start_date' => $currentYear . '-11-01', + 'end_date' => null, + 'category' => 'Religious', + 'description' => 'Festival of lights celebrated by Hindu, Sikh, and Jain communities', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Holi', + 'start_date' => $currentYear . '-03-25', + 'end_date' => null, + 'category' => 'Religious', + 'description' => 'Festival of colors celebrating the arrival of spring', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Eid al-Fitr', + 'start_date' => $currentYear . '-04-10', + 'end_date' => null, + 'category' => 'Religious', + 'description' => 'Islamic festival marking the end of Ramadan fasting period', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Christmas Day', + 'start_date' => $currentYear . '-12-25', + 'end_date' => null, + 'category' => 'Religious', + 'description' => 'Christian festival celebrating the birth of Jesus Christ', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Good Friday', + 'start_date' => $currentYear . '-03-29', + 'end_date' => null, + 'category' => 'Religious', + 'description' => 'Christian observance commemorating the crucifixion of Jesus Christ', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Company Foundation Day', + 'start_date' => $currentYear . '-06-15', + 'end_date' => null, + 'category' => 'Company Specific', + 'description' => 'Annual celebration of company establishment and achievements', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Annual Team Outing', + 'start_date' => $currentYear . '-09-20', + 'end_date' => $currentYear . '-09-21', + 'category' => 'Company Specific', + 'description' => 'Two-day company team building and recreational activities', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => false + ], + [ + 'name' => 'Employee Appreciation Day', + 'start_date' => $currentYear . '-05-10', + 'end_date' => null, + 'category' => 'Company Specific', + 'description' => 'Special day to recognize and appreciate employee contributions', + 'is_recurring' => true, + 'is_paid' => true, + 'is_half_day' => true + ] + ]; + + foreach ($companies as $company) { + // Get branches for this company + $branches = Branch::where('created_by', $company->id)->get(); + + foreach ($holidays as $holidayData) { + // Check if holiday already exists for this company + if (Holiday::where('name', $holidayData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + $holiday = Holiday::create([ + 'name' => $holidayData['name'], + 'start_date' => $holidayData['start_date'], + 'end_date' => $holidayData['end_date'], + 'category' => $holidayData['category'], + 'description' => $holidayData['description'], + 'is_recurring' => $holidayData['is_recurring'], + 'is_paid' => $holidayData['is_paid'], + 'is_half_day' => $holidayData['is_half_day'], + 'created_by' => $company->id, + ]); + + // Attach holiday to all branches of the company + if ($branches->isNotEmpty()) { + $holiday->branches()->attach($branches->pluck('id')); + } + } catch (\Exception $e) { + $this->command->error('Failed to create holiday: ' . $holidayData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Holiday seeder completed successfully!'); + } +} diff --git a/database/seeders/HrDocumentSeeder.php b/database/seeders/HrDocumentSeeder.php new file mode 100644 index 000000000..84c874ecc --- /dev/null +++ b/database/seeders/HrDocumentSeeder.php @@ -0,0 +1,241 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed HR documents for consistent data + $hrDocuments = [ + [ + 'title' => 'Employee Handbook 2024', + 'description' => 'Comprehensive employee handbook covering company policies, procedures, and guidelines for all employees', + 'category' => 'Compliance Documents', + 'file_name' => 'employee_handbook_2024.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 2048576, + 'version' => '2.1', + 'status' => 'Published', + 'effective_date' => '2024-01-01', + 'expiry_date' => '2024-12-31', + 'requires_acknowledgment' => true, + 'download_count' => 45 + ], + [ + 'title' => 'Code of Conduct Policy', + 'description' => 'Company code of conduct and ethical guidelines for professional behavior and workplace standards', + 'category' => 'Compliance Documents', + 'file_name' => 'code_of_conduct_policy.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 1024768, + 'version' => '1.5', + 'status' => 'Published', + 'effective_date' => '2024-01-15', + 'expiry_date' => null, + 'requires_acknowledgment' => true, + 'download_count' => 38 + ], + [ + 'title' => 'Leave Policy Document', + 'description' => 'Detailed leave policy including annual leave, sick leave, maternity/paternity leave, and special leave provisions', + 'category' => 'Employment Documents', + 'file_name' => 'leave_policy_2024.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 768432, + 'version' => '1.3', + 'status' => 'Published', + 'effective_date' => '2024-02-01', + 'expiry_date' => '2024-12-31', + 'requires_acknowledgment' => true, + 'download_count' => 52 + ], + [ + 'title' => 'Health and Safety Guidelines', + 'description' => 'Workplace health and safety procedures, emergency protocols, and safety compliance requirements', + 'category' => 'Medical Records', + 'file_name' => 'health_safety_guidelines.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 1536789, + 'version' => '1.8', + 'status' => 'Published', + 'effective_date' => '2024-01-10', + 'expiry_date' => null, + 'requires_acknowledgment' => true, + 'download_count' => 29 + ], + [ + 'title' => 'Performance Evaluation Form', + 'description' => 'Standard performance evaluation form template for annual and quarterly employee assessments', + 'category' => 'Performance Records', + 'file_name' => 'performance_evaluation_form.docx', + 'file_path' => randomImage(), + 'file_type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'file_size' => 245760, + 'version' => '2.0', + 'status' => 'Published', + 'effective_date' => '2024-03-01', + 'expiry_date' => null, + 'requires_acknowledgment' => false, + 'download_count' => 67 + ], + [ + 'title' => 'Training and Development Policy', + 'description' => 'Company policy on employee training, skill development programs, and professional growth opportunities', + 'category' => 'Training Certificates', + 'file_name' => 'training_development_policy.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 892345, + 'version' => '1.2', + 'status' => 'Published', + 'effective_date' => '2024-02-15', + 'expiry_date' => '2025-02-14', + 'requires_acknowledgment' => false, + 'download_count' => 34 + ], + [ + 'title' => 'Remote Work Policy', + 'description' => 'Guidelines and procedures for remote work arrangements, equipment, and productivity expectations', + 'category' => 'Employment Documents', + 'file_name' => 'remote_work_policy.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 654321, + 'version' => '1.4', + 'status' => 'Published', + 'effective_date' => '2024-01-20', + 'expiry_date' => null, + 'requires_acknowledgment' => true, + 'download_count' => 78 + ], + [ + 'title' => 'Expense Reimbursement Policy', + 'description' => 'Policy and procedures for business expense reimbursements, travel expenses, and claim processes', + 'category' => 'Financial Documents', + 'file_name' => 'expense_reimbursement_policy.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 432198, + 'version' => '1.6', + 'status' => 'Published', + 'effective_date' => '2024-01-05', + 'expiry_date' => null, + 'requires_acknowledgment' => false, + 'download_count' => 41 + ], + [ + 'title' => 'Data Privacy and Security Policy', + 'description' => 'Company data protection policy, privacy guidelines, and information security protocols', + 'category' => 'Legal Documents', + 'file_name' => 'data_privacy_security_policy.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 1234567, + 'version' => '2.3', + 'status' => 'Published', + 'effective_date' => '2024-01-01', + 'expiry_date' => null, + 'requires_acknowledgment' => true, + 'download_count' => 56 + ], + [ + 'title' => 'Emergency Contact Form', + 'description' => 'Standard form for employees to provide emergency contact information and medical details', + 'category' => 'Personal Documents', + 'file_name' => 'emergency_contact_form.png', + 'file_path' => randomImage(), + 'file_type' => 'image/png', + 'file_size' => 187654, + 'version' => '1.1', + 'status' => 'Published', + 'effective_date' => '2024-01-01', + 'expiry_date' => null, + 'requires_acknowledgment' => false, + 'download_count' => 89 + ] + ]; + + foreach ($companies as $company) { + // Get document categories for this company + $documentCategories = DocumentCategory::where('created_by', $company->id)->get(); + + if ($documentCategories->isEmpty()) { + $this->command->warn('No document categories found for company: ' . $company->name . '. Please run DocumentCategorySeeder first.'); + continue; + } + + // Get HR users for uploading and approval + $hrUsers = User::whereIn('type', ['hr', 'manager']) + ->where('created_by', $company->id) + ->get(); + + if ($hrUsers->isEmpty()) { + $this->command->warn('No HR users found for company: ' . $company->name); + continue; + } + + foreach ($hrDocuments as $index => $documentData) { + // Find matching category + $category = $documentCategories->where('name', $documentData['category'])->first(); + if (!$category) $category = $documentCategories->first(); + + // Check if document already exists for this company + if (HrDocument::where('title', $documentData['title'])->where('created_by', $company->id)->exists()) { + continue; + } + + // Select uploader and approver + $uploader = $hrUsers->first(); + $approver = $hrUsers->count() > 1 ? $hrUsers->skip(1)->first() : $hrUsers->first(); + + try { + HrDocument::create([ + 'title' => $documentData['title'], + 'description' => $documentData['description'], + 'category_id' => $category->id, + 'file_name' => $documentData['file_name'], + 'file_path' => $documentData['file_path'], + 'file_type' => $documentData['file_type'], + 'file_size' => $documentData['file_size'], + 'version' => $documentData['version'], + 'status' => $documentData['status'], + 'effective_date' => $documentData['effective_date'], + 'expiry_date' => $documentData['expiry_date'], + 'requires_acknowledgment' => $documentData['requires_acknowledgment'], + 'download_count' => $documentData['download_count'], + 'uploaded_by' => $company->id, + 'approved_by' => $documentData['status'] === 'Published' ? $approver->id : null, + 'approved_at' => $documentData['status'] === 'Published' ? now() : null, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create HR document: ' . $documentData['title'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('HrDocument seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/InterviewFeedbackSeeder.php b/database/seeders/InterviewFeedbackSeeder.php new file mode 100644 index 000000000..50ce3fc91 --- /dev/null +++ b/database/seeders/InterviewFeedbackSeeder.php @@ -0,0 +1,160 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed feedback data for consistent results + $feedbackData = [ + [ + 'technical_rating' => 4, + 'communication_rating' => 5, + 'cultural_fit_rating' => 4, + 'overall_rating' => 4, + 'strengths' => 'Strong technical skills, excellent problem-solving abilities, good understanding of software development principles', + 'weaknesses' => 'Could improve knowledge in advanced algorithms, needs more experience with cloud technologies', + 'comments' => 'Candidate demonstrates solid technical foundation and shows potential for growth. Communication skills are excellent.', + 'recommendation' => 'Hire' + ], + [ + 'technical_rating' => 5, + 'communication_rating' => 4, + 'cultural_fit_rating' => 5, + 'overall_rating' => 5, + 'strengths' => 'Exceptional technical expertise, innovative thinking, strong leadership potential, excellent team collaboration', + 'weaknesses' => 'Sometimes over-engineers solutions, could benefit from more business context understanding', + 'comments' => 'Outstanding candidate with impressive technical skills and great cultural fit. Highly recommended for the position.', + 'recommendation' => 'Strong Hire' + ], + [ + 'technical_rating' => 3, + 'communication_rating' => 3, + 'cultural_fit_rating' => 3, + 'overall_rating' => 3, + 'strengths' => 'Basic technical knowledge, willing to learn, shows enthusiasm for the role', + 'weaknesses' => 'Limited experience with required technologies, needs improvement in communication skills', + 'comments' => 'Candidate has potential but requires significant training and mentoring to meet job requirements.', + 'recommendation' => 'Maybe' + ], + [ + 'technical_rating' => 2, + 'communication_rating' => 2, + 'cultural_fit_rating' => 2, + 'overall_rating' => 2, + 'strengths' => 'Shows interest in learning, has basic understanding of fundamental concepts', + 'weaknesses' => 'Lacks required technical skills, poor communication, insufficient experience for the role', + 'comments' => 'Candidate does not meet the minimum requirements for this position. Significant skill gaps identified.', + 'recommendation' => 'Reject' + ], + [ + 'technical_rating' => 4, + 'communication_rating' => 4, + 'cultural_fit_rating' => 4, + 'overall_rating' => 4, + 'strengths' => 'Good technical skills, clear communication, positive attitude, relevant experience', + 'weaknesses' => 'Could improve depth in certain technical areas, needs more exposure to large-scale systems', + 'comments' => 'Solid candidate with good all-around skills. Would be a good addition to the team with proper onboarding.', + 'recommendation' => 'Hire' + ], + [ + 'technical_rating' => 1, + 'communication_rating' => 1, + 'cultural_fit_rating' => 1, + 'overall_rating' => 1, + 'strengths' => 'Punctual for the interview, shows basic interest in the field', + 'weaknesses' => 'Severely lacking technical skills, poor communication, does not understand job requirements', + 'comments' => 'Candidate is not suitable for this position. Major skill and experience gaps that cannot be addressed.', + 'recommendation' => 'Strong Reject' + ], + [ + 'technical_rating' => 5, + 'communication_rating' => 5, + 'cultural_fit_rating' => 4, + 'overall_rating' => 5, + 'strengths' => 'Excellent technical expertise, outstanding communication, proven track record, strong problem-solving', + 'weaknesses' => 'May be overqualified for some aspects of the role, high salary expectations', + 'comments' => 'Exceptional candidate who would bring significant value to the organization. Highly recommended.', + 'recommendation' => 'Strong Hire' + ], + [ + 'technical_rating' => 3, + 'communication_rating' => 4, + 'cultural_fit_rating' => 3, + 'overall_rating' => 3, + 'strengths' => 'Good communication skills, shows enthusiasm, has relevant educational background', + 'weaknesses' => 'Limited practical experience, needs training in key technical areas, lacks industry exposure', + 'comments' => 'Candidate has potential but would require significant investment in training and development.', + 'recommendation' => 'Maybe' + ] + ]; + + foreach ($companies as $company) { + // Get completed interviews for this company + $interviews = Interview::where('created_by', $company->id) + ->where('status', 'Completed') + ->get(); + + if ($interviews->isEmpty()) { + $this->command->warn('No completed interviews found for company: ' . $company->name . '. Please run InterviewSeeder first.'); + continue; + } + + foreach ($interviews as $index => $interview) { + $feedback = $feedbackData[$index % 8]; + + // Get interviewers from the interview + $interviewers = is_array($interview->interviewers) ? $interview->interviewers : []; + + // Create feedback for each interviewer + foreach ($interviewers as $interviewerIndex => $interviewerId) { + // Check if feedback already exists + if (InterviewFeedback::where('interview_id', $interview->id) + ->where('interviewer_id', $interviewerId) + ->exists() + ) { + continue; + } + + try { + InterviewFeedback::create([ + 'interview_id' => $interview->id, + 'interviewer_id' => $interviewerId, + 'technical_rating' => $feedback['technical_rating'], + 'communication_rating' => $feedback['communication_rating'], + 'cultural_fit_rating' => $feedback['cultural_fit_rating'], + 'overall_rating' => $feedback['overall_rating'], + 'strengths' => $feedback['strengths'], + 'weaknesses' => $feedback['weaknesses'], + 'comments' => $feedback['comments'], + 'recommendation' => $feedback['recommendation'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create interview feedback for interview ID: ' . $interview->id . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('InterviewFeedback seeder completed successfully!'); + } +} diff --git a/database/seeders/InterviewRoundSeeder.php b/database/seeders/InterviewRoundSeeder.php new file mode 100644 index 000000000..8af999b4c --- /dev/null +++ b/database/seeders/InterviewRoundSeeder.php @@ -0,0 +1,97 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed interview rounds for different job types + $interviewRounds = [ + 'technical' => [ + ['name' => 'Technical Assessment', 'sequence' => 1, 'description' => 'Technical skills evaluation and coding assessment'], + ['name' => 'Technical Interview', 'sequence' => 2, 'description' => 'In-depth technical discussion with senior developers'] + ], + 'management' => [ + ['name' => 'Behavioral Interview', 'sequence' => 1, 'description' => 'Assessment of leadership skills and behavioral competencies'], + ['name' => 'Panel Interview', 'sequence' => 2, 'description' => 'Final panel interview with senior management team'] + ], + 'sales' => [ + ['name' => 'Phone Interview', 'sequence' => 1, 'description' => 'Initial phone interview to assess communication and sales aptitude'], + ['name' => 'Sales Presentation', 'sequence' => 2, 'description' => 'Sales pitch presentation and role-playing exercise'] + ], + 'general' => [ + ['name' => 'Initial Screening', 'sequence' => 1, 'description' => 'Basic qualification and interest assessment'], + ['name' => 'Final Interview', 'sequence' => 2, 'description' => 'Final interview with hiring manager'] + ] + ]; + + foreach ($companies as $company) { + // Get job postings for this company + $jobPostings = JobPosting::where('created_by', $company->id)->get(); + + if ($jobPostings->isEmpty()) { + $this->command->warn('No job postings found for company: ' . $company->name . '. Please run JobPostingSeeder first.'); + continue; + } + + foreach ($jobPostings as $index => $jobPosting) { + // Determine interview round type based on job title + $roundType = 'general'; + $jobTitle = strtolower($jobPosting->title); + + if (str_contains($jobTitle, 'developer') || str_contains($jobTitle, 'engineer') || str_contains($jobTitle, 'technical')) { + $roundType = 'technical'; + } elseif (str_contains($jobTitle, 'manager') || str_contains($jobTitle, 'lead') || str_contains($jobTitle, 'director')) { + $roundType = 'management'; + } elseif (str_contains($jobTitle, 'sales') || str_contains($jobTitle, 'business development')) { + $roundType = 'sales'; + } + + $rounds = $interviewRounds[$roundType]; + + foreach ($rounds as $roundData) { + // Check if round already exists for this job + if (InterviewRound::where('job_id', $jobPosting->id) + ->where('name', $roundData['name']) + ->where('sequence_number', $roundData['sequence']) + ->exists()) { + continue; + } + + try { + InterviewRound::create([ + 'job_id' => $jobPosting->id, + 'name' => $roundData['name'], + 'sequence_number' => $roundData['sequence'], + 'description' => $roundData['description'], + 'status' => 'active', + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create interview round: ' . $roundData['name'] . ' for job: ' . $jobPosting->title); + continue; + } + } + } + } + + $this->command->info('InterviewRound seeder completed successfully!'); + } +} diff --git a/database/seeders/InterviewSeeder.php b/database/seeders/InterviewSeeder.php new file mode 100644 index 000000000..64ed2fa9b --- /dev/null +++ b/database/seeders/InterviewSeeder.php @@ -0,0 +1,196 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed interview data (70% completed) + $interviewData = [ + [ + 'scheduled_date' => '-1 day', + 'scheduled_time' => '10:00:00', + 'duration' => 60, + 'location' => 'Conference Room A', + 'meeting_link' => null, + 'status' => 'Completed', + 'feedback_submitted' => true, + 'interview_type' => 'Technical Interview' + ], + [ + 'scheduled_date' => '-2 days', + 'scheduled_time' => '14:00:00', + 'duration' => 45, + 'location' => null, + 'meeting_link' => 'https://zoom.us/j/123456789', + 'status' => 'Completed', + 'feedback_submitted' => true, + 'interview_type' => 'Video Interview' + ], + [ + 'scheduled_date' => '-3 days', + 'scheduled_time' => '11:00:00', + 'duration' => 30, + 'location' => 'HR Office', + 'meeting_link' => null, + 'status' => 'Completed', + 'feedback_submitted' => true, + 'interview_type' => 'HR Interview' + ], + [ + 'scheduled_date' => '-4 days', + 'scheduled_time' => '15:30:00', + 'duration' => 90, + 'location' => 'Meeting Room B', + 'meeting_link' => null, + 'status' => 'Completed', + 'feedback_submitted' => true, + 'interview_type' => 'Panel Interview' + ], + [ + 'scheduled_date' => '-5 days', + 'scheduled_time' => '09:30:00', + 'duration' => 60, + 'location' => null, + 'meeting_link' => 'https://teams.microsoft.com/meeting/join', + 'status' => 'Completed', + 'feedback_submitted' => true, + 'interview_type' => 'Behavioral Interview' + ], + [ + 'scheduled_date' => '-6 days', + 'scheduled_time' => '16:00:00', + 'duration' => 45, + 'location' => 'Training Room', + 'meeting_link' => null, + 'status' => 'Completed', + 'feedback_submitted' => true, + 'interview_type' => 'Practical Assessment' + ], + [ + 'scheduled_date' => '+1 day', + 'scheduled_time' => '13:00:00', + 'duration' => 30, + 'location' => null, + 'meeting_link' => 'https://meet.google.com/abc-defg-hij', + 'status' => 'Scheduled', + 'feedback_submitted' => false, + 'interview_type' => 'Phone Screening' + ], + [ + 'scheduled_date' => '+2 days', + 'scheduled_time' => '12:00:00', + 'duration' => 120, + 'location' => 'Boardroom', + 'meeting_link' => null, + 'status' => 'Scheduled', + 'feedback_submitted' => false, + 'interview_type' => 'Case Study Interview' + ] + ]; + + foreach ($companies as $company) { + // Get candidates for this company + $candidates = Candidate::where('created_by', $company->id)->where('status', 'Interview')->get(); + + if ($candidates->isEmpty()) { + $this->command->warn('No candidates found for company: ' . $company->name . '. Please run CandidateSeeder first.'); + continue; + } + + // Get interview types for this company + $interviewTypes = InterviewType::where('created_by', $company->id)->get(); + + if ($interviewTypes->isEmpty()) { + $this->command->warn('No interview types found for company: ' . $company->name . '. Please run InterviewTypeSeeder first.'); + continue; + } + + // Get employees for interviewers + $employees = User::whereIn('type', ['manager', 'hr', 'employee']) + ->where('created_by', $company->id) + ->get(); + + // Create interviews for first 4 candidates + $selectedCandidates = $candidates->take(5); + + foreach ($selectedCandidates as $candIndex => $candidate) { + // Get interview rounds for this job + $interviewRounds = InterviewRound::where('job_id', $candidate->job_id)->get(); + + if ($interviewRounds->isEmpty()) { + continue; + } + + // Create 2 interviews per candidate (first 3 rounds) + $selectedRounds = $interviewRounds->take(3); + + foreach ($selectedRounds as $roundIndex => $round) { + // Check if interview already exists for this candidate and round + if (Interview::where('candidate_id', $candidate->id) + ->where('round_id', $round->id) + ->exists()) { + continue; + } + + $dataIndex = ($candIndex * 2) + $roundIndex; + $interview = $interviewData[$dataIndex % 8]; + + // Find matching interview type + $interviewType = $interviewTypes->where('name', $interview['interview_type'])->first(); + if (!$interviewType) + $interviewType = $interviewTypes->first(); + + // Select interviewers + $selectedInterviewers = $employees->take(2); + $interviewers = $selectedInterviewers->pluck('id')->map(function ($id) { + return (string) $id; })->toArray(); + + $scheduledDate = date('Y-m-d', strtotime($interview['scheduled_date'])); + + try { + Interview::create([ + 'candidate_id' => $candidate->id, + 'job_id' => $candidate->job_id, + 'round_id' => $round->id, + 'interview_type_id' => $interviewType->id, + 'scheduled_date' => $scheduledDate, + 'scheduled_time' => $interview['scheduled_time'], + 'duration' => $interview['duration'], + 'location' => $interview['location'], + 'meeting_link' => $interview['meeting_link'], + 'interviewers' => $interviewers, + 'status' => $interview['status'], + 'feedback_submitted' => $interview['feedback_submitted'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create interview for candidate: ' . $candidate->first_name . ' ' . $candidate->last_name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('Interview seeder completed successfully!'); + } +} diff --git a/database/seeders/InterviewTypeSeeder.php b/database/seeders/InterviewTypeSeeder.php new file mode 100644 index 000000000..aa2df1388 --- /dev/null +++ b/database/seeders/InterviewTypeSeeder.php @@ -0,0 +1,101 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed interview types for consistent data + $interviewTypes = [ + [ + 'name' => 'Phone Screening', + 'description' => 'Initial phone interview to assess basic qualifications, communication skills, and interest level', + 'status' => 'active' + ], + [ + 'name' => 'Video Interview', + 'description' => 'Remote video interview conducted via video conferencing platforms to evaluate candidate fit', + 'status' => 'active' + ], + [ + 'name' => 'Technical Interview', + 'description' => 'In-depth technical assessment focusing on job-specific skills, problem-solving, and technical knowledge', + 'status' => 'active' + ], + [ + 'name' => 'Behavioral Interview', + 'description' => 'Interview focusing on past behavior, situational responses, and cultural fit assessment', + 'status' => 'active' + ], + [ + 'name' => 'Panel Interview', + 'description' => 'Interview conducted by multiple interviewers simultaneously to get diverse perspectives', + 'status' => 'active' + ], + [ + 'name' => 'HR Interview', + 'description' => 'Human resources interview covering company policies, benefits, salary negotiation, and final assessment', + 'status' => 'active' + ], + [ + 'name' => 'Case Study Interview', + 'description' => 'Problem-solving interview where candidates analyze and present solutions to business scenarios', + 'status' => 'active' + ], + [ + 'name' => 'Group Interview', + 'description' => 'Interview format where multiple candidates are assessed together through group activities and discussions', + 'status' => 'active' + ], + [ + 'name' => 'Final Interview', + 'description' => 'Final round interview typically conducted by senior management or hiring manager for final decision', + 'status' => 'active' + ], + [ + 'name' => 'Practical Assessment', + 'description' => 'Hands-on evaluation where candidates demonstrate skills through practical tasks and assignments', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($interviewTypes as $typeData) { + // Check if interview type already exists for this company + if (InterviewType::where('name', $typeData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + InterviewType::create([ + 'name' => $typeData['name'], + 'description' => $typeData['description'], + 'status' => $typeData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create interview type: ' . $typeData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('InterviewType seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/IpRestrictionSeeder.php b/database/seeders/IpRestrictionSeeder.php new file mode 100644 index 000000000..77e4c7fe5 --- /dev/null +++ b/database/seeders/IpRestrictionSeeder.php @@ -0,0 +1,69 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + + return; + } + + // Sample IP addresses for demo data + $ipAddresses = [ + '192.168.1.1', + '192.168.1.100', + '10.0.0.1', + '172.16.0.1', + '203.0.113.1', + '198.51.100.1', + '192.0.2.1', + '203.0.113.100', + ]; + + foreach ($companies as $company) { + // Check if company already has 5 or more IP restrictions + $existingCount = IpRestriction::where('created_by', $company->id)->count(); + if ($existingCount >= 5) { + continue; + } + + // Create exactly 5 IP restrictions for each company + $created = 0; + $index = 0; + + while ($created < 5 && $index < count($ipAddresses)) { + $ipAddress = $ipAddresses[$index]; + + // Check if IP address already exists for this company + if (! IpRestriction::where('ip_address', $ipAddress)->where('created_by', $company->id)->exists()) { + try { + IpRestriction::create([ + 'ip_address' => $ipAddress, + 'created_by' => $company->id, + ]); + $created++; + } catch (\Exception $e) { + $this->command->error('Failed to create IP restriction: '.$ipAddress.' for company: '.$company->name); + } + } + $index++; + } + } + + $this->command->info('IpRestriction seeder completed successfully!'); + } +} diff --git a/database/seeders/JobCategorySeeder.php b/database/seeders/JobCategorySeeder.php new file mode 100644 index 000000000..ebad0ef91 --- /dev/null +++ b/database/seeders/JobCategorySeeder.php @@ -0,0 +1,111 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed job categories for consistent data + $jobCategories = [ + [ + 'name' => 'Information Technology', + 'description' => 'Software development, system administration, cybersecurity, data analysis, and technical support roles', + 'status' => 'active' + ], + [ + 'name' => 'Sales and Marketing', + 'description' => 'Sales representatives, marketing specialists, digital marketing, business development, and customer acquisition roles', + 'status' => 'active' + ], + [ + 'name' => 'Human Resources', + 'description' => 'HR generalists, talent acquisition, employee relations, compensation and benefits, and organizational development roles', + 'status' => 'active' + ], + [ + 'name' => 'Finance and Accounting', + 'description' => 'Financial analysts, accountants, auditors, tax specialists, and financial planning and analysis roles', + 'status' => 'active' + ], + [ + 'name' => 'Operations and Management', + 'description' => 'Operations managers, project managers, team leads, supervisors, and executive management positions', + 'status' => 'active' + ], + [ + 'name' => 'Customer Service', + 'description' => 'Customer support representatives, call center agents, client relations, and customer success managers', + 'status' => 'active' + ], + [ + 'name' => 'Engineering', + 'description' => 'Mechanical engineers, electrical engineers, civil engineers, quality assurance, and technical engineering roles', + 'status' => 'active' + ], + [ + 'name' => 'Healthcare', + 'description' => 'Medical professionals, nurses, healthcare administrators, medical technicians, and healthcare support staff', + 'status' => 'active' + ], + [ + 'name' => 'Education and Training', + 'description' => 'Teachers, trainers, instructional designers, curriculum developers, and educational administrators', + 'status' => 'active' + ], + [ + 'name' => 'Legal and Compliance', + 'description' => 'Legal counsel, compliance officers, paralegals, contract specialists, and regulatory affairs professionals', + 'status' => 'active' + ], + [ + 'name' => 'Research and Development', + 'description' => 'Research scientists, product developers, innovation specialists, and research and development engineers', + 'status' => 'active' + ], + [ + 'name' => 'Administrative and Support', + 'description' => 'Administrative assistants, office managers, data entry clerks, receptionists, and general support staff', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($jobCategories as $categoryData) { + // Check if job category already exists for this company + if (JobCategory::where('name', $categoryData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + JobCategory::create([ + 'name' => $categoryData['name'], + 'description' => $categoryData['description'], + 'status' => $categoryData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create job category: ' . $categoryData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('JobCategory seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/JobLocationSeeder.php b/database/seeders/JobLocationSeeder.php new file mode 100644 index 000000000..00c9cbf49 --- /dev/null +++ b/database/seeders/JobLocationSeeder.php @@ -0,0 +1,136 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed job locations for consistent data + $jobLocations = [ + [ + 'name' => 'Head Office - Mumbai', + 'address' => 'Bandra Kurla Complex, Bandra East', + 'city' => 'Mumbai', + 'state' => 'Maharashtra', + 'country' => 'India', + 'postal_code' => '400051', + 'is_remote' => false, + 'status' => 'active' + ], + [ + 'name' => 'Branch Office - Delhi', + 'address' => 'Connaught Place, Central Delhi', + 'city' => 'New Delhi', + 'state' => 'Delhi', + 'country' => 'India', + 'postal_code' => '110001', + 'is_remote' => false, + 'status' => 'active' + ], + [ + 'name' => 'Tech Hub - Bangalore', + 'address' => 'Electronic City Phase 1', + 'city' => 'Bangalore', + 'state' => 'Karnataka', + 'country' => 'India', + 'postal_code' => '560100', + 'is_remote' => false, + 'status' => 'active' + ], + [ + 'name' => 'Development Center - Pune', + 'address' => 'Hinjewadi IT Park Phase 2', + 'city' => 'Pune', + 'state' => 'Maharashtra', + 'country' => 'India', + 'postal_code' => '411057', + 'is_remote' => false, + 'status' => 'active' + ], + [ + 'name' => 'Regional Office - Chennai', + 'address' => 'OMR IT Corridor, Thoraipakkam', + 'city' => 'Chennai', + 'state' => 'Tamil Nadu', + 'country' => 'India', + 'postal_code' => '600097', + 'is_remote' => false, + 'status' => 'active' + ], + [ + 'name' => 'Service Center - Hyderabad', + 'address' => 'HITEC City, Madhapur', + 'city' => 'Hyderabad', + 'state' => 'Telangana', + 'country' => 'India', + 'postal_code' => '500081', + 'is_remote' => false, + 'status' => 'active' + ], + [ + 'name' => 'Remote Work - India', + 'address' => null, + 'city' => null, + 'state' => null, + 'country' => 'India', + 'postal_code' => null, + 'is_remote' => true, + 'status' => 'active' + ], + [ + 'name' => 'Remote Work - Global', + 'address' => null, + 'city' => null, + 'state' => null, + 'country' => null, + 'postal_code' => null, + 'is_remote' => true, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($jobLocations as $locationData) { + // Check if job location already exists for this company + if (JobLocation::where('name', $locationData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + JobLocation::create([ + 'name' => $locationData['name'], + 'address' => $locationData['address'], + 'city' => $locationData['city'], + 'state' => $locationData['state'], + 'country' => $locationData['country'], + 'postal_code' => $locationData['postal_code'], + 'is_remote' => $locationData['is_remote'], + 'status' => $locationData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create job location: ' . $locationData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('JobLocation seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/JobPostingSeeder.php b/database/seeders/JobPostingSeeder.php new file mode 100644 index 000000000..be71eb8e6 --- /dev/null +++ b/database/seeders/JobPostingSeeder.php @@ -0,0 +1,350 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $jobPostingData = [ + [ + 'title' => 'Senior Software Engineer', + 'min_experience' => 5.0, + 'max_experience' => 8.0, + 'min_salary' => 80000, + 'max_salary' => 120000, + 'positions' => 2, + 'priority' => 'High', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => true, + 'description' => 'We are seeking a talented Senior Software Engineer to join our dynamic development team. You will be responsible for designing, developing, and maintaining high-quality software solutions. This role requires strong technical expertise, leadership skills, and the ability to work collaboratively in an agile environment. You will mentor junior developers, participate in code reviews, and contribute to architectural decisions that shape our technology stack.', + 'requirements' => 'Bachelor\'s degree in Computer Science or related field. Minimum 5 years of professional experience in web application development. Strong proficiency in PHP, Laravel framework, and modern JavaScript. Experience with MySQL database design and optimization. Solid understanding of RESTful APIs, version control (Git), and agile methodologies. Excellent problem-solving skills and attention to detail. Strong communication and teamwork abilities.', + 'benefits' => 'Comprehensive health insurance coverage for you and your family. Provident fund with company matching. Annual performance-based bonus. Professional development budget for courses and certifications. Flexible working hours and remote work options. Modern office with latest technology and equipment. Team building activities and company events. Paid time off and sick leave.', + 'skills' => ['PHP', 'Laravel', 'JavaScript', 'MySQL'], + 'applicant' => ['gender', 'date_of_birth'], + 'visibility' => ['terms_and_conditions', 'cover_letter'] + ], + [ + 'title' => 'Marketing Manager', + 'min_experience' => 3.0, + 'max_experience' => 6.0, + 'min_salary' => 60000, + 'max_salary' => 90000, + 'positions' => 1, + 'priority' => 'Medium', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => false, + 'description' => 'We are seeking an experienced Marketing Manager to lead our marketing initiatives and drive brand growth. You will develop and execute comprehensive marketing strategies, manage digital campaigns, and analyze market trends. This role requires creativity, strategic thinking, and strong leadership skills. You will work closely with cross-functional teams to ensure consistent brand messaging and achieve business objectives.', + 'requirements' => 'MBA in Marketing or related field preferred. Minimum 3 years of experience in digital marketing and brand management. Proven track record of successful marketing campaigns. Strong understanding of SEO, SEM, social media marketing, and content strategy. Experience with marketing analytics tools and data-driven decision making. Excellent communication, presentation, and project management skills. Creative mindset with attention to detail.', + 'benefits' => 'Comprehensive medical coverage including dental and vision. Annual leave and paid holidays. Professional training and development programs. Performance-based incentives and bonuses. Modern office environment with collaborative spaces. Employee wellness programs. Career advancement opportunities. Company-sponsored team outings and events.', + 'skills' => ['Digital Marketing', 'SEO', 'Content Strategy', 'Analytics'], + 'applicant' => ['gender'], + 'visibility' => ['terms_and_conditions'] + ], + [ + 'title' => 'HR Specialist', + 'min_experience' => 2.0, + 'max_experience' => 4.0, + 'min_salary' => 45000, + 'max_salary' => 65000, + 'positions' => 1, + 'priority' => 'Medium', + 'status' => 'Draft', + 'is_published' => false, + 'is_featured' => false, + 'description' => 'Join our HR team as an HR Specialist focusing on talent acquisition and employee relations. This remote position offers the flexibility to work from anywhere while contributing to our company culture. You will manage the full recruitment cycle, handle employee inquiries, maintain HR records, and support various HR initiatives. The ideal candidate is passionate about people, organized, and has excellent interpersonal skills.', + 'requirements' => 'Bachelor\'s degree in Human Resources, Business Administration, or related field. Minimum 2 years of experience in HR operations and recruitment. Strong knowledge of HR policies, labor laws, and best practices. Experience with applicant tracking systems and HRIS. Excellent communication and interpersonal skills. Ability to handle confidential information with discretion. Strong organizational and time management abilities. Self-motivated and able to work independently in a remote environment.', + 'benefits' => 'Remote work allowance for home office setup. Comprehensive health and wellness benefits. Flexible work schedule to maintain work-life balance. Professional development opportunities and HR certifications support. Paid vacation, sick leave, and personal days. Company-provided laptop and necessary equipment. Virtual team building activities. Annual performance reviews with salary adjustments.', + 'skills' => ['Recruitment', 'Employee Relations', 'HR Policies', 'Communication'] + ], + [ + 'title' => 'Frontend Developer', + 'min_experience' => 2.0, + 'max_experience' => 5.0, + 'min_salary' => 50000, + 'max_salary' => 75000, + 'positions' => 3, + 'priority' => 'High', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => true, + 'description' => 'We are looking for passionate Frontend Developers to join our growing team and build amazing user interfaces. You will work on cutting-edge web applications, collaborate with designers and backend developers, and contribute to creating seamless user experiences. This role offers opportunities to work with modern frameworks, participate in technical discussions, and grow your skills in a supportive environment.', + 'requirements' => 'Bachelor\'s degree in Computer Science or equivalent experience. Minimum 2 years of professional frontend development experience. Strong proficiency in React or Vue.js frameworks. Solid understanding of JavaScript, HTML5, and CSS3. Experience with responsive design and cross-browser compatibility. Knowledge of state management (Redux, Vuex) and RESTful APIs. Familiarity with version control systems (Git). Understanding of web performance optimization. Portfolio of previous work required.', + 'benefits' => 'Comprehensive health insurance for you and dependents. Annual learning and development budget for courses and conferences. Flexible working hours with option for remote work. Modern office with ergonomic workstations. Latest technology and tools. Collaborative and innovative work environment. Regular team events and hackathons. Competitive salary with annual reviews. Paid time off and holidays.', + 'skills' => ['React', 'Vue.js', 'JavaScript', 'CSS', 'HTML'] + ], + [ + 'title' => 'Data Analyst', + 'min_experience' => 1.0, + 'max_experience' => 3.0, + 'min_salary' => 40000, + 'max_salary' => 60000, + 'positions' => 2, + 'priority' => 'Medium', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => false, + 'description' => 'We are seeking detail-oriented Data Analysts to join our analytics team and help drive data-driven decision making across the organization. You will collect, process, and analyze large datasets to uncover insights and trends. This role involves creating reports, dashboards, and visualizations to communicate findings to stakeholders. You will work closely with various departments to understand their data needs and provide actionable recommendations.', + 'requirements' => 'Bachelor\'s degree in Statistics, Mathematics, Computer Science, or related field. Minimum 1 year of experience in data analysis or related role. Strong proficiency in SQL for data extraction and manipulation. Experience with Python or R for statistical analysis. Knowledge of data visualization tools like Tableau, Power BI, or similar. Strong analytical and problem-solving skills. Excellent attention to detail. Ability to communicate complex data insights to non-technical audiences. Experience with Excel for data analysis and reporting.', + 'benefits' => 'Comprehensive health coverage including medical, dental, and vision. Professional training programs and certifications support. Career growth opportunities within the analytics team. Collaborative work environment with experienced mentors. Modern office with latest analytics tools and software. Flexible work arrangements. Paid vacation and sick leave. Performance-based bonuses. Employee assistance programs.', + 'skills' => ['SQL', 'Python', 'Excel', 'Tableau', 'Statistics'] + ], + [ + 'title' => 'Project Manager', + 'min_experience' => 4.0, + 'max_experience' => 7.0, + 'min_salary' => 70000, + 'max_salary' => 100000, + 'positions' => 1, + 'priority' => 'High', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => true, + 'description' => 'Lead cross-functional teams to deliver projects on time.', + 'requirements' => 'PMP certification preferred, 4+ years PM experience', + 'benefits' => 'Health insurance, Bonus, Professional development', + 'skills' => ['Project Management', 'Agile', 'Scrum', 'Leadership'] + ], + [ + 'title' => 'UX Designer', + 'min_experience' => 2.0, + 'max_experience' => 5.0, + 'min_salary' => 55000, + 'max_salary' => 80000, + 'positions' => 1, + 'priority' => 'Medium', + 'status' => 'Draft', + 'is_published' => false, + 'is_featured' => false, + 'description' => 'Design intuitive user experiences for our products.', + 'requirements' => 'Bachelor in Design, portfolio required', + 'benefits' => 'Creative environment, Health benefits, Equipment allowance', + 'skills' => ['Figma', 'Sketch', 'User Research', 'Prototyping'] + ], + [ + 'title' => 'DevOps Engineer', + 'min_experience' => 3.0, + 'max_experience' => 6.0, + 'min_salary' => 65000, + 'max_salary' => 95000, + 'positions' => 2, + 'priority' => 'High', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => false, + 'description' => 'Manage infrastructure and deployment pipelines.', + 'requirements' => 'Bachelor in CS, AWS/Azure experience', + 'benefits' => 'Health insurance, Certification budget, Remote work', + 'skills' => ['AWS', 'Docker', 'Kubernetes', 'CI/CD', 'Linux'] + ], + [ + 'title' => 'Sales Executive', + 'min_experience' => 1.0, + 'max_experience' => 4.0, + 'min_salary' => 35000, + 'max_salary' => 55000, + 'positions' => 4, + 'priority' => 'Medium', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => false, + 'description' => 'Drive sales growth and build client relationships.', + 'requirements' => 'Bachelor degree, excellent communication skills', + 'benefits' => 'Commission structure, Health benefits, Travel allowance', + 'skills' => ['Sales', 'Communication', 'CRM', 'Negotiation'] + ], + [ + 'title' => 'Quality Assurance Engineer', + 'min_experience' => 2.0, + 'max_experience' => 5.0, + 'min_salary' => 45000, + 'max_salary' => 70000, + 'positions' => 2, + 'priority' => 'Medium', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => false, + 'description' => 'Ensure product quality through comprehensive testing.', + 'requirements' => 'Bachelor in CS, testing experience required', + 'benefits' => 'Health insurance, Learning opportunities, Flexible schedule', + 'skills' => ['Manual Testing', 'Automation', 'Selenium', 'API Testing'] + ], + [ + 'title' => 'Business Analyst', + 'min_experience' => 2.0, + 'max_experience' => 5.0, + 'min_salary' => 50000, + 'max_salary' => 75000, + 'positions' => 1, + 'priority' => 'Medium', + 'status' => 'Draft', + 'is_published' => false, + 'is_featured' => false, + 'description' => 'Analyze business processes and recommend improvements.', + 'requirements' => 'MBA preferred, analytical mindset required', + 'benefits' => 'Health coverage, Professional development, Bonus', + 'skills' => ['Business Analysis', 'Requirements Gathering', 'Process Improvement'] + ], + [ + 'title' => 'Content Writer', + 'min_experience' => 1.0, + 'max_experience' => 3.0, + 'min_salary' => 30000, + 'max_salary' => 45000, + 'positions' => 2, + 'priority' => 'Low', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => false, + 'description' => 'Create engaging content for various marketing channels.', + 'requirements' => 'Bachelor in English/Journalism, writing portfolio', + 'benefits' => 'Creative environment, Health benefits, Flexible hours', + 'skills' => ['Content Writing', 'SEO Writing', 'Research', 'Editing'] + ], + [ + 'title' => 'Network Administrator', + 'min_experience' => 3.0, + 'max_experience' => 6.0, + 'min_salary' => 55000, + 'max_salary' => 80000, + 'positions' => 1, + 'priority' => 'Medium', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => false, + 'description' => 'Maintain and optimize network infrastructure.', + 'requirements' => 'Bachelor in IT, networking certifications preferred', + 'benefits' => 'Health insurance, Certification support, Equipment', + 'skills' => ['Networking', 'Cisco', 'Security', 'Troubleshooting'] + ], + [ + 'title' => 'Financial Analyst', + 'min_experience' => 2.0, + 'max_experience' => 4.0, + 'min_salary' => 45000, + 'max_salary' => 65000, + 'positions' => 1, + 'priority' => 'Medium', + 'status' => 'Draft', + 'is_published' => false, + 'is_featured' => false, + 'description' => 'Analyze financial data and prepare reports.', + 'requirements' => 'Bachelor in Finance/Accounting, Excel proficiency', + 'benefits' => 'Health coverage, Retirement plan, Professional development', + 'skills' => ['Financial Analysis', 'Excel', 'Reporting', 'Budgeting'] + ], + [ + 'title' => 'Customer Support Representative', + 'min_experience' => 0.0, + 'max_experience' => 2.0, + 'min_salary' => 25000, + 'max_salary' => 35000, + 'positions' => 5, + 'priority' => 'Low', + 'status' => 'Published', + 'is_published' => true, + 'is_featured' => false, + 'description' => 'Provide excellent customer service and support.', + 'requirements' => 'High school diploma, excellent communication skills', + 'benefits' => 'Health benefits, Training provided, Career growth', + 'skills' => ['Customer Service', 'Communication', 'Problem Solving', 'Patience'] + ] + ]; + + foreach ($companies as $company) { + $jobTypes = JobType::where('created_by', $company->id)->get(); + $jobLocations = JobLocation::where('created_by', $company->id)->get(); + $departments = Department::where('created_by', $company->id)->get(); + $branches = Branch::where('created_by', $company->id)->get(); + $customQuestions = CustomQuestion::where('created_by', $company->id)->pluck('id')->toArray(); + + if ($jobTypes->isEmpty() || $jobLocations->isEmpty()) { + $this->command->warn('Missing job types or locations for company: ' . $company->name); + continue; + } + + foreach ($jobPostingData as $index => $data) { + // Generate job code manually since creatorId() requires auth + $jobCode = 'JOB-' . $company->id . '-' . str_pad(($index + 1), 5, '0', STR_PAD_LEFT); + + if (JobPosting::where('job_code', $jobCode)->exists()) { + continue; + } + + // Dynamically select job type, location, branch and department + $jobType = $jobTypes->skip($index % $jobTypes->count())->first(); + $jobLocation = $jobLocations->skip($index % $jobLocations->count())->first(); + $branch = $branches->skip($index % max(1, $branches->count()))->first(); + + + // Get departments for the selected branch + $branchDepartments = $departments->where('branch_id', $branch?->id); + $department = $branchDepartments->isNotEmpty() ? + $branchDepartments->skip($index % $branchDepartments->count())->first() : + null; + + $applicationDeadline = $data['is_published'] ? date('Y-m-d', strtotime('+30 days')) : null; + $publishDate = $data['is_published'] ? now() : null; + $startDate = $data['is_published'] ? date('Y-m-d', strtotime('+7 days')) : null; + + // Get random custom questions (2-3 questions) + $selectedQuestions = !empty($customQuestions) ? array_slice($customQuestions, 0, rand(2, min(3, count($customQuestions)))) : null; + + try { + JobPosting::create([ + 'job_code' => $jobCode, + 'code' => strtoupper(uniqid('JP')), + 'title' => $data['title'], + 'job_type_id' => $jobType->id, + 'location_id' => $jobLocation->id, + 'department_id' => $department?->id, + 'branch_id' => $branch?->id, + 'min_experience' => $data['min_experience'], + 'max_experience' => $data['max_experience'], + 'min_salary' => $data['min_salary'], + 'max_salary' => $data['max_salary'], + 'positions' => $data['positions'], + 'priority' => $data['priority'], + 'description' => $data['description'], + 'requirements' => $data['requirements'], + 'benefits' => $data['benefits'], + 'skills' => $data['skills'], + 'applicant' => $data['applicant'] ?? null, + 'visibility' => $data['visibility'] ?? null, + 'custom_question' => $selectedQuestions, + 'start_date' => $startDate, + 'application_deadline' => $applicationDeadline, + 'is_published' => $data['is_published'], + 'publish_date' => $publishDate, + 'is_featured' => $data['is_featured'], + 'status' => $data['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create job posting: ' . $data['title'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('JobPosting seeder completed successfully!'); + } +} diff --git a/database/seeders/JobRequisitionSeeder.php b/database/seeders/JobRequisitionSeeder.php new file mode 100644 index 000000000..d0d1ab172 --- /dev/null +++ b/database/seeders/JobRequisitionSeeder.php @@ -0,0 +1,177 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed job requisitions by category for consistent data + $requisitionsByCategory = [ + 'Information Technology' => [ + ['title' => 'Senior Software Developer', 'positions' => 2, 'budget_min' => 80000, 'budget_max' => 120000, 'priority' => 'High', 'status' => 'Approved'], + ['title' => 'DevOps Engineer', 'positions' => 1, 'budget_min' => 70000, 'budget_max' => 100000, 'priority' => 'Medium', 'status' => 'Pending Approval'], + ['title' => 'UI/UX Designer', 'positions' => 1, 'budget_min' => 60000, 'budget_max' => 85000, 'priority' => 'Medium', 'status' => 'Approved'] + ], + 'Sales and Marketing' => [ + ['title' => 'Sales Manager', 'positions' => 1, 'budget_min' => 65000, 'budget_max' => 90000, 'priority' => 'High', 'status' => 'Approved'], + ['title' => 'Digital Marketing Specialist', 'positions' => 2, 'budget_min' => 45000, 'budget_max' => 65000, 'priority' => 'Medium', 'status' => 'Draft'], + ['title' => 'Business Development Executive', 'positions' => 3, 'budget_min' => 50000, 'budget_max' => 70000, 'priority' => 'High', 'status' => 'Approved'] + ], + 'Human Resources' => [ + ['title' => 'HR Business Partner', 'positions' => 1, 'budget_min' => 70000, 'budget_max' => 95000, 'priority' => 'Medium', 'status' => 'Approved'], + ['title' => 'Talent Acquisition Specialist', 'positions' => 2, 'budget_min' => 55000, 'budget_max' => 75000, 'priority' => 'High', 'status' => 'Pending Approval'] + ], + 'Finance and Accounting' => [ + ['title' => 'Financial Analyst', 'positions' => 1, 'budget_min' => 60000, 'budget_max' => 80000, 'priority' => 'Medium', 'status' => 'Approved'], + ['title' => 'Senior Accountant', 'positions' => 1, 'budget_min' => 55000, 'budget_max' => 75000, 'priority' => 'Low', 'status' => 'On Hold'] + ], + 'Operations and Management' => [ + ['title' => 'Operations Manager', 'positions' => 1, 'budget_min' => 85000, 'budget_max' => 110000, 'priority' => 'High', 'status' => 'Approved'], + ['title' => 'Project Manager', 'positions' => 2, 'budget_min' => 75000, 'budget_max' => 95000, 'priority' => 'Medium', 'status' => 'Approved'] + ], + 'Customer Service' => [ + ['title' => 'Customer Success Manager', 'positions' => 1, 'budget_min' => 50000, 'budget_max' => 70000, 'priority' => 'Medium', 'status' => 'Approved'], + ['title' => 'Customer Support Representative', 'positions' => 4, 'budget_min' => 35000, 'budget_max' => 45000, 'priority' => 'High', 'status' => 'Approved'] + ] + ]; + + // Fixed job details + $jobDetails = [ + 'Senior Software Developer' => [ + 'skills' => 'JavaScript, React, Node.js, Python, SQL, Git, Agile methodologies', + 'education' => 'Bachelor\'s degree in Computer Science or related field', + 'experience' => '5+ years of software development experience', + 'description' => 'We are seeking an experienced Senior Software Developer to join our dynamic development team', + 'responsibilities' => 'Design and develop scalable web applications, mentor junior developers, participate in code reviews, collaborate with cross-functional teams' + ], + 'DevOps Engineer' => [ + 'skills' => 'AWS, Docker, Kubernetes, Jenkins, Terraform, Linux, CI/CD pipelines', + 'education' => 'Bachelor\'s degree in Computer Science or Engineering', + 'experience' => '3+ years of DevOps or system administration experience', + 'description' => 'Looking for a skilled DevOps Engineer to manage our cloud infrastructure and deployment processes', + 'responsibilities' => 'Manage cloud infrastructure, implement CI/CD pipelines, monitor system performance, ensure security compliance' + ], + 'Sales Manager' => [ + 'skills' => 'Sales leadership, CRM software, negotiation, team management, market analysis', + 'education' => 'Bachelor\'s degree in Business, Marketing, or related field', + 'experience' => '5+ years of sales experience with 2+ years in management', + 'description' => 'Seeking an experienced Sales Manager to lead our sales team and drive revenue growth', + 'responsibilities' => 'Lead sales team, develop sales strategies, manage key accounts, analyze market trends, achieve sales targets' + ], + 'HR Business Partner' => [ + 'skills' => 'HR strategy, employee relations, performance management, organizational development', + 'education' => 'Bachelor\'s degree in Human Resources, Business, or Psychology', + 'experience' => '4+ years of HR experience with business partnering focus', + 'description' => 'We need an HR Business Partner to support our business units and drive HR initiatives', + 'responsibilities' => 'Partner with business leaders, manage employee relations, develop HR policies, support organizational change' + ], + 'Financial Analyst' => [ + 'skills' => 'Financial modeling, Excel, SQL, data analysis, budgeting, forecasting', + 'education' => 'Bachelor\'s degree in Finance, Accounting, or Economics', + 'experience' => '2-4 years of financial analysis experience', + 'description' => 'Looking for a detail-oriented Financial Analyst to support our finance team', + 'responsibilities' => 'Prepare financial reports, conduct variance analysis, support budgeting process, provide financial insights' + ], + 'Operations Manager' => [ + 'skills' => 'Operations management, process improvement, team leadership, project management', + 'education' => 'Bachelor\'s degree in Business, Operations, or Engineering', + 'experience' => '5+ years of operations management experience', + 'description' => 'Seeking an Operations Manager to optimize our business processes and operations', + 'responsibilities' => 'Manage daily operations, improve processes, lead operational teams, ensure quality standards' + ] + ]; + + foreach ($companies as $company) { + // Get job categories for this company + $jobCategories = JobCategory::where('created_by', $company->id)->get(); + + if ($jobCategories->isEmpty()) { + $this->command->warn('No job categories found for company: ' . $company->name . '. Please run JobCategorySeeder first.'); + continue; + } + + // Get departments for this company + $departments = Department::where('created_by', $company->id)->get(); + + // Get managers/HR for approval + $approvers = User::whereIn('type', ['manager', 'hr'])->where('created_by', $company->id)->get(); + $approver = $approvers->isNotEmpty() ? $approvers->first() : null; + + $requisitionCounter = ($company->id - 1) * 100 + 1; + + foreach ($jobCategories as $category) { + $categoryRequisitions = $requisitionsByCategory[$category->name] ?? []; + + foreach ($categoryRequisitions as $reqData) { + $requisitionCode = 'REQ-' . $currentYear . '-' . str_pad($requisitionCounter, 4, '0', STR_PAD_LEFT); + + // Check if requisition already exists + if (JobRequisition::where('requisition_code', $requisitionCode)->exists()) { + $requisitionCounter++; + continue; + } + + $selectedDepartments = $departments->take(5); + $department = $selectedDepartments->isNotEmpty() ? $selectedDepartments->random() : null; + $details = $jobDetails[$reqData['title']] ?? [ + 'skills' => 'Relevant skills for the position', + 'education' => 'Bachelor\'s degree or equivalent', + 'experience' => '2+ years of relevant experience', + 'description' => 'We are looking for a qualified candidate for this position', + 'responsibilities' => 'Perform duties as assigned and contribute to team success' + ]; + + try { + JobRequisition::create([ + 'requisition_code' => $requisitionCode, + 'title' => $reqData['title'], + 'job_category_id' => $category->id, + 'department_id' => $department?->id, + 'positions_count' => $reqData['positions'], + 'budget_min' => $reqData['budget_min'], + 'budget_max' => $reqData['budget_max'], + 'skills_required' => $details['skills'], + 'education_required' => $details['education'], + 'experience_required' => $details['experience'], + 'description' => $details['description'], + 'responsibilities' => $details['responsibilities'], + 'status' => $reqData['status'], + 'approved_by' => $reqData['status'] === 'Approved' ? $approver?->id : null, + 'approval_date' => $reqData['status'] === 'Approved' ? now() : null, + 'priority' => $reqData['priority'], + 'created_by' => $company->id, + ]); + + $requisitionCounter++; + } catch (\Exception $e) { + $this->command->error('Failed to create job requisition: ' . $reqData['title'] . ' for company: ' . $company->name); + $requisitionCounter++; + continue; + } + } + } + } + + $this->command->info('JobRequisition seeder completed successfully!'); + } +} diff --git a/database/seeders/JobTypeSeeder.php b/database/seeders/JobTypeSeeder.php new file mode 100644 index 000000000..54b980e36 --- /dev/null +++ b/database/seeders/JobTypeSeeder.php @@ -0,0 +1,91 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed job types for consistent data + $jobTypes = [ + [ + 'name' => 'Full-time', + 'description' => 'Regular full-time employment with standard working hours, typically 40 hours per week with full benefits', + 'status' => 'active' + ], + [ + 'name' => 'Part-time', + 'description' => 'Part-time employment with reduced working hours, typically less than 30 hours per week with limited benefits', + 'status' => 'active' + ], + [ + 'name' => 'Contract', + 'description' => 'Fixed-term contract employment for specific projects or duration with defined start and end dates', + 'status' => 'active' + ], + [ + 'name' => 'Temporary', + 'description' => 'Short-term temporary employment to cover immediate staffing needs or seasonal work requirements', + 'status' => 'active' + ], + [ + 'name' => 'Freelance', + 'description' => 'Independent contractor arrangement for specific tasks or projects with flexible working arrangements', + 'status' => 'active' + ], + [ + 'name' => 'Internship', + 'description' => 'Educational work experience program for students or recent graduates to gain practical skills', + 'status' => 'active' + ], + [ + 'name' => 'Remote', + 'description' => 'Work-from-home or remote work arrangement allowing employees to work from any location', + 'status' => 'active' + ], + [ + 'name' => 'Hybrid', + 'description' => 'Combination of office and remote work, typically requiring presence in office for certain days', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($jobTypes as $typeData) { + // Check if job type already exists for this company + if (JobType::where('name', $typeData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + JobType::create([ + 'name' => $typeData['name'], + 'description' => $typeData['description'], + 'status' => $typeData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create job type: ' . $typeData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('JobType seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/JoiningLetterTemplateSeeder.php b/database/seeders/JoiningLetterTemplateSeeder.php new file mode 100644 index 000000000..d58ab7203 --- /dev/null +++ b/database/seeders/JoiningLetterTemplateSeeder.php @@ -0,0 +1,83 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $languages = [ + 'ar' => 'خطاب الانضمام', + 'da' => 'Tiltrædelsesbreve', + 'de' => 'Beitrittsschreiben', + 'en' => 'Joining Letter', + 'es' => 'Carta de Incorporación', + 'fr' => 'Lettre d\'Adhésion', + 'he' => 'מכתב הצטרפות', + 'it' => 'Lettera di Adesione', + 'ja' => '入社通知書', + 'nl' => 'Toetredingsbrief', + 'pl' => 'List Dołączenia', + 'pt' => 'Carta de Adesão', + 'pt-BR' => 'Carta de Adesão', + 'ru' => 'Письмо о Присоединении', + 'tr' => 'Katılım Mektubu', + 'zh' => '入职信' + ]; + + $templates = [ + 'ar' => '

خطاب الانضمام

التاريخ: {date}

عزيزي/عزيزتي {employee_name}،

يسعدنا أن نرحب بك في {company_name} بصفتك {designation}.

تاريخ بدء العمل: {joining_date}
الراتب: {salary}
القسم: {department}

نتطلع إلى مساهمتك القيمة في نجاح شركتنا.

مع أطيب التحيات،
قسم الموارد البشرية
{company_name}

', + 'da' => '

Tiltrædelsesbreve

Dato: {date}

Kære {employee_name},

Vi er glade for at byde dig velkommen til {company_name} som {designation}.

Startdato: {joining_date}
Løn: {salary}
Afdeling: {department}

Vi ser frem til dit værdifulde bidrag til vores virksomheds succes.

Med venlig hilsen,
HR-afdelingen
{company_name}

', + 'de' => '

Beitrittsschreiben

Datum: {date}

Liebe/r {employee_name},

Wir freuen uns, Sie bei {company_name} als {designation} willkommen zu heißen.

Startdatum: {joining_date}
Gehalt: {salary}
Abteilung: {department}

Wir freuen uns auf Ihren wertvollen Beitrag zum Erfolg unseres Unternehmens.

Mit freundlichen Grüßen,
Personalabteilung
{company_name}

', + 'en' => '

Joining Letter

Date: {date}

Dear {employee_name},

We are pleased to welcome you to {company_name} as {designation}.

Joining Date: {joining_date}
Salary: {salary}
Department: {department}

We look forward to your valuable contribution to our company\'s success.

Best regards,
HR Department
{company_name}

', + 'es' => '

Carta de Incorporación

Fecha: {date}

Estimado/a {employee_name},

Nos complace darle la bienvenida a {company_name} como {designation}.

Fecha de Incorporación: {joining_date}
Salario: {salary}
Departamento: {department}

Esperamos con interés su valiosa contribución al éxito de nuestra empresa.

Saludos cordiales,
Departamento de RRHH
{company_name}

', + 'fr' => '

Lettre d\'Adhésion

Date: {date}

Cher/Chère {employee_name},

Nous sommes heureux de vous accueillir chez {company_name} en tant que {designation}.

Date d\'Entrée: {joining_date}
Salaire: {salary}
Département: {department}

Nous attendons avec impatience votre précieuse contribution au succès de notre entreprise.

Cordialement,
Département RH
{company_name}

', + 'he' => '

מכתב הצטרפות

תאריך: {date}

{employee_name} יקר/ה,

אנו שמחים לקבל אותך ל-{company_name} בתפקיד {designation}.

תאריך התחלה: {joining_date}
משכורת: {salary}
מחלקה: {department}

אנו מצפים לתרומתך החשובה להצלחת החברה שלנו.

בברכה,
מחלקת משאבי אנוש
{company_name}

', + 'it' => '

Lettera di Adesione

Data: {date}

Caro/a {employee_name},

Siamo lieti di darti il benvenuto in {company_name} come {designation}.

Data di Inizio: {joining_date}
Stipendio: {salary}
Dipartimento: {department}

Non vediamo l\'ora del tuo prezioso contributo al successo della nostra azienda.

Cordiali saluti,
Dipartimento HR
{company_name}

', + 'ja' => '

入社通知書

日付: {date}

{employee_name}

{company_name}に{designation}としてご入社いただき、心より歓迎いたします。

入社日: {joining_date}
給与: {salary}
部署: {department}

弊社の成功への貴重な貢献を楽しみにしております。

敬具
人事部
{company_name}

', + 'nl' => '

Toetredingsbrief

Datum: {date}

Beste {employee_name},

We zijn verheugd u te verwelkomen bij {company_name} als {designation}.

Startdatum: {joining_date}
Salaris: {salary}
Afdeling: {department}

We kijken uit naar uw waardevolle bijdrage aan het succes van ons bedrijf.

Met vriendelijke groet,
HR Afdeling
{company_name}

', + 'pl' => '

List Dołączenia

Data: {date}

Drogi/a {employee_name},

Mamy przyjemność powitać Cię w {company_name} na stanowisku {designation}.

Data Rozpoczęcia: {joining_date}
Wynagrodzenie: {salary}
Dział: {department}

Czekamy na Twój cenny wkład w sukces naszej firmy.

Z poważaniem,
Dział HR
{company_name}

', + 'pt' => '

Carta de Adesão

Data: {date}

Caro/a {employee_name},

Temos o prazer de dar-lhe as boas-vindas à {company_name} como {designation}.

Data de Início: {joining_date}
Salário: {salary}
Departamento: {department}

Esperamos ansiosamente sua valiosa contribuição para o sucesso da nossa empresa.

Atenciosamente,
Departamento de RH
{company_name}

', + 'pt-BR' => '

Carta de Adesão

Data: {date}

Caro/a {employee_name},

Temos o prazer de dar-lhe as boas-vindas à {company_name} como {designation}.

Data de Início: {joining_date}
Salário: {salary}
Departamento: {department}

Esperamos ansiosamente sua valiosa contribuição para o sucesso da nossa empresa.

Atenciosamente,
Departamento de RH
{company_name}

', + 'ru' => '

Письмо о Присоединении

Дата: {date}

Уважаемый/ая {employee_name},

Мы рады приветствовать вас в {company_name} на должности {designation}.

Дата Начала Работы: {joining_date}
Зарплата: {salary}
Отдел: {department}

Мы с нетерпением ждем вашего ценного вклада в успех нашей компании.

С уважением,
Отдел кадров
{company_name}

', + 'tr' => '

Katılım Mektubu

Tarih: {date}

Sayın {employee_name},

Sizi {company_name} şirketinde {designation} pozisyonunda karşılamaktan memnuniyet duyuyoruz.

İşe Başlama Tarihi: {joining_date}
Maaş: {salary}
Departman: {department}

Şirketimizin başarısına değerli katkınızı dört gözle bekliyoruz.

Saygılarımızla,
İnsan Kaynakları Departmanı
{company_name}

', + 'zh' => '

入职信

日期:{date}

亲爱的{employee_name}

我们很高兴欢迎您加入{company_name},担任{designation}职位。

入职日期:{joining_date}
薪资:{salary}
部门:{department}

我们期待您为公司成功做出宝贵贡献。

此致
人力资源部
{company_name}

' + ]; + + $variables = json_encode(['date', 'company_name', 'employee_name', 'designation', 'joining_date', 'salary', 'department']); + + foreach ($companies as $company) { + foreach ($languages as $code => $title) { + try { + JoiningLetterTemplate::updateOrCreate( + [ + 'language' => $code, + 'created_by' => $company->id + ], + [ + 'content' => $templates[$code] ?? $templates['en'], + 'variables' => $variables + ] + ); + } catch (\Exception $e) { + $this->command->error('Failed to create Joining Letter template for language: ' . $code . ' and company: ' . $company->name); + continue; + } + } + } + + $this->command->info('JoiningLetterTemplate seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/LandingPageCustomPageSeeder.php b/database/seeders/LandingPageCustomPageSeeder.php new file mode 100644 index 000000000..0e6b1c7e1 --- /dev/null +++ b/database/seeders/LandingPageCustomPageSeeder.php @@ -0,0 +1,143 @@ +getSaasPages() : $this->getNonSaasPages(); + + foreach ($pages as $pageData) { + LandingPageCustomPage::firstOrCreate( + ['slug' => $pageData['slug']], + $pageData + ); + } + + $this->command->info('Landing page custom pages seeded successfully!'); + } + + private function getSaasPages() + { + return [ + [ + 'title' => 'About Us', + 'slug' => 'about-us', + 'content' => "About Our HRM SaaS Solution: Transforming workforce management with smart HR software.
We are dedicated to helping businesses streamline HR operations and empower employees.
Our HRM SaaS platform simplifies human resource management for companies of all sizes, from recruitment to payroll, performance tracking to compliance, ensuring efficiency, accuracy, and employee satisfaction.
Built with a vision to make HR effortless, we automate repetitive tasks, centralize data, and provide actionable insights. Effective HR management is the backbone of organizational success.
Stats: • 5+ Years Industry Experience • 15K+ Active Users • 60+ Countries Served
Our Mission: Redefine HR management by providing simple, scalable, and intelligent solutions that save time and boost productivity.
Our Values: Innovation, integrity, and inclusivity ensure businesses and employees thrive together.
Our Commitment: Deliver secure, scalable, and user-friendly HR solutions with outstanding customer support.
Our Vision: A future where businesses focus on growth while HR runs seamlessly in the background with automation and intelligence.", + 'meta_title' => 'About Us - HRM SaaS Platform', + 'meta_description' => 'Discover our HRM SaaS solution – a smart platform that simplifies payroll, attendance, recruitment, and performance management for businesses worldwide.', + 'is_active' => true, + 'sort_order' => 1 + ], + [ + 'title' => 'Privacy Policy', + 'slug' => 'privacy-policy', + 'content' => "Your privacy is important to us. This Privacy Policy explains how our HRM SaaS software collects, uses, and safeguards your data.
Information We Collect: • Employee information such as name, contact details, and work profile • Payroll and compensation details • Attendance, leave records, and shift schedules • Performance reviews, training data, and appraisals • System usage analytics and activity logs
How We Use Your Information: • Provide, maintain, and improve HRM SaaS services • Automate payroll processing, tax calculations, and compliance reports • Track attendance, leaves, and performance efficiently • Communicate important updates, policies, and notifications • Ensure data accuracy, security, and compliance with labor laws
Information Sharing: We do not sell or trade personal employee data. Information may be shared with: • Authorized company administrators and HR managers • Third-party service providers for operational purposes • Legal authorities if required by law
Data Security: We use encryption, access control, and audits to protect HR data from unauthorized access, misuse, or disclosure.
Data Retention: Data is retained as long as required to fulfill policy purposes or as mandated by law; can be deleted or anonymized when no longer needed.
Your Rights: You may access, update, or request deletion of personal data. Contact your employer or our support team for platform concerns.", + 'meta_title' => 'Privacy Policy - HRM SaaS', + 'meta_description' => 'Read the privacy policy of our HRM SaaS platform to understand how employee and HR data is collected, used, and protected.', + 'is_active' => true, + 'sort_order' => 2 + ], + [ + 'title' => 'Terms of Service', + 'slug' => 'terms-of-service', + 'content' => "Please read these terms carefully before using our HRM SaaS platform. By accessing or using our services, you agree to these terms.

Acceptance of Terms: By creating an account or using our HRM SaaS product, you confirm that you have read, understood, and agree to be bound by these Terms of Service. If you do not agree, you may not use the platform.

Service Description: Our platform provides businesses with Human Resource Management solutions, including but not limited to:
• Employee records and profile management
• Attendance and leave tracking
• Payroll and compensation management
• Performance evaluation tools
• Reports, analytics, and integrations

User Responsibilities: As a user of our HRM SaaS, you agree to:
• Provide accurate and updated information when creating an account
• Maintain confidentiality of your login credentials
• Ensure that all uploaded content complies with applicable laws
• Use the platform only for lawful HR management purposes

Subscription & Payments: You agree to pay all fees associated with your chosen plan in accordance with the billing terms. Failure to pay may result in suspension or termination of your account.

Termination of Service: We reserve the right to suspend or terminate your access if you violate these Terms or engage in harmful activities.

Data & Privacy: Your data will be handled per our Privacy Policy. You are responsible for safeguarding your account access.

Limitation of Liability: Our company shall not be held liable for any indirect, incidental, or consequential damages arising from your use of the HRM SaaS platform.", + 'meta_title' => 'Terms of Service - HRM SaaS', + 'meta_description' => 'Read our terms of service to understand the rules and responsibilities for using our HRM SaaS platform.', + 'is_active' => true, + 'sort_order' => 3 + ], + [ + 'title' => 'Contact Us', + 'slug' => 'contact-us', + 'content' => "Have questions about HRM SaaS ? Our team is here to guide you every step of the way.

Send us a Message: Fill out the form with your Full Name, Email Address, Subject, and Message. Our team will respond promptly.

Contact Information:
Email Us: support@hrm.com (Our team typically responds within 24 hours)
Call Us: +1 (555) 123-4567 (Available Monday – Friday, 9am – 6pm EST)
Visit Us: 123 Business Ave, Suite 100, San Francisco, CA 94105

Business Hours:
• Monday - Friday: 9:00 AM - 6:00 PM EST
• Saturday: 10:00 AM - 4:00 PM EST
• Sunday: Closed", + 'meta_title' => 'Contact Us - HRM Support', + 'meta_description' => 'Get in touch with the HRM team for product support, questions, or partnership opportunities.', + 'is_active' => true, + 'sort_order' => 4 + ], + [ + 'title' => 'FAQ', + 'slug' => 'faq', + 'content' => "Get quick answers to the most common queries about using our HRM SaaS platform.

Getting Started:
What is HRM SaaS? Our HRM SaaS is an all-in-one platform to manage your employees, payroll, attendance, performance, and leave. It simplifies HR operations and helps businesses run efficiently.
How do I get started? Follow these steps to set up your account:
• Sign up for an HRM SaaS account
• Set up your company profile and departments
• Add employees and assign roles
• Configure payroll, attendance, and leave policies
• Start managing HR processes efficiently

Features & Plans:
Which plans are available? We offer multiple subscription plans including Basic, Professional, and Enterprise, each with features tailored for businesses of all sizes.
Can I customize HR workflows? Yes, you can customize workflows for attendance, leave approvals, performance reviews, and payroll according to your company policies.

Analytics & Support:
How can I monitor HR metrics? Our analytics dashboard provides real-time insights on employee attendance, payroll reports, performance trends, and leave balances, helping you make informed HR decisions.", + 'meta_title' => 'FAQ - HRM Help Center', + 'meta_description' => 'Find answers to frequently asked questions about HRM SaaS, including features, plans, employee management, and analytics.', + 'is_active' => true, + 'sort_order' => 5 + ], + [ + 'title' => 'Refund Policy', + 'slug' => 'refund-policy', + 'content' => "We are committed to your satisfaction. Below is our refund policy, including eligibility, process, and exceptions.

30-Day Money Back Guarantee: We offer a 30-day money-back guarantee for all premium plans. If you are not completely satisfied with HRM SaaS, we will refund your payment in full within 30 days of purchase.

Eligible Refunds:
- Monthly and annual subscription plans
- One-time premium features or add-ons
- Unused portions of prepaid services

Refund Process:
1. Contact our support team within 30 days of purchase
2. Provide your account details and the reason for the refund
3. We will review and process your request within 3–5 business days
4. Refunds will be issued to your original payment method

Non-Refundable Items:
- Custom development work or integrations
- Third-party services and add-ons
- Domain registration fees
- Services used after the 30-day guarantee period

If you have any questions regarding our refund policy, please contact our support team. We are happy to assist!", + 'meta_title' => 'Refund Policy - HRM', + 'meta_description' => 'Learn about our refund policy and money-back guarantee for HRM services.', + 'is_active' => true, + 'sort_order' => 6 + ] + ]; + } + + private function getNonSaasPages() + { + return [ + [ + 'title' => 'About Us', + 'slug' => 'about-us', + 'content' => "About Our HRM Solution: Transforming workforce management with smart HR software.
We are dedicated to helping businesses streamline HR operations and empower employees.
Our HRM platform simplifies human resource management for companies of all sizes, from recruitment to payroll, performance tracking to compliance, ensuring efficiency, accuracy, and employee satisfaction.
Built with a vision to make HR effortless, we automate repetitive tasks, centralize data, and provide actionable insights. Effective HR management is the backbone of organizational success.
Stats: • 5+ Years Industry Experience • 15K+ Active Users • 60+ Countries Served
Our Mission: Redefine HR management by providing simple, scalable, and intelligent solutions that save time and boost productivity.
Our Values: Innovation, integrity, and inclusivity ensure businesses and employees thrive together.
Our Commitment: Deliver secure, scalable, and user-friendly HR solutions with outstanding customer support.
Our Vision: A future where businesses focus on growth while HR runs seamlessly in the background with automation and intelligence.", + 'meta_title' => 'About Us - HRM Platform', + 'meta_description' => 'Discover our HRM solution – a smart platform that simplifies payroll, attendance, recruitment, and performance management for businesses worldwide.', + 'is_active' => true, + 'sort_order' => 1 + ], + [ + 'title' => 'Privacy Policy', + 'slug' => 'privacy-policy', + 'content' => "Your privacy is important to us. This Privacy Policy explains how our HRM software collects, uses, and safeguards your data.
Information We Collect: • Employee information such as name, contact details, and work profile • Payroll and compensation details • Attendance, leave records, and shift schedules • Performance reviews, training data, and appraisals • System usage analytics and activity logs
How We Use Your Information: • Provide, maintain, and improve HRM services • Automate payroll processing, tax calculations, and compliance reports • Track attendance, leaves, and performance efficiently • Communicate important updates, policies, and notifications • Ensure data accuracy, security, and compliance with labor laws
Information Sharing: We do not sell or trade personal employee data. Information may be shared with: • Authorized company administrators and HR managers • Third-party service providers for operational purposes • Legal authorities if required by law
Data Security: We use encryption, access control, and audits to protect HR data from unauthorized access, misuse, or disclosure.
Data Retention: Data is retained as long as required to fulfill policy purposes or as mandated by law; can be deleted or anonymized when no longer needed.
Your Rights: You may access, update, or request deletion of personal data. Contact your employer or our support team for platform concerns.", + 'meta_title' => 'Privacy Policy - HRM', + 'meta_description' => 'Read the privacy policy of our HRM platform to understand how employee and HR data is collected, used, and protected.', + 'is_active' => true, + 'sort_order' => 2 + ], + [ + 'title' => 'Terms of Service', + 'slug' => 'terms-of-service', + 'content' => "Please read these terms carefully before using our HRM platform. By accessing or using our services, you agree to these terms.

Acceptance of Terms: By creating an account or using our HRM product, you confirm that you have read, understood, and agree to be bound by these Terms of Service. If you do not agree, you may not use the platform.

Service Description: Our platform provides businesses with Human Resource Management solutions, including but not limited to:
• Employee records and profile management
• Attendance and leave tracking
• Payroll and compensation management
• Performance evaluation tools
• Reports, analytics, and integrations

User Responsibilities: As a user of our HRM, you agree to:
• Provide accurate and updated information when creating an account
• Maintain confidentiality of your login credentials
• Ensure that all uploaded content complies with applicable laws
• Use the platform only for lawful HR management purposes

Service & Payments: You agree to pay all fees associated with your service in accordance with the billing terms. Failure to pay may result in suspension or termination of your account.

Termination of Service: We reserve the right to suspend or terminate your access if you violate these Terms or engage in harmful activities.

Data & Privacy: Your data will be handled per our Privacy Policy. You are responsible for safeguarding your account access.

Limitation of Liability: Our company shall not be held liable for any indirect, incidental, or consequential damages arising from your use of the HRM platform.", + 'meta_title' => 'Terms of Service - HRM', + 'meta_description' => 'Read our terms of service to understand the rules and responsibilities for using our HRM platform.', + 'is_active' => true, + 'sort_order' => 3 + ], + [ + 'title' => 'Contact Us', + 'slug' => 'contact-us', + 'content' => "Have questions about HRM? Our team is here to guide you every step of the way.

Send us a Message: Fill out the form with your Full Name, Email Address, Subject, and Message. Our team will respond promptly.

Contact Information:
Email Us: support@hrm.com (Our team typically responds within 24 hours)
Call Us: +1 (555) 123-4567 (Available Monday – Friday, 9am – 6pm EST)
Visit Us: 123 Business Ave, Suite 100, San Francisco, CA 94105

Business Hours:
• Monday - Friday: 9:00 AM - 6:00 PM EST
• Saturday: 10:00 AM - 4:00 PM EST
• Sunday: Closed", + 'meta_title' => 'Contact Us - HRM Support', + 'meta_description' => 'Get in touch with the HRM team for product support, questions, or partnership opportunities.', + 'is_active' => true, + 'sort_order' => 4 + ], + [ + 'title' => 'FAQ', + 'slug' => 'faq', + 'content' => "Get quick answers to the most common queries about using our HRM platform.

Getting Started:
What is HRM? Our HRM is an all-in-one platform to manage your employees, payroll, attendance, performance, and leave. It simplifies HR operations and helps businesses run efficiently.
How do I get started? Follow these steps to set up your account:
• Contact us to set up your HRM account
• Set up your company profile and departments
• Add employees and assign roles
• Configure payroll, attendance, and leave policies
• Start managing HR processes efficiently

Features & Implementation:
What features are available? We offer comprehensive HR management features including employee management, payroll processing, attendance tracking, performance reviews, and reporting.
Can I customize HR workflows? Yes, you can customize workflows for attendance, leave approvals, performance reviews, and payroll according to your company policies.

Analytics & Support:
How can I monitor HR metrics? Our analytics dashboard provides real-time insights on employee attendance, payroll reports, performance trends, and leave balances, helping you make informed HR decisions.", + 'meta_title' => 'FAQ - HRM Help Center', + 'meta_description' => 'Find answers to frequently asked questions about HRM, including features, implementation, employee management, and analytics.', + 'is_active' => true, + 'sort_order' => 5 + ], + [ + 'title' => 'Refund Policy', + 'slug' => 'refund-policy', + 'content' => "We are committed to your satisfaction. Below is our refund policy, including eligibility, process, and exceptions.

30-Day Money Back Guarantee: We offer a 30-day money-back guarantee for all services. If you are not completely satisfied with HRM, we will refund your payment in full within 30 days of purchase.

Eligible Refunds:
- Service fees and implementation costs
- One-time premium features or add-ons
- Unused portions of prepaid services

Refund Process:
1. Contact our support team within 30 days of purchase
2. Provide your account details and the reason for the refund
3. We will review and process your request within 3–5 business days
4. Refunds will be issued to your original payment method

Non-Refundable Items:
- Custom development work or integrations
- Third-party services and add-ons
- Domain registration fees
- Services used after the 30-day guarantee period

If you have any questions regarding our refund policy, please contact our support team. We are happy to assist!", + 'meta_title' => 'Refund Policy - HRM', + 'meta_description' => 'Learn about our refund policy and money-back guarantee for HRM services.', + 'is_active' => true, + 'sort_order' => 6 + ] + ]; + } +} \ No newline at end of file diff --git a/database/seeders/LeaveApplicationSeeder.php b/database/seeders/LeaveApplicationSeeder.php new file mode 100644 index 000000000..a6409a03b --- /dev/null +++ b/database/seeders/LeaveApplicationSeeder.php @@ -0,0 +1,141 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed leave application patterns for each month + $monthlyLeavePatterns = [ + 1 => ['leave_type' => 'Annual Leave', 'days' => 2, 'reason' => 'New Year vacation', 'status' => 'approved'], + 2 => ['leave_type' => 'Sick Leave', 'days' => 1, 'reason' => 'Fever and cold symptoms', 'status' => 'approved'], + 3 => ['leave_type' => 'Personal Leave', 'days' => 1, 'reason' => 'Personal appointment', 'status' => 'approved'], + 4 => ['leave_type' => 'Annual Leave', 'days' => 3, 'reason' => 'Family vacation', 'status' => 'approved'], + 5 => ['leave_type' => 'Emergency Leave', 'days' => 1, 'reason' => 'Family emergency', 'status' => 'approved'], + 6 => ['leave_type' => 'Annual Leave', 'days' => 2, 'reason' => 'Weekend extension', 'status' => 'approved'], + 7 => ['leave_type' => 'Sick Leave', 'days' => 2, 'reason' => 'Medical checkup and recovery', 'status' => 'approved'], + 8 => ['leave_type' => 'Annual Leave', 'days' => 1, 'reason' => 'Personal work', 'status' => 'approved'], + 9 => ['leave_type' => 'Annual Leave', 'days' => 3, 'reason' => 'Festival celebration', 'status' => 'approved'], + 10 => ['leave_type' => 'Sick Leave', 'days' => 1, 'reason' => 'Routine medical appointment', 'status' => 'approved'], + 11 => ['leave_type' => 'Annual Leave', 'days' => 2, 'reason' => 'Long weekend break', 'status' => 'approved'], + 12 => ['leave_type' => 'Annual Leave', 'days' => 4, 'reason' => 'Year end vacation', 'status' => 'approved'] + ]; + + $currentYear = date('Y'); + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get leave types and policies for this company + $leaveTypes = LeaveType::where('created_by', $company->id)->get(); + $leavePolicies = LeavePolicy::where('created_by', $company->id)->get(); + + if ($leaveTypes->isEmpty() || $leavePolicies->isEmpty()) { + $this->command->warn('No leave types or policies found for company: ' . $company->name . '. Please run LeaveTypeSeeder and LeavePolicySeeder first.'); + continue; + } + + // Get managers for approval + $managers = User::whereIn('type', ['manager', 'hr'])->where('created_by', $company->id)->get(); + + // Select first 5 employees for leave applications + $selectedEmployees = $employees->take(5); + + foreach ($selectedEmployees as $empIndex => $employee) { + // Create leave applications for each month of current year + for ($month = 1; $month <= 12; $month++) { + $pattern = $monthlyLeavePatterns[$month]; + + // Find matching leave type and policy + $leaveType = $leaveTypes->where('name', $pattern['leave_type'])->first(); + if (!$leaveType) $leaveType = $leaveTypes->first(); + + $leavePolicy = $leavePolicies->where('leave_type_id', $leaveType->id)->first(); + if (!$leavePolicy) $leavePolicy = $leavePolicies->first(); + + // Calculate leave dates (avoid weekends and vary dates) + $baseDay = 5 + ($empIndex * 3) + ($month * 2); // Vary start day based on employee and month + $startCarbon = Carbon::create($currentYear, $month, min($baseDay, 28)); + + // Skip weekends for start date + while ($startCarbon->isWeekend()) { + $startCarbon->addDay(); + } + + $endCarbon = $startCarbon->copy()->addDays($pattern['days'] - 1); + + // Skip weekends for end date + while ($endCarbon->isWeekend()) { + $endCarbon->addDay(); + } + + $startDate = Carbon::parse($startCarbon->format('Y-m-d')); + $endDate = Carbon::parse($endCarbon->format('Y-m-d')); + $leaveDays = $startDate->diffInDays($endDate) + 1; + + + + // Select approver + $approver = $managers->isNotEmpty() ? $managers->first() : null; + + // Check if leave application already exists + if (LeaveApplication::where('employee_id', $employee->id) + ->where('start_date', $startDate) + ->where('leave_type_id', $leaveType->id) + ->exists() + ) { + continue; + } + + try { + LeaveApplication::create([ + 'employee_id' => $employee->id, + 'leave_type_id' => $leaveType->id, + 'leave_policy_id' => $leavePolicy->id, + 'start_date' => $startDate, + 'end_date' => $endDate, + 'total_days' => $leaveDays, + 'reason' => $pattern['reason'], + 'attachment' => randomImage(), + 'status' => $pattern['status'], + 'manager_comments' => $pattern['status'] === 'approved' ? 'Leave approved as per company policy' : null, + 'approved_by' => $pattern['status'] === 'approved' ? $approver?->id : null, + 'approved_at' => $pattern['status'] === 'approved' ? now() : null, + 'created_by' => $employee->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create leave application for employee: ' . $employee->name . ' in month: ' . $month . ' for company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('LeaveApplication seeder completed successfully!'); + } +} diff --git a/database/seeders/LeaveBalanceSeeder.php b/database/seeders/LeaveBalanceSeeder.php new file mode 100644 index 000000000..8bccab602 --- /dev/null +++ b/database/seeders/LeaveBalanceSeeder.php @@ -0,0 +1,107 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get leave types and policies for this company + $leaveTypes = LeaveType::where('created_by', $company->id)->get(); + $leavePolicies = LeavePolicy::where('created_by', $company->id)->get(); + + if ($leaveTypes->isEmpty() || $leavePolicies->isEmpty()) { + $this->command->warn('No leave types or policies found for company: ' . $company->name . '. Please run LeaveTypeSeeder and LeavePolicySeeder first.'); + continue; + } + + foreach ($employees as $employee) { + foreach ($leaveTypes as $leaveType) { + // Find matching leave policy + $leavePolicy = $leavePolicies->where('leave_type_id', $leaveType->id)->first(); + if (!$leavePolicy) continue; + + // Check if leave balance already exists + if (LeaveBalance::where('employee_id', $employee->id) + ->where('leave_type_id', $leaveType->id) + ->where('year', $currentYear) + ->exists() + ) { + continue; + } + + // Calculate allocated days from policy + $allocatedDays = $leavePolicy->accrual_rate; + + // Calculate used days from approved leave applications + $usedDays = LeaveApplication::where('employee_id', $employee->id) + ->where('leave_type_id', $leaveType->id) + ->where('status', 'approved') + ->whereYear('start_date', $currentYear) + ->sum('total_days'); + + // Calculate remaining days + $remainingDays = $allocatedDays - $usedDays; + + // Set carried forward based on leave type (only for Annual Leave) + $carriedForward = 0; + if ($leaveType->name === 'Annual Leave') { + $carriedForward = 2; // Fixed 2 days carried forward for annual leave + $allocatedDays += $carriedForward; + $remainingDays += $carriedForward; + } + + try { + LeaveBalance::create([ + 'employee_id' => $employee->id, + 'leave_type_id' => $leaveType->id, + 'leave_policy_id' => $leavePolicy->id, + 'year' => $currentYear, + 'allocated_days' => $allocatedDays, + 'used_days' => $usedDays, + 'remaining_days' => max(0, $remainingDays), // Ensure non-negative + 'carried_forward' => $carriedForward, + 'manual_adjustment' => 0, + 'adjustment_reason' => null, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create leave balance for employee: ' . $employee->name . ' and leave type: ' . $leaveType->name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('LeaveBalance seeder completed successfully!'); + } +} diff --git a/database/seeders/LeavePolicySeeder.php b/database/seeders/LeavePolicySeeder.php new file mode 100644 index 000000000..93373862b --- /dev/null +++ b/database/seeders/LeavePolicySeeder.php @@ -0,0 +1,183 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed leave policies mapped to leave types + $leavePolicies = [ + 'Annual Leave' => [ + 'name' => 'Annual Leave Policy', + 'description' => 'Standard annual leave policy with yearly accrual and carry forward provisions', + 'accrual_type' => 'yearly', + 'accrual_rate' => 21.00, + 'carry_forward_limit' => 5, + 'min_days_per_application' => 1, + 'max_days_per_application' => 15, + 'requires_approval' => true, + 'status' => 'active' + ], + 'Sick Leave' => [ + 'name' => 'Sick Leave Policy', + 'description' => 'Medical leave policy for health-related absences with flexible application', + 'accrual_type' => 'yearly', + 'accrual_rate' => 10.00, + 'carry_forward_limit' => 2, + 'min_days_per_application' => 1, + 'max_days_per_application' => 10, + 'requires_approval' => false, + 'status' => 'active' + ], + 'Maternity Leave' => [ + 'name' => 'Maternity Leave Policy', + 'description' => 'Comprehensive maternity leave policy with extended duration and full pay', + 'accrual_type' => 'yearly', + 'accrual_rate' => 90.00, + 'carry_forward_limit' => 0, + 'min_days_per_application' => 30, + 'max_days_per_application' => 90, + 'requires_approval' => true, + 'status' => 'active' + ], + 'Paternity Leave' => [ + 'name' => 'Paternity Leave Policy', + 'description' => 'Paternity leave policy for new fathers with flexible scheduling', + 'accrual_type' => 'yearly', + 'accrual_rate' => 15.00, + 'carry_forward_limit' => 0, + 'min_days_per_application' => 1, + 'max_days_per_application' => 15, + 'requires_approval' => true, + 'status' => 'active' + ], + 'Emergency Leave' => [ + 'name' => 'Emergency Leave Policy', + 'description' => 'Urgent leave policy for unexpected situations with immediate approval', + 'accrual_type' => 'yearly', + 'accrual_rate' => 5.00, + 'carry_forward_limit' => 0, + 'min_days_per_application' => 1, + 'max_days_per_application' => 3, + 'requires_approval' => false, + 'status' => 'active' + ], + 'Bereavement Leave' => [ + 'name' => 'Bereavement Leave Policy', + 'description' => 'Compassionate leave policy for family bereavement with immediate effect', + 'accrual_type' => 'yearly', + 'accrual_rate' => 7.00, + 'carry_forward_limit' => 0, + 'min_days_per_application' => 1, + 'max_days_per_application' => 7, + 'requires_approval' => false, + 'status' => 'active' + ], + 'Study Leave' => [ + 'name' => 'Study Leave Policy', + 'description' => 'Educational leave policy for professional development and skill enhancement', + 'accrual_type' => 'yearly', + 'accrual_rate' => 10.00, + 'carry_forward_limit' => 5, + 'min_days_per_application' => 1, + 'max_days_per_application' => 10, + 'requires_approval' => true, + 'status' => 'active' + ], + 'Compensatory Leave' => [ + 'name' => 'Compensatory Leave Policy', + 'description' => 'Time off policy for overtime work compensation with monthly accrual', + 'accrual_type' => 'monthly', + 'accrual_rate' => 1.00, + 'carry_forward_limit' => 3, + 'min_days_per_application' => 1, + 'max_days_per_application' => 5, + 'requires_approval' => true, + 'status' => 'active' + ], + 'Personal Leave' => [ + 'name' => 'Personal Leave Policy', + 'description' => 'Personal time off policy for individual matters without pay', + 'accrual_type' => 'yearly', + 'accrual_rate' => 5.00, + 'carry_forward_limit' => 0, + 'min_days_per_application' => 1, + 'max_days_per_application' => 3, + 'requires_approval' => true, + 'status' => 'active' + ], + 'Marriage Leave' => [ + 'name' => 'Marriage Leave Policy', + 'description' => 'Special leave policy for wedding ceremonies and related celebrations', + 'accrual_type' => 'yearly', + 'accrual_rate' => 7.00, + 'carry_forward_limit' => 0, + 'min_days_per_application' => 1, + 'max_days_per_application' => 7, + 'requires_approval' => true, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + // Get leave types for this company + $leaveTypes = LeaveType::where('created_by', $company->id)->get(); + + if ($leaveTypes->isEmpty()) { + $this->command->warn('No leave types found for company: ' . $company->name . '. Please run LeaveTypeSeeder first.'); + continue; + } + + foreach ($leaveTypes as $leaveType) { + $policyData = $leavePolicies[$leaveType->name] ?? null; + + if (!$policyData) { + continue; + } + + // Check if leave policy already exists for this leave type and company + if (LeavePolicy::where('leave_type_id', $leaveType->id)->where('created_by', $company->id)->exists()) { + continue; + } + + try { + LeavePolicy::create([ + 'name' => $policyData['name'], + 'description' => $policyData['description'], + 'leave_type_id' => $leaveType->id, + 'accrual_type' => $policyData['accrual_type'], + 'accrual_rate' => $policyData['accrual_rate'], + 'carry_forward_limit' => $policyData['carry_forward_limit'], + 'min_days_per_application' => $policyData['min_days_per_application'], + 'max_days_per_application' => $policyData['max_days_per_application'], + 'requires_approval' => $policyData['requires_approval'], + 'status' => $policyData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create leave policy: ' . $policyData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('LeavePolicy seeder completed successfully!'); + } +} diff --git a/database/seeders/LeaveTypeSeeder.php b/database/seeders/LeaveTypeSeeder.php new file mode 100644 index 000000000..e07bedd06 --- /dev/null +++ b/database/seeders/LeaveTypeSeeder.php @@ -0,0 +1,134 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed leave types for consistent data + $leaveTypes = [ + [ + 'name' => 'Annual Leave', + 'description' => 'Yearly vacation leave for rest and recreation', + 'max_days_per_year' => 21, + 'is_paid' => true, + 'color' => '#10b77f', + 'status' => 'active' + ], + [ + 'name' => 'Sick Leave', + 'description' => 'Medical leave for illness or health-related issues', + 'max_days_per_year' => 10, + 'is_paid' => true, + 'color' => '#EF4444', + 'status' => 'active' + ], + [ + 'name' => 'Maternity Leave', + 'description' => 'Leave for new mothers during childbirth and recovery period', + 'max_days_per_year' => 90, + 'is_paid' => true, + 'color' => '#EC4899', + 'status' => 'active' + ], + [ + 'name' => 'Paternity Leave', + 'description' => 'Leave for new fathers to support family during childbirth', + 'max_days_per_year' => 15, + 'is_paid' => true, + 'color' => '#3B82F6', + 'status' => 'active' + ], + [ + 'name' => 'Emergency Leave', + 'description' => 'Urgent leave for unexpected personal or family emergencies', + 'max_days_per_year' => 5, + 'is_paid' => true, + 'color' => '#F59E0B', + 'status' => 'active' + ], + [ + 'name' => 'Bereavement Leave', + 'description' => 'Compassionate leave for death of family members or close relatives', + 'max_days_per_year' => 7, + 'is_paid' => true, + 'color' => '#6B7280', + 'status' => 'active' + ], + [ + 'name' => 'Study Leave', + 'description' => 'Educational leave for professional development and training', + 'max_days_per_year' => 10, + 'is_paid' => false, + 'color' => '#8B5CF6', + 'status' => 'active' + ], + [ + 'name' => 'Compensatory Leave', + 'description' => 'Time off in lieu of overtime work or weekend duties', + 'max_days_per_year' => 12, + 'is_paid' => true, + 'color' => '#06B6D4', + 'status' => 'active' + ], + [ + 'name' => 'Personal Leave', + 'description' => 'Personal time off for individual matters and commitments', + 'max_days_per_year' => 5, + 'is_paid' => false, + 'color' => '#84CC16', + 'status' => 'active' + ], + [ + 'name' => 'Marriage Leave', + 'description' => 'Special leave for wedding ceremonies and related celebrations', + 'max_days_per_year' => 7, + 'is_paid' => true, + 'color' => '#F97316', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($leaveTypes as $typeData) { + // Check if leave type already exists for this company + if (LeaveType::where('name', $typeData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + LeaveType::create([ + 'name' => $typeData['name'], + 'description' => $typeData['description'], + 'max_days_per_year' => $typeData['max_days_per_year'], + 'is_paid' => $typeData['is_paid'], + 'color' => $typeData['color'], + 'status' => $typeData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create leave type: ' . $typeData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('LeaveType seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/MediaItemSeeder.php b/database/seeders/MediaItemSeeder.php new file mode 100644 index 000000000..e0136d0a1 --- /dev/null +++ b/database/seeders/MediaItemSeeder.php @@ -0,0 +1,181 @@ +first(); + $companies = User::where('type', 'company')->get(); + + if (!$superAdmin && $isSaas) { + $this->command->warn('No super admin found. Please run DefaultCompanySeeder first.'); + return; + } + + if ($isSaas) { + // SaaS mode: Create media for both super admin and companies + if ($superAdmin) { + $this->createSuperAdminMedia($superAdmin); + } + foreach ($companies as $company) { + $this->createCompanyMedia($company); + } + } else { + // Non-SaaS mode: Create media only for companies + foreach ($companies as $company) { + $this->createCompanyMedia($company); + } + } + + $this->command->info('MediaItem seeder completed successfully!'); + } + + /** + * Create media directories and items for Super Admin + */ + private function createSuperAdminMedia($superAdmin) + { + // Create main system directories for Super Admin only + $directories = [ + ['name' => 'System Templates', 'slug' => 'system-templates'], + ['name' => 'System Documentation', 'slug' => 'system-documentation'], + ['name' => 'Default Assets', 'slug' => 'default-assets'] + ]; + + foreach ($directories as $dirData) { + if (!MediaDirectory::where('slug', $dirData['slug'])->where('created_by', $superAdmin->id)->exists()) { + MediaDirectory::create([ + 'name' => $dirData['name'], + 'slug' => $dirData['slug'], + 'parent_id' => null, + 'created_by' => $superAdmin->id, + ]); + } + } + + // Create system media items for Super Admin + $mediaItems = [ + ['collection_name' => 'system', 'name' => 'Default System Logo', 'file_name' => 'system-default-logo.png', 'mime_type' => 'image/png', 'size' => 15360, 'directory' => 'default-assets'], + ['collection_name' => 'templates', 'name' => 'System Template Image', 'file_name' => 'system-template-image.png', 'mime_type' => 'image/png', 'size' => 25600, 'directory' => 'system-templates'], + ['collection_name' => 'documentation', 'name' => 'System Documentation', 'file_name' => 'system-user-manual.pdf', 'mime_type' => 'application/pdf', 'size' => 1048576, 'directory' => 'system-documentation'], + ['collection_name' => 'system', 'name' => 'System Banner', 'file_name' => 'system-banner.png', 'mime_type' => 'image/png', 'size' => 204800, 'directory' => 'default-assets'], + ['collection_name' => 'system', 'name' => 'System Icon Set', 'file_name' => 'system-icons.png', 'mime_type' => 'image/png', 'size' => 51200, 'directory' => 'default-assets'], + ['collection_name' => 'templates', 'name' => 'Email Header Template', 'file_name' => 'email-header-template.png', 'mime_type' => 'image/png', 'size' => 30720, 'directory' => 'system-templates'], + ['collection_name' => 'templates', 'name' => 'Invoice Template', 'file_name' => 'invoice-template.png', 'mime_type' => 'image/png', 'size' => 40960, 'directory' => 'system-templates'], + ['collection_name' => 'documentation', 'name' => 'API Documentation', 'file_name' => 'api-documentation.pdf', 'mime_type' => 'application/pdf', 'size' => 512000, 'directory' => 'system-documentation'], + ['collection_name' => 'system', 'name' => 'Default Avatar', 'file_name' => 'default-avatar.png', 'mime_type' => 'image/png', 'size' => 20480, 'directory' => 'default-assets'], + ['collection_name' => 'system', 'name' => 'Loading Animation', 'file_name' => 'loading-animation.png', 'mime_type' => 'image/png', 'size' => 102400, 'directory' => 'default-assets'], + ['collection_name' => 'templates', 'name' => 'Report Template', 'file_name' => 'report-template.png', 'mime_type' => 'image/png', 'size' => 35840, 'directory' => 'system-templates'], + ['collection_name' => 'system', 'name' => 'Error Page Image', 'file_name' => 'error-page.png', 'mime_type' => 'image/png', 'size' => 81920, 'directory' => 'default-assets'], + ['collection_name' => 'system', 'name' => 'Success Icon', 'file_name' => 'success-icon.png', 'mime_type' => 'image/png', 'size' => 12288, 'directory' => 'default-assets'], + ['collection_name' => 'documentation', 'name' => 'Installation Guide', 'file_name' => 'installation-guide.pdf', 'mime_type' => 'application/pdf', 'size' => 256000, 'directory' => 'system-documentation'], + ['collection_name' => 'templates', 'name' => 'Certificate Template', 'file_name' => 'certificate-template.png', 'mime_type' => 'image/png', 'size' => 122880, 'directory' => 'system-templates'] + ]; + + foreach ($mediaItems as $mediaData) { + $directory = MediaDirectory::where('slug', $mediaData['directory'])->where('created_by', $superAdmin->id)->first(); + + if (!Media::where('file_name', $mediaData['file_name'])->where('created_by', $superAdmin->id)->exists()) { + $media = new Media(); + $media->model_type = User::class; + $media->model_id = $superAdmin->id; + $media->uuid = Str::uuid(); + $media->collection_name = $mediaData['collection_name']; + $media->name = $mediaData['name']; + $media->file_name = $mediaData['file_name']; + $media->mime_type = $mediaData['mime_type']; + $media->disk = 'public'; + $media->conversions_disk = 'public'; + $media->size = $mediaData['size']; + $media->manipulations = []; + $media->custom_properties = []; + $media->generated_conversions = []; + $media->responsive_images = []; + $media->order_column = 1; + $media->directory_id = $directory?->id; + $media->created_by = $superAdmin->id; + $media->saveQuietly(); + } + } + } + + /** + * Create media directories and items for Company + */ + private function createCompanyMedia($company) + { + // Create organized directories for Company with company name prefix + $companyName = str_replace(' ', '-', strtolower($company->name)); + $directories = [ + ['name' => $company->name . ' - Assets', 'slug' => $companyName . '-assets'], + ['name' => $company->name . ' - Documents', 'slug' => $companyName . '-documents'], + ['name' => $company->name . ' - Branding', 'slug' => $companyName . '-branding'] + ]; + + foreach ($directories as $dirData) { + if (!MediaDirectory::where('slug', $dirData['slug'])->where('created_by', $company->id)->exists()) { + MediaDirectory::create([ + 'name' => $dirData['name'], + 'slug' => $dirData['slug'], + 'parent_id' => null, + 'created_by' => $company->id, + ]); + } + } + + // Create organized media items for Company + $companyName = str_replace(' ', '-', strtolower($company->name)); + $mediaItems = [ + ['collection_name' => 'branding', 'name' => $company->name . ' Logo', 'file_name' => $companyName . '-logo.png', 'mime_type' => 'image/png', 'size' => 25600, 'directory' => $companyName . '-branding'], + ['collection_name' => 'documents', 'name' => $company->name . ' Employee Handbook', 'file_name' => $companyName . '-employee-handbook.pdf', 'mime_type' => 'application/pdf', 'size' => 512000, 'directory' => $companyName . '-documents'], + ['collection_name' => 'assets', 'name' => $company->name . ' Office Photo', 'file_name' => $companyName . '-office-photo.png', 'mime_type' => 'image/png', 'size' => 307200, 'directory' => $companyName . '-assets'], + ['collection_name' => 'branding', 'name' => $company->name . ' Business Card', 'file_name' => $companyName . '-business-card.png', 'mime_type' => 'image/png', 'size' => 40960, 'directory' => $companyName . '-branding'], + ['collection_name' => 'branding', 'name' => $company->name . ' Letterhead', 'file_name' => $companyName . '-letterhead.png', 'mime_type' => 'image/png', 'size' => 61440, 'directory' => $companyName . '-branding'], + ['collection_name' => 'documents', 'name' => $company->name . ' Policy Document', 'file_name' => $companyName . '-policy-document.pdf', 'mime_type' => 'application/pdf', 'size' => 256000, 'directory' => $companyName . '-documents'], + ['collection_name' => 'documents', 'name' => $company->name . ' Training Manual', 'file_name' => $companyName . '-training-manual.pdf', 'mime_type' => 'application/pdf', 'size' => 768000, 'directory' => $companyName . '-documents'], + ['collection_name' => 'assets', 'name' => $company->name . ' Team Photo', 'file_name' => $companyName . '-team-photo.png', 'mime_type' => 'image/png', 'size' => 409600, 'directory' => $companyName . '-assets'], + ['collection_name' => 'assets', 'name' => $company->name . ' Building Exterior', 'file_name' => $companyName . '-building-exterior.png', 'mime_type' => 'image/png', 'size' => 512000, 'directory' => $companyName . '-assets'], + ['collection_name' => 'branding', 'name' => $company->name . ' Social Media Banner', 'file_name' => $companyName . '-social-banner.png', 'mime_type' => 'image/png', 'size' => 153600, 'directory' => $companyName . '-branding'] + ]; + + foreach ($mediaItems as $mediaData) { + $directory = MediaDirectory::where('slug', $mediaData['directory'])->where('created_by', $company->id)->first(); + + if (!Media::where('file_name', $mediaData['file_name'])->where('created_by', $company->id)->exists()) { + Media::create([ + 'model_type' => User::class, + 'model_id' => $company->id, + 'uuid' => Str::uuid(), + 'collection_name' => $mediaData['collection_name'], + 'name' => $mediaData['name'], + 'file_name' => $mediaData['file_name'], + 'mime_type' => $mediaData['mime_type'], + 'disk' => 'public', + 'conversions_disk' => 'public', + 'size' => $mediaData['size'], + 'manipulations' => [], + 'custom_properties' => [], + 'generated_conversions' => [], + 'responsive_images' => [], + 'order_column' => 1, + 'directory_id' => $directory?->id, + 'created_by' => $company->id, + ]); + } + } + } +} diff --git a/database/seeders/MeetingAttendeeSeeder.php b/database/seeders/MeetingAttendeeSeeder.php new file mode 100644 index 000000000..fcf2db0a9 --- /dev/null +++ b/database/seeders/MeetingAttendeeSeeder.php @@ -0,0 +1,139 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed attendee patterns for consistent data + $attendeePatterns = [ + ['type' => 'Required', 'rsvp_status' => 'Accepted', 'attendance_status' => 'Present', 'decline_reason' => null], + ['type' => 'Required', 'rsvp_status' => 'Accepted', 'attendance_status' => 'Present', 'decline_reason' => null], + ['type' => 'Optional', 'rsvp_status' => 'Accepted', 'attendance_status' => 'Present', 'decline_reason' => null], + ['type' => 'Required', 'rsvp_status' => 'Declined', 'attendance_status' => 'Not Attended', 'decline_reason' => 'Conflicting meeting schedule'], + ['type' => 'Optional', 'rsvp_status' => 'Tentative', 'attendance_status' => 'Not Attended', 'decline_reason' => null], + ['type' => 'Required', 'rsvp_status' => 'Accepted', 'attendance_status' => 'Late', 'decline_reason' => null], + ['type' => 'Optional', 'rsvp_status' => 'Accepted', 'attendance_status' => 'Left Early', 'decline_reason' => null], + ['type' => 'Required', 'rsvp_status' => 'Pending', 'attendance_status' => 'Not Attended', 'decline_reason' => null] + ]; + + foreach ($companies as $company) { + // Get meetings for this company + $meetings = Meeting::where('created_by', $company->id)->get(); + + if ($meetings->isEmpty()) { + $this->command->warn('No meetings found for company: ' . $company->name . '. Please run MeetingSeeder first.'); + continue; + } + + // Get employees for this company + $employees = User::whereIn('type', ['manager', 'hr', 'employee']) + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name); + continue; + } + + foreach ($meetings as $meeting) { + // Determine number of attendees based on meeting type + $attendeeCount = $this->getAttendeeCount($meeting->type->name ?? 'Team Meeting'); + + // Select attendees (excluding organizer to avoid duplicates) + $availableEmployees = $employees->where('id', '!=', $meeting->organizer_id); + $selectedAttendees = $availableEmployees->take($attendeeCount); + + foreach ($selectedAttendees as $index => $attendee) { + // Check if attendee already exists for this meeting + if (MeetingAttendee::where('meeting_id', $meeting->id)->where('user_id', $attendee->id)->exists()) { + continue; + } + + $pattern = $attendeePatterns[$index % 8]; + + // Set RSVP date if status is not pending + $rsvpDate = $pattern['rsvp_status'] !== 'Pending' ? + date('Y-m-d H:i:s', strtotime($meeting->meeting_date . ' -1 day')) : null; + + // For completed meetings, set appropriate attendance status + if ($meeting->status === 'Completed') { + if ($pattern['rsvp_status'] === 'Accepted') { + $attendanceStatus = $pattern['attendance_status']; + } else { + $attendanceStatus = 'Not Attended'; + } + } else { + $attendanceStatus = 'Not Attended'; + } + + try { + MeetingAttendee::create([ + 'meeting_id' => $meeting->id, + 'user_id' => $attendee->id, + 'type' => $pattern['type'], + 'rsvp_status' => $pattern['rsvp_status'], + 'attendance_status' => $attendanceStatus, + 'rsvp_date' => $rsvpDate, + 'decline_reason' => $pattern['decline_reason'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create meeting attendee for meeting: ' . $meeting->title . ' and user: ' . $attendee->name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('MeetingAttendee seeder completed successfully!'); + } + + /** + * Get attendee count based on meeting type + */ + private function getAttendeeCount($meetingType) + { + switch ($meetingType) { + case 'Team Meeting': + return 5; + case 'One-on-One': + return 1; + case 'Client Meeting': + return 4; + case 'Board Meeting': + return 5; + case 'Training Session': + return 5; + case 'Interview': + return 2; + case 'Project Review': + return 5; + case 'All Hands': + return 5; + case 'Performance Review': + return 1; + case 'Brainstorming': + return 7; + default: + return 5; + } + } +} diff --git a/database/seeders/MeetingMinuteSeeder.php b/database/seeders/MeetingMinuteSeeder.php new file mode 100644 index 000000000..6f2b20f98 --- /dev/null +++ b/database/seeders/MeetingMinuteSeeder.php @@ -0,0 +1,117 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed meeting minutes templates by meeting type + $minuteTemplates = [ + 'Team Meeting' => [ + ['topic' => 'Sprint Progress Review', 'content' => 'Team reviewed current sprint progress. 80% of planned tasks completed. Discussed blockers and dependencies for remaining tasks.', 'type' => 'Discussion'], + ['topic' => 'Resource Allocation Decision', 'content' => 'Decided to allocate additional developer resources to Project Alpha. Sarah will join the team starting next Monday.', 'type' => 'Decision'], + ['topic' => 'Code Review Process', 'content' => 'Action item: Implement automated code review process using GitHub Actions. John to research and propose solution by Friday.', 'type' => 'Action Item'], + ['topic' => 'Team Building Event', 'content' => 'Note: Team building event scheduled for next month. HR will send details via email.', 'type' => 'Note'] + ], + 'Client Meeting' => [ + ['topic' => 'Project Scope Discussion', 'content' => 'Client requested additional features for the mobile app. Discussed timeline implications and budget adjustments required.', 'type' => 'Discussion'], + ['topic' => 'Delivery Timeline Approval', 'content' => 'Client approved the revised delivery timeline. New deadline set for March 15th with phased rollout approach.', 'type' => 'Decision'], + ['topic' => 'Weekly Status Reports', 'content' => 'Action item: Provide weekly status reports every Friday. Include progress metrics and risk assessments.', 'type' => 'Action Item'], + ['topic' => 'Next Meeting Schedule', 'content' => 'Note: Next client meeting scheduled for two weeks from today at 2 PM.', 'type' => 'Note'] + ], + 'Board Meeting' => [ + ['topic' => 'Financial Performance Review', 'content' => 'Q4 financial results exceeded expectations. Revenue increased by 15% compared to previous quarter. Cost optimization initiatives showing positive results.', 'type' => 'Discussion'], + ['topic' => 'Strategic Investment Approval', 'content' => 'Board approved $2M investment in new technology infrastructure. Implementation to begin in Q2.', 'type' => 'Decision'], + ['topic' => 'Market Expansion Plan', 'content' => 'Action item: Prepare detailed market expansion plan for European markets. Present findings at next board meeting.', 'type' => 'Action Item'], + ['topic' => 'Regulatory Compliance', 'content' => 'Note: New regulatory requirements effective from next quarter. Legal team monitoring developments.', 'type' => 'Note'] + ], + 'Training Session' => [ + ['topic' => 'Learning Objectives Review', 'content' => 'Reviewed training objectives and expected outcomes. Participants demonstrated good understanding of core concepts.', 'type' => 'Discussion'], + ['topic' => 'Certification Requirements', 'content' => 'Decided that all participants must complete assessment within 30 days to receive certification.', 'type' => 'Decision'], + ['topic' => 'Follow-up Training', 'content' => 'Action item: Schedule follow-up training session for advanced topics. Trainer to prepare curriculum by next week.', 'type' => 'Action Item'], + ['topic' => 'Training Materials', 'content' => 'Note: Training materials will be shared via company portal within 24 hours.', 'type' => 'Note'] + ], + 'Project Review' => [ + ['topic' => 'Milestone Achievement', 'content' => 'Project successfully completed Phase 2 milestones. All deliverables met quality standards and timeline requirements.', 'type' => 'Discussion'], + ['topic' => 'Budget Reallocation', 'content' => 'Approved budget reallocation from marketing to development to address technical challenges in Phase 3.', 'type' => 'Decision'], + ['topic' => 'Risk Mitigation Plan', 'content' => 'Action item: Develop comprehensive risk mitigation plan for identified technical risks. Present plan next week.', 'type' => 'Action Item'], + ['topic' => 'Stakeholder Communication', 'content' => 'Note: Regular stakeholder updates to be sent bi-weekly starting from next month.', 'type' => 'Note'] + ], + 'Performance Review' => [ + ['topic' => 'Goal Achievement Assessment', 'content' => 'Employee exceeded 90% of set objectives for the review period. Demonstrated strong performance in key areas.', 'type' => 'Discussion'], + ['topic' => 'Promotion Recommendation', 'content' => 'Decided to recommend employee for promotion to Senior level based on consistent performance and leadership qualities.', 'type' => 'Decision'], + ['topic' => 'Professional Development Plan', 'content' => 'Action item: Create personalized professional development plan focusing on leadership skills and technical expertise.', 'type' => 'Action Item'], + ['topic' => 'Next Review Schedule', 'content' => 'Note: Next performance review scheduled for six months from today.', 'type' => 'Note'] + ] + ]; + + foreach ($companies as $company) { + // Get completed meetings for this company + $meetings = Meeting::where('created_by', $company->id) + ->where('status', 'Completed') + ->get(); + + if ($meetings->isEmpty()) { + $this->command->warn('No completed meetings found for company: ' . $company->name . '. Please run MeetingSeeder first.'); + continue; + } + + // Get employees for recording minutes + $employees = User::whereIn('type', ['manager', 'hr', 'employee']) + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name); + continue; + } + + foreach ($meetings as $meeting) { + $meetingTypeName = $meeting->type->name ?? 'Team Meeting'; + $minutes = $minuteTemplates[$meetingTypeName] ?? $minuteTemplates['Team Meeting']; + + // Select recorder from first 5 employees + $selectedEmployees = $employees->take(5); + $recorder = $selectedEmployees->first(); + + $recordedAt = now()->subDays(1)->format('Y-m-d H:i:s'); + + foreach ($minutes as $index => $minute) { + try { + MeetingMinute::create([ + 'meeting_id' => $meeting->id, + 'topic' => $minute['topic'], + 'content' => $minute['content'], + 'type' => $minute['type'], + 'recorded_by' => $meeting->organizer_id, + 'recorded_at' => $recordedAt, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error($e->getMessage()); + continue; + } + } + } + } + + $this->command->info('MeetingMinute seeder completed successfully!'); + } +} diff --git a/database/seeders/MeetingRoomSeeder.php b/database/seeders/MeetingRoomSeeder.php new file mode 100644 index 000000000..82d9ceda4 --- /dev/null +++ b/database/seeders/MeetingRoomSeeder.php @@ -0,0 +1,156 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed meeting rooms for consistent data + $meetingRooms = [ + [ + 'name' => 'Conference Room A', + 'description' => 'Large conference room with presentation facilities and video conferencing setup', + 'type' => 'Physical', + 'location' => 'Ground Floor, East Wing', + 'capacity' => 20, + 'equipment' => ['Projector', 'Whiteboard', 'Video Conferencing', 'Audio System', 'WiFi'], + 'booking_url' => null, + 'status' => 'active' + ], + [ + 'name' => 'Meeting Room B', + 'description' => 'Medium-sized meeting room suitable for team meetings and discussions', + 'type' => 'Physical', + 'location' => 'First Floor, North Wing', + 'capacity' => 12, + 'equipment' => ['TV Display', 'Whiteboard', 'Conference Phone', 'WiFi'], + 'booking_url' => null, + 'status' => 'active' + ], + [ + 'name' => 'Boardroom', + 'description' => 'Executive boardroom for senior management meetings and client presentations', + 'type' => 'Physical', + 'location' => 'Second Floor, Executive Suite', + 'capacity' => 16, + 'equipment' => ['Smart Board', 'Video Conferencing', 'Premium Audio System', 'Climate Control', 'WiFi'], + 'booking_url' => null, + 'status' => 'active' + ], + [ + 'name' => 'Training Room', + 'description' => 'Spacious training room with flexible seating arrangement for workshops and seminars', + 'type' => 'Physical', + 'location' => 'Ground Floor, West Wing', + 'capacity' => 30, + 'equipment' => ['Projector', 'Sound System', 'Microphones', 'Flipcharts', 'WiFi'], + 'booking_url' => null, + 'status' => 'active' + ], + [ + 'name' => 'Interview Room 1', + 'description' => 'Small interview room for candidate interviews and one-on-one meetings', + 'type' => 'Physical', + 'location' => 'First Floor, HR Department', + 'capacity' => 4, + 'equipment' => ['Table', 'Chairs', 'WiFi'], + 'booking_url' => null, + 'status' => 'active' + ], + [ + 'name' => 'Interview Room 2', + 'description' => 'Comfortable interview room with video recording capability for assessments', + 'type' => 'Physical', + 'location' => 'First Floor, HR Department', + 'capacity' => 6, + 'equipment' => ['Video Recording', 'Audio System', 'Whiteboard', 'WiFi'], + 'booking_url' => null, + 'status' => 'active' + ], + [ + 'name' => 'Zoom Room 1', + 'description' => 'Virtual meeting room for remote team collaboration and client meetings', + 'type' => 'Virtual', + 'location' => 'Online', + 'capacity' => 100, + 'equipment' => ['Screen Sharing', 'Recording', 'Breakout Rooms', 'Chat'], + 'booking_url' => 'https://zoom.us/j/1234567890', + 'status' => 'active' + ], + [ + 'name' => 'Teams Room 1', + 'description' => 'Microsoft Teams virtual meeting space for internal meetings and presentations', + 'type' => 'Virtual', + 'location' => 'Online', + 'capacity' => 250, + 'equipment' => ['Screen Sharing', 'Recording', 'File Sharing', 'Whiteboard'], + 'booking_url' => 'https://teams.microsoft.com/l/meetup-join/meeting1', + 'status' => 'active' + ], + [ + 'name' => 'Huddle Space 1', + 'description' => 'Small informal meeting space for quick discussions and brainstorming sessions', + 'type' => 'Physical', + 'location' => 'First Floor, Open Area', + 'capacity' => 6, + 'equipment' => ['Comfortable Seating', 'Whiteboard', 'WiFi'], + 'booking_url' => null, + 'status' => 'active' + ], + [ + 'name' => 'Webinar Room', + 'description' => 'Virtual webinar room for large-scale presentations and company-wide meetings', + 'type' => 'Virtual', + 'location' => 'Online', + 'capacity' => 500, + 'equipment' => ['HD Video', 'Professional Audio', 'Recording', 'Q&A', 'Polls'], + 'booking_url' => 'https://webinar.company.com/room1', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($meetingRooms as $roomData) { + // Check if meeting room already exists for this company + if (MeetingRoom::where('name', $roomData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + MeetingRoom::create([ + 'name' => $roomData['name'], + 'description' => $roomData['description'], + 'type' => $roomData['type'], + 'location' => $roomData['location'], + 'capacity' => $roomData['capacity'], + 'equipment' => $roomData['equipment'], + 'booking_url' => $roomData['booking_url'], + 'status' => $roomData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create meeting room: ' . $roomData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('MeetingRoom seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/MeetingSeeder.php b/database/seeders/MeetingSeeder.php new file mode 100644 index 000000000..bcc2b7e3b --- /dev/null +++ b/database/seeders/MeetingSeeder.php @@ -0,0 +1,224 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed meeting data for consistent results + $meetingData = [ + [ + 'title' => 'Weekly Team Standup', + 'description' => 'Weekly team standup meeting to discuss progress, blockers, and upcoming tasks', + 'meeting_type' => 'Team Meeting', + 'room_name' => 'Conference Room A', + 'days_from_now' => 1, + 'start_time' => '09:00:00', + 'duration' => 60, + 'agenda' => 'Progress updates from team members, Discussion of current blockers, Planning for upcoming week, Q&A session', + 'status' => 'Completed', + 'recurrence' => 'Weekly', + 'recurrence_end_days' => 90 + ], + [ + 'title' => 'Client Presentation - Q4 Results', + 'description' => 'Quarterly business review presentation for key client stakeholders', + 'meeting_type' => 'Client Meeting', + 'room_name' => 'Boardroom', + 'days_from_now' => 3, + 'start_time' => '14:00:00', + 'duration' => 120, + 'agenda' => 'Q4 performance overview, Key achievements and milestones, Challenges and solutions, Q1 roadmap presentation', + 'status' => 'Completed', + 'recurrence' => 'None', + 'recurrence_end_days' => null + ], + [ + 'title' => 'Performance Review Session', + 'description' => 'Monthly performance review meetings with team members', + 'meeting_type' => 'Performance Review', + 'room_name' => 'Meeting Room B', + 'days_from_now' => 5, + 'start_time' => '11:00:00', + 'duration' => 60, + 'agenda' => 'Review of monthly objectives, Performance assessment, Career development discussion, Goal setting', + 'status' => 'Scheduled', + 'recurrence' => 'Monthly', + 'recurrence_end_days' => 180 + ], + [ + 'title' => 'Daily Scrum Meeting', + 'description' => 'Daily scrum meeting for agile development team', + 'meeting_type' => 'Team Meeting', + 'room_name' => 'Huddle Space 1', + 'days_from_now' => 0, + 'start_time' => '10:00:00', + 'duration' => 30, + 'agenda' => 'Yesterday progress, Today plans, Blockers discussion', + 'status' => 'Scheduled', + 'recurrence' => 'Daily', + 'recurrence_end_days' => 30 + ], + [ + 'title' => 'Project Alpha Review', + 'description' => 'Monthly review meeting for Project Alpha to assess progress and next steps', + 'meeting_type' => 'Project Review', + 'room_name' => 'Conference Room A', + 'days_from_now' => 7, + 'start_time' => '15:30:00', + 'duration' => 90, + 'agenda' => 'Project milestone review, Budget and timeline assessment, Risk analysis, Next phase planning', + 'status' => 'Scheduled', + 'recurrence' => 'Monthly', + 'recurrence_end_days' => 180 + ], + [ + 'title' => 'All Hands Company Meeting', + 'description' => 'Quarterly all-hands meeting for company updates and announcements', + 'meeting_type' => 'All Hands', + 'room_name' => 'Webinar Room', + 'days_from_now' => 10, + 'start_time' => '16:00:00', + 'duration' => 90, + 'agenda' => 'CEO address and company updates, Financial performance overview, New initiatives and projects, Employee recognition', + 'status' => 'Scheduled', + 'recurrence' => 'None', + 'recurrence_end_days' => null + ] + ]; + + foreach ($companies as $company) { + // Get meeting types for this company + $meetingTypes = MeetingType::where('created_by', $company->id)->get(); + + if ($meetingTypes->isEmpty()) { + $this->command->warn('No meeting types found for company: ' . $company->name . '. Please run MeetingTypeSeeder first.'); + continue; + } + + // Get meeting rooms for this company + $meetingRooms = MeetingRoom::where('created_by', $company->id)->get(); + + if ($meetingRooms->isEmpty()) { + $this->command->warn('No meeting rooms found for company: ' . $company->name . '. Please run MeetingRoomSeeder first.'); + continue; + } + + // Get employees for organizers + $employees = User::whereIn('type', ['manager', 'hr', 'employee']) + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name); + continue; + } + + foreach ($meetingData as $index => $meeting) { + // Find matching meeting type + $meetingType = $meetingTypes->where('name', $meeting['meeting_type'])->first(); + if (!$meetingType) $meetingType = $meetingTypes->first(); + + // Find matching meeting room + $meetingRoom = $meetingRooms->where('name', $meeting['room_name'])->first(); + if (!$meetingRoom) $meetingRoom = $meetingRooms->first(); + + // Select organizer from first 5 employees + $selectedEmployees = $employees->take(5); + $organizer = $selectedEmployees->skip($index % 5)->first() ?: $selectedEmployees->first(); + + $startDate = Carbon::now()->addDays($meeting['days_from_now']); + $endTime = Carbon::createFromFormat('H:i:s', $meeting['start_time'])->addMinutes($meeting['duration'])->format('H:i:s'); + + // Create recurring meetings + $this->createRecurringMeetings( + $meeting, + $meetingType, + $meetingRoom, + $organizer, + $company, + $startDate, + $endTime + ); + } + } + + $this->command->info('Meeting seeder completed successfully!'); + } + + /** + * Create recurring meetings based on recurrence pattern + */ + private function createRecurringMeetings($meeting, $meetingType, $meetingRoom, $organizer, $company, $startDate, $endTime) + { + $currentDate = $startDate->copy(); + $recurrenceEndDate = $meeting['recurrence_end_days'] ? + $startDate->copy()->addDays($meeting['recurrence_end_days']) : + $startDate->copy()->addDays(30); // Default 30 days if no end date + + + $meetingCount = 0; + $maxMeetings = 50; // Safety limit to prevent infinite loops + + while ($currentDate->lte($recurrenceEndDate) && $meetingCount < $maxMeetings) { + try { + Meeting::create([ + 'title' => $meeting['title'], + 'description' => $meeting['description'], + 'type_id' => $meetingType->id, + 'room_id' => $meetingRoom->id, + 'meeting_date' => $currentDate->format('Y-m-d'), + 'start_time' => $meeting['start_time'], + 'end_time' => $endTime, + 'duration' => $meeting['duration'], + 'agenda' => $meeting['agenda'], + 'status' => $currentDate->isPast() ? 'Completed' : $meeting['status'], + 'recurrence' => $meeting['recurrence'], + 'recurrence_end_date' => $meeting['recurrence_end_days'] ? $recurrenceEndDate->format('Y-m-d') : null, + 'organizer_id' => $organizer->id, + 'created_by' => $company->id, + ]); + + $meetingCount++; + } catch (\Exception $e) { + $this->command->error('Failed to create meeting: ' . $meeting['title'] . ' on ' . $currentDate->format('Y-m-d') . ' for company: ' . $company->name); + } + + // Calculate next occurrence based on recurrence pattern + switch ($meeting['recurrence']) { + case 'Daily': + $currentDate->addDay(); + break; + case 'Weekly': + $currentDate->addWeek(); + break; + case 'Monthly': + $currentDate->addMonth(); + break; + case 'None': + default: + // For non-recurring meetings, break after first iteration + break 2; + } + } + } +} diff --git a/database/seeders/MeetingTypeSeeder.php b/database/seeders/MeetingTypeSeeder.php new file mode 100644 index 000000000..67141f9cb --- /dev/null +++ b/database/seeders/MeetingTypeSeeder.php @@ -0,0 +1,123 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed meeting types for consistent data + $meetingTypes = [ + [ + 'name' => 'Team Meeting', + 'description' => 'Regular team meetings for project updates, discussions, and coordination', + 'color' => '#3B82F6', + 'default_duration' => 60, + 'status' => 'active' + ], + [ + 'name' => 'One-on-One', + 'description' => 'Individual meetings between manager and team member for performance discussions and feedback', + 'color' => '#10b77f', + 'default_duration' => 30, + 'status' => 'active' + ], + [ + 'name' => 'Client Meeting', + 'description' => 'Meetings with clients for project discussions, presentations, and business development', + 'color' => '#F59E0B', + 'default_duration' => 90, + 'status' => 'active' + ], + [ + 'name' => 'Board Meeting', + 'description' => 'Executive board meetings for strategic decisions and company governance', + 'color' => '#EF4444', + 'default_duration' => 120, + 'status' => 'active' + ], + [ + 'name' => 'Training Session', + 'description' => 'Training and development sessions for skill enhancement and knowledge sharing', + 'color' => '#8B5CF6', + 'default_duration' => 120, + 'status' => 'active' + ], + [ + 'name' => 'Interview', + 'description' => 'Job interviews for candidate evaluation and recruitment process', + 'color' => '#06B6D4', + 'default_duration' => 45, + 'status' => 'active' + ], + [ + 'name' => 'Project Review', + 'description' => 'Project milestone reviews, progress assessments, and deliverable evaluations', + 'color' => '#84CC16', + 'default_duration' => 90, + 'status' => 'active' + ], + [ + 'name' => 'All Hands', + 'description' => 'Company-wide meetings for announcements, updates, and organizational communication', + 'color' => '#F97316', + 'default_duration' => 60, + 'status' => 'active' + ], + [ + 'name' => 'Performance Review', + 'description' => 'Employee performance evaluation meetings and appraisal discussions', + 'color' => '#EC4899', + 'default_duration' => 60, + 'status' => 'active' + ], + [ + 'name' => 'Brainstorming', + 'description' => 'Creative sessions for idea generation, problem-solving, and innovation discussions', + 'color' => '#6366F1', + 'default_duration' => 90, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($meetingTypes as $typeData) { + // Check if meeting type already exists for this company + if (MeetingType::where('name', $typeData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + MeetingType::create([ + 'name' => $typeData['name'], + 'description' => $typeData['description'], + 'color' => $typeData['color'], + 'default_duration' => $typeData['default_duration'], + 'status' => $typeData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create meeting type: ' . $typeData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('MeetingType seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/NewsletterSeeder.php b/database/seeders/NewsletterSeeder.php new file mode 100644 index 000000000..84ab0d092 --- /dev/null +++ b/database/seeders/NewsletterSeeder.php @@ -0,0 +1,89 @@ +first(); + + if (!$superAdmin) { + $this->command->warn('No super admin user found. Please run UserSeeder first.'); + return; + } + + $this->createNewslettersForUser($superAdmin); + } else { + // For non-SaaS mode - create newsletters for companies + $companies = User::where('type', 'company')->get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + foreach ($companies as $company) { + $this->createNewslettersForUser($company); + } + } + + $this->command->info('NewsletterSeeder completed successfully!'); + } + + private function createNewslettersForUser($user) + { + // Sample newsletter subscription data + $newsletters = [ + ['email' => 'john.doe@example.com', 'status' => 'subscribed'], + ['email' => 'jane.smith@company.com', 'status' => 'subscribed'], + ['email' => 'michael.johnson@business.org', 'status' => 'subscribed'], + ['email' => 'sarah.williams@startup.io', 'status' => 'subscribed'], + ['email' => 'david.brown@enterprise.com', 'status' => 'subscribed'], + ['email' => 'lisa.davis@tech.com', 'status' => 'subscribed'], + ['email' => 'robert.miller@consulting.net', 'status' => 'subscribed'], + ['email' => 'jennifer.wilson@nonprofit.org', 'status' => 'subscribed'], + ['email' => 'mark.taylor@agency.com', 'status' => 'subscribed'], + ['email' => 'amanda.anderson@corporation.net', 'status' => 'subscribed'], + ['email' => 'chris.martinez@solutions.com', 'status' => 'subscribed'], + ['email' => 'emily.garcia@innovations.net', 'status' => 'subscribed'], + ['email' => 'daniel.rodriguez@systems.org', 'status' => 'subscribed'], + ['email' => 'michelle.lopez@services.com', 'status' => 'subscribed'], + ['email' => 'kevin.hernandez@digital.io', 'status' => 'subscribed'], + ['email' => 'rachel.gonzalez@creative.net', 'status' => 'subscribed'], + ['email' => 'brian.perez@marketing.com', 'status' => 'subscribed'], + ['email' => 'stephanie.sanchez@design.org', 'status' => 'subscribed'], + ['email' => 'jason.ramirez@development.com', 'status' => 'subscribed'], + ['email' => 'nicole.torres@management.net', 'status' => 'subscribed'] + ]; + + // Create all newsletters for each user + foreach ($newsletters as $newsletter) { + try { + NewsLetter::updateOrCreate( + [ + 'email' => $newsletter['email'] + ], + [ + 'status' => $newsletter['status'], + ] + ); + } catch (\Exception $e) { + $this->command->error('Failed to create/update newsletter: ' . $newsletter['email'] . ' for user: ' . $user->name); + continue; + } + } + } +} \ No newline at end of file diff --git a/database/seeders/NocTemplateSeeder.php b/database/seeders/NocTemplateSeeder.php new file mode 100644 index 000000000..baa41d152 --- /dev/null +++ b/database/seeders/NocTemplateSeeder.php @@ -0,0 +1,83 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $languages = [ + 'ar' => 'شهادة عدم ممانعة', + 'da' => 'Ingen indsigelse certifikat', + 'de' => 'Unbedenklichkeitsbescheinigung', + 'en' => 'No Objection Certificate', + 'es' => 'Certificado de No Objeción', + 'fr' => 'Certificat de Non-Objection', + 'he' => 'תעודת אי התנגדות', + 'it' => 'Certificato di Non Obiezione', + 'ja' => '異議なし証明書', + 'nl' => 'Geen Bezwaar Certificaat', + 'pl' => 'Certyfikat Braku Sprzeciwu', + 'pt' => 'Certificado de Não Objeção', + 'pt-BR' => 'Certificado de Não Objeção', + 'ru' => 'Справка об отсутствии возражений', + 'tr' => 'İtiraz Yok Belgesi', + 'zh' => '无异议证明' + ]; + + $templates = [ + 'ar' => '

شهادة عدم ممانعة

التاريخ: {date}

إلى من يهمه الأمر،

نشهد بأن {employee_name} يعمل حالياً لدى {company_name} بمنصب {designation}.

ليس لدينا أي اعتراض على الموظف المذكور أعلاه لأي أغراض رسمية.

مع خالص التقدير،
قسم الموارد البشرية
{company_name}

', + 'da' => '

Ingen indsigelse certifikat

Dato: {date}

Til hvem det måtte vedkomme,

Dette er for at bekræfte, at {employee_name} i øjeblikket er ansat hos {company_name} som {designation}.

Vi har ingen indvendinger mod ovennævnte medarbejder til officielle formål.

Med venlig hilsen,
HR-afdelingen
{company_name}

', + 'de' => '

Unbedenklichkeitsbescheinigung

Datum: {date}

An wen es betrifft,

Hiermit wird bestätigt, dass {employee_name} derzeit bei {company_name} als {designation} beschäftigt ist.

Wir haben keine Einwände gegen den oben genannten Mitarbeiter für offizielle Zwecke.

Mit freundlichen Grüßen,
Personalabteilung
{company_name}

', + 'en' => '

No Objection Certificate

Date: {date}

To Whom It May Concern,

This is to certify that {employee_name} is currently employed with {company_name} as {designation}.

We have no objection to the above mentioned employee for any official purposes.

Sincerely,
HR Department
{company_name}

', + 'es' => '

Certificado de No Objeción

Fecha: {date}

A quien corresponda,

Por la presente certificamos que {employee_name} está actualmente empleado en {company_name} como {designation}.

No tenemos objeción alguna al empleado mencionado anteriormente para cualquier propósito oficial.

Atentamente,
Departamento de RRHH
{company_name}

', + 'fr' => '

Certificat de Non-Objection

Date: {date}

À qui de droit,

Ceci certifie que {employee_name} est actuellement employé chez {company_name} en tant que {designation}.

Nous n\'avons aucune objection concernant l\'employé mentionné ci-dessus à des fins officielles.

Cordialement,
Département RH
{company_name}

', + 'he' => '

תעודת אי התנגדות

תאריך: {date}

למי שזה נוגע,

זאת להעיד כי {employee_name} מועסק כעת ב-{company_name} בתפקיד {designation}.

אין לנו התנגדות לעובד הנ"ל לכל מטרה רשמית.

בכבוד רב,
מחלקת משאבי אנוש
{company_name}

', + 'it' => '

Certificato di Non Obiezione

Data: {date}

A chi di competenza,

Si certifica che {employee_name} è attualmente impiegato presso {company_name} come {designation}.

Non abbiamo obiezioni riguardo al suddetto dipendente per scopi ufficiali.

Cordiali saluti,
Dipartimento HR
{company_name}

', + 'ja' => '

異議なし証明書

日付: {date}

関係者各位

{employee_name}が現在{company_name}で{designation}として雇用されていることを証明いたします。

上記従業員に関して、公的な目的での異議はございません。

敬具
人事部
{company_name}

', + 'nl' => '

Geen Bezwaar Certificaat

Datum: {date}

Aan wie het betreft,

Hierbij wordt bevestigd dat {employee_name} momenteel werkzaam is bij {company_name} als {designation}.

Wij hebben geen bezwaar tegen bovengenoemde werknemer voor officiële doeleinden.

Met vriendelijke groet,
HR Afdeling
{company_name}

', + 'pl' => '

Certyfikat Braku Sprzeciwu

Data: {date}

Do kogo to dotyczy,

Niniejszym poświadczamy, że {employee_name} jest obecnie zatrudniony w {company_name} na stanowisku {designation}.

Nie mamy sprzeciwu wobec wyżej wymienionego pracownika w celach urzędowych.

Z poważaniem,
Dział HR
{company_name}

', + 'pt' => '

Certificado de Não Objeção

Data: {date}

A quem possa interessar,

Certificamos que {employee_name} está atualmente empregado na {company_name} como {designation}.

Não temos objeção ao funcionário mencionado acima para fins oficiais.

Atenciosamente,
Departamento de RH
{company_name}

', + 'pt-BR' => '

Certificado de Não Objeção

Data: {date}

A quem possa interessar,

Certificamos que {employee_name} está atualmente empregado na {company_name} como {designation}.

Não temos objeção ao funcionário mencionado acima para fins oficiais.

Atenciosamente,
Departamento de RH
{company_name}

', + 'ru' => '

Справка об отсутствии возражений

Дата: {date}

Кого это касается,

Настоящим подтверждаем, что {employee_name} в настоящее время работает в {company_name} в должности {designation}.

У нас нет возражений против вышеупомянутого сотрудника для официальных целей.

С уважением,
Отдел кадров
{company_name}

', + 'tr' => '

İtiraz Yok Belgesi

Tarih: {date}

İlgili Makama,

{employee_name} adlı kişinin {company_name} şirketinde {designation} pozisyonunda çalıştığını onaylarız.

Yukarıda belirtilen çalışanımız için resmi amaçlar doğrultusunda herhangi bir itirazımız bulunmamaktadır.

Saygılarımızla,
İnsan Kaynakları Departmanı
{company_name}

', + 'zh' => '

无异议证明

日期:{date}

致相关人员:

兹证明{employee_name}目前在{company_name}担任{designation}职位。

我们对上述员工用于官方目的无任何异议。

此致
人力资源部
{company_name}

' + ]; + + $variables = json_encode(['date', 'company_name', 'employee_name', 'designation']); + + foreach ($companies as $company) { + foreach ($languages as $code => $title) { + try { + NocTemplate::updateOrCreate( + [ + 'language' => $code, + 'created_by' => $company->id + ], + [ + 'content' => $templates[$code] ?? $templates['en'], + 'variables' => $variables + ] + ); + } catch (\Exception $e) { + $this->command->error('Failed to create NOC template for language: ' . $code . ' and company: ' . $company->name); + continue; + } + } + } + + $this->command->info('NocTemplate seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/OfferSeeder.php b/database/seeders/OfferSeeder.php new file mode 100644 index 000000000..6982fc130 --- /dev/null +++ b/database/seeders/OfferSeeder.php @@ -0,0 +1,105 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed offer data - Half Draft, Half Sent + $offerTemplates = [ + ['status' => 'Draft', 'days_ago' => 0, 'expiry_days' => 10, 'start_days' => 21], + ['status' => 'Sent', 'days_ago' => 2, 'expiry_days' => 10, 'start_days' => 21], + ['status' => 'Draft', 'days_ago' => 0, 'expiry_days' => 14, 'start_days' => 28], + ['status' => 'Sent', 'days_ago' => 3, 'expiry_days' => 14, 'start_days' => 28], + ['status' => 'Draft', 'days_ago' => 0, 'expiry_days' => 7, 'start_days' => 14], + ['status' => 'Sent', 'days_ago' => 1, 'expiry_days' => 7, 'start_days' => 14], + ['status' => 'Draft', 'days_ago' => 0, 'expiry_days' => 10, 'start_days' => 21], + ['status' => 'Sent', 'days_ago' => 2, 'expiry_days' => 10, 'start_days' => 21] + ]; + + foreach ($companies as $company) { + // Get candidates with Offer status for this company + $offerCandidates = Candidate::where('created_by', $company->id) + ->where('status', 'Offer') + ->get(); + + if ($offerCandidates->isEmpty()) { + $this->command->warn('No offer candidates found for company: ' . $company->name . '. Please run CandidateSeeder first.'); + continue; + } + + // Get managers/HR for approval + $approvers = User::whereIn('type', ['manager', 'hr']) + ->where('created_by', $company->id) + ->get(); + + // Create one offer per candidate + foreach ($offerCandidates as $index => $candidate) { + // Check if offer already exists for this candidate in this company + if (Offer::where('candidate_id', $candidate->id)->where('created_by', $company->id)->exists()) { + continue; + } + + $offerTemplate = $offerTemplates[$index % count($offerTemplates)]; + + // Get job posting to extract position and department + $jobPosting = $candidate->job; + if (!$jobPosting) { + continue; + } + + // Select approver + $approver = $approvers->isNotEmpty() ? $approvers->first() : null; + + $offerDate = date('Y-m-d', strtotime('-' . $offerTemplate['days_ago'] . ' days')); + $expirationDate = date('Y-m-d', strtotime($offerDate . ' +' . $offerTemplate['expiry_days'] . ' days')); + $startDate = date('Y-m-d', strtotime($offerDate . ' +' . $offerTemplate['start_days'] . ' days')); + + try { + Offer::create([ + 'candidate_id' => $candidate->id, + 'job_id' => $candidate->job_id, + 'offer_date' => $offerDate, + 'position' => $candidate->job_id, + 'department_id' => $jobPosting->department_id, + 'salary' => $candidate->expected_salary ?? 50000, + 'bonus' => null, + 'equity' => null, + 'benefits' => $jobPosting->benefits ?? null, + 'start_date' => $startDate, + 'expiration_date' => $expirationDate, + 'offer_letter_path' => 'offers/' . $candidate->id . '_offer_letter.pdf', + 'status' => $offerTemplate['status'], + 'response_date' => null, + 'decline_reason' => null, + 'created_by' => $company->id, + 'approved_by' => $offerTemplate['status'] === 'Sent' ? $approver?->id : null, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create offer for candidate: ' . $candidate->first_name . ' ' . $candidate->last_name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Offer seeder completed successfully!'); + } +} diff --git a/database/seeders/OfferTemplateSeeder.php b/database/seeders/OfferTemplateSeeder.php new file mode 100644 index 000000000..94191f592 --- /dev/null +++ b/database/seeders/OfferTemplateSeeder.php @@ -0,0 +1,374 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed offer templates for consistent data + $offerTemplates = [ + [ + 'name' => 'Standard Full-Time Offer', + 'template_content' => 'Dear {{candidate_name}}, + +We are pleased to extend an offer of employment for the position of {{job_title}} at {{company_name}}. We believe your skills and experience will be a valuable addition to our team. + +Position Details: +- Job Title: {{job_title}} +- Department: {{department}} +- Start Date: {{start_date}} +- Reporting Manager: {{manager_name}} + +Compensation Package: +- Annual Salary: {{salary}} +- Performance Bonus: {{bonus}} +- Benefits: {{benefits}} + +Terms and Conditions: +- Employment Type: Full-time +- Probation Period: {{probation_period}} +- Notice Period: {{notice_period}} +- Working Hours: {{working_hours}} + +This offer is contingent upon successful completion of background verification and reference checks. Please confirm your acceptance by {{offer_expiry_date}}. + +We look forward to welcoming you to our team. + +Best regards, +{{hr_manager_name}} +Human Resources +{{company_name}}', + 'variables' => [ + 'candidate_name', + 'job_title', + 'company_name', + 'department', + 'start_date', + 'manager_name', + 'salary', + 'bonus', + 'benefits', + 'probation_period', + 'notice_period', + 'working_hours', + 'offer_expiry_date', + 'hr_manager_name' + ], + 'status' => 'active' + ], + [ + 'name' => 'Contract Position Offer', + 'template_content' => 'Dear {{candidate_name}}, + +We are pleased to offer you a contract position for {{job_title}} at {{company_name}}. + +Contract Details: +- Position: {{job_title}} +- Contract Duration: {{contract_duration}} +- Start Date: {{start_date}} +- End Date: {{end_date}} +- Location: {{work_location}} + +Compensation: +- Contract Rate: {{contract_rate}} +- Payment Terms: {{payment_terms}} +- Additional Benefits: {{contract_benefits}} + +Terms: +- Contract Type: {{contract_type}} +- Renewal Options: {{renewal_terms}} +- Termination Clause: {{termination_clause}} + +Please review the attached contract agreement and confirm your acceptance by {{acceptance_deadline}}. + +Best regards, +{{hr_contact_name}} +{{company_name}}', + 'variables' => [ + 'candidate_name', + 'job_title', + 'company_name', + 'contract_duration', + 'start_date', + 'end_date', + 'work_location', + 'contract_rate', + 'payment_terms', + 'contract_benefits', + 'contract_type', + 'renewal_terms', + 'termination_clause', + 'acceptance_deadline', + 'hr_contact_name' + ], + 'status' => 'active' + ], + [ + 'name' => 'Senior Management Offer', + 'template_content' => 'Dear {{candidate_name}}, + +On behalf of {{company_name}}, I am delighted to extend an offer for the position of {{job_title}}. + +Executive Position Details: +- Title: {{job_title}} +- Division: {{division}} +- Reporting: {{reporting_structure}} +- Start Date: {{start_date}} + +Executive Compensation Package: +- Base Salary: {{base_salary}} +- Variable Compensation: {{variable_pay}} +- Equity/Stock Options: {{equity_details}} +- Executive Benefits: {{executive_benefits}} +- Car Allowance: {{car_allowance}} +- Club Membership: {{club_membership}} + +Additional Terms: +- Employment Agreement: {{employment_agreement}} +- Confidentiality: {{confidentiality_terms}} +- Non-Compete: {{non_compete_clause}} + +This offer reflects our confidence in your ability to contribute significantly to our organization\'s success. + +Please confirm your acceptance by {{response_deadline}}. + +Sincerely, +{{ceo_name}} +Chief Executive Officer +{{company_name}}', + 'variables' => [ + 'candidate_name', + 'company_name', + 'job_title', + 'division', + 'reporting_structure', + 'start_date', + 'base_salary', + 'variable_pay', + 'equity_details', + 'executive_benefits', + 'car_allowance', + 'club_membership', + 'employment_agreement', + 'confidentiality_terms', + 'non_compete_clause', + 'response_deadline', + 'ceo_name' + ], + 'status' => 'active' + ], + [ + 'name' => 'Internship Offer', + 'template_content' => 'Dear {{candidate_name}}, + +Congratulations! We are pleased to offer you an internship position at {{company_name}}. + +Internship Details: +- Position: {{internship_title}} +- Department: {{department}} +- Duration: {{internship_duration}} +- Start Date: {{start_date}} +- End Date: {{end_date}} +- Supervisor: {{supervisor_name}} + +Internship Benefits: +- Monthly Stipend: {{stipend_amount}} +- Learning Opportunities: {{learning_programs}} +- Mentorship: {{mentorship_details}} +- Certificate: {{certificate_details}} + +Expectations: +- Working Hours: {{working_hours}} +- Project Assignments: {{project_details}} +- Performance Evaluation: {{evaluation_process}} + +We are excited to have you join our team and contribute to your professional development. + +Please confirm your acceptance by {{confirmation_date}}. + +Best regards, +{{internship_coordinator}} +{{company_name}}', + 'variables' => [ + 'candidate_name', + 'company_name', + 'internship_title', + 'department', + 'internship_duration', + 'start_date', + 'end_date', + 'supervisor_name', + 'stipend_amount', + 'learning_programs', + 'mentorship_details', + 'certificate_details', + 'working_hours', + 'project_details', + 'evaluation_process', + 'confirmation_date', + 'internship_coordinator' + ], + 'status' => 'active' + ], + [ + 'name' => 'Remote Work Offer', + 'template_content' => 'Dear {{candidate_name}}, + +We are excited to offer you the remote position of {{job_title}} at {{company_name}}. + +Remote Work Details: +- Position: {{job_title}} +- Work Model: {{work_model}} +- Time Zone: {{time_zone}} +- Start Date: {{start_date}} +- Team: {{team_name}} + +Compensation & Benefits: +- Annual Salary: {{annual_salary}} +- Remote Work Allowance: {{remote_allowance}} +- Equipment Provided: {{equipment_list}} +- Health Benefits: {{health_benefits}} +- Vacation Days: {{vacation_days}} + +Remote Work Policies: +- Communication Tools: {{communication_tools}} +- Meeting Schedule: {{meeting_schedule}} +- Performance Tracking: {{performance_metrics}} +- Office Visits: {{office_visit_requirements}} + +Technology Setup: +- Laptop/Desktop: {{computer_specs}} +- Software Licenses: {{software_provided}} +- Internet Reimbursement: {{internet_allowance}} + +Please confirm your acceptance and provide your shipping address for equipment delivery by {{acceptance_date}}. + +Welcome to the team! + +{{hiring_manager_name}} +{{company_name}}', + 'variables' => [ + 'candidate_name', + 'job_title', + 'company_name', + 'work_model', + 'time_zone', + 'start_date', + 'team_name', + 'annual_salary', + 'remote_allowance', + 'equipment_list', + 'health_benefits', + 'vacation_days', + 'communication_tools', + 'meeting_schedule', + 'performance_metrics', + 'office_visit_requirements', + 'computer_specs', + 'software_provided', + 'internet_allowance', + 'acceptance_date', + 'hiring_manager_name' + ], + 'status' => 'active' + ], + [ + 'name' => 'Part-Time Position Offer', + 'template_content' => 'Dear {{candidate_name}}, + +We are pleased to offer you a part-time position as {{job_title}} at {{company_name}}. + +Part-Time Position Details: +- Job Title: {{job_title}} +- Department: {{department}} +- Working Hours: {{part_time_hours}} +- Schedule: {{work_schedule}} +- Start Date: {{start_date}} + +Compensation: +- Hourly Rate: {{hourly_rate}} +- Monthly Salary: {{monthly_salary}} +- Pro-rated Benefits: {{prorated_benefits}} + +Work Arrangement: +- Flexible Hours: {{flexible_hours}} +- Core Hours: {{core_hours}} +- Remote Work: {{remote_options}} + +Benefits (Pro-rated): +- Health Insurance: {{health_coverage}} +- Paid Time Off: {{pto_days}} +- Professional Development: {{training_budget}} + +This position offers excellent work-life balance while contributing meaningfully to our organization. + +Please respond by {{response_date}}. + +Best regards, +{{hr_representative}} +{{company_name}}', + 'variables' => [ + 'candidate_name', + 'job_title', + 'company_name', + 'department', + 'part_time_hours', + 'work_schedule', + 'start_date', + 'hourly_rate', + 'monthly_salary', + 'prorated_benefits', + 'flexible_hours', + 'core_hours', + 'remote_options', + 'health_coverage', + 'pto_days', + 'training_budget', + 'response_date', + 'hr_representative' + ], + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($offerTemplates as $templateData) { + // Check if offer template already exists for this company + if (OfferTemplate::where('name', $templateData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + OfferTemplate::create([ + 'name' => $templateData['name'], + 'template_content' => $templateData['template_content'], + 'variables' => $templateData['variables'], + 'status' => $templateData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create offer template: ' . $templateData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('OfferTemplate seeder completed successfully!'); + } +} diff --git a/database/seeders/OnboardingChecklistSeeder.php b/database/seeders/OnboardingChecklistSeeder.php new file mode 100644 index 000000000..20c6e4a38 --- /dev/null +++ b/database/seeders/OnboardingChecklistSeeder.php @@ -0,0 +1,100 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed onboarding checklists for consistent data + $onboardingChecklists = [ + [ + 'name' => 'Standard Employee Onboarding', + 'description' => 'Comprehensive onboarding checklist for all new employees covering documentation, orientation, and initial setup requirements', + 'is_default' => true, + 'status' => 'active' + ], + [ + 'name' => 'Technical Team Onboarding', + 'description' => 'Specialized onboarding process for technical roles including development environment setup, code access, and technical training', + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Management Level Onboarding', + 'description' => 'Executive and management onboarding checklist including leadership orientation, strategic briefings, and team introductions', + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Sales Team Onboarding', + 'description' => 'Sales-focused onboarding covering CRM training, product knowledge, sales processes, and territory assignments', + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Remote Employee Onboarding', + 'description' => 'Onboarding checklist specifically designed for remote workers including equipment setup, virtual introductions, and remote work policies', + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Intern Onboarding Program', + 'description' => 'Streamlined onboarding process for interns and temporary employees with essential orientation and basic requirements', + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Customer Service Onboarding', + 'description' => 'Customer service team onboarding including product training, customer interaction protocols, and support system access', + 'is_default' => false, + 'status' => 'active' + ], + [ + 'name' => 'Finance Department Onboarding', + 'description' => 'Finance team specific onboarding covering financial systems, compliance requirements, and accounting procedures', + 'is_default' => false, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($onboardingChecklists as $checklistData) { + // Check if onboarding checklist already exists for this company + if (OnboardingChecklist::where('name', $checklistData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + OnboardingChecklist::create([ + 'name' => $checklistData['name'], + 'description' => $checklistData['description'], + 'is_default' => $checklistData['is_default'], + 'status' => $checklistData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create onboarding checklist: ' . $checklistData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('OnboardingChecklist seeder completed successfully!'); + } +} diff --git a/database/seeders/PayoutRequestSeeder.php b/database/seeders/PayoutRequestSeeder.php new file mode 100644 index 000000000..b13e385ed --- /dev/null +++ b/database/seeders/PayoutRequestSeeder.php @@ -0,0 +1,55 @@ +take(4)->get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please seed users first.'); + return; + } + + $payoutRequests = [ + [ + 'company_id' => $companies->first()->id, + 'amount' => 250.00, + 'status' => 'pending', + 'notes' => 'Monthly commission payout request' + ], + [ + 'company_id' => $companies->skip(1)->first()->id, + 'amount' => 150.75, + 'status' => 'approved', + 'notes' => 'Referral commission for Q1' + ], + [ + 'company_id' => $companies->skip(2)->first()->id, + 'amount' => 500.00, + 'status' => 'rejected', + 'notes' => 'Insufficient commission balance' + ], + [ + 'company_id' => $companies->last()->id, + 'amount' => 75.50, + 'status' => 'pending', + 'notes' => 'Weekly payout request' + ] + ]; + + foreach ($payoutRequests as $requestData) { + PayoutRequest::create($requestData); + } + + $this->command->info('Payout requests seeded successfully!'); + } + } +} diff --git a/database/seeders/PayrollRunSeeder.php b/database/seeders/PayrollRunSeeder.php new file mode 100644 index 000000000..f7a2a3fdb --- /dev/null +++ b/database/seeders/PayrollRunSeeder.php @@ -0,0 +1,71 @@ +get(); + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + $currentYear = date('Y'); + + foreach ($companies as $company) { + + // Create payroll runs for May, June, July, August 2025 + for ($month = 4; $month <= 8; $month++) { + $this->createPayrollRun($company, $currentYear, $month); + } + } + + $this->command->info('PayrollRun seeder completed successfully!'); + } + + /** + * Create payroll run for specific month + */ + private function createPayrollRun($company, $year, $month) + { + $startDate = Carbon::create($year, $month, 1); + $endDate = $startDate->copy()->endOfMonth(); + $payDate = $endDate->copy()->addDays(5); // Pay 5 days after month end + + $monthName = $startDate->format('F'); + $title = "{$monthName} {$year} Payroll"; + + // Check if payroll run already exists + if (PayrollRun::where('title', $title)->where('created_by', $company->id)->exists()) { + return; + } + + try { + PayrollRun::create([ + 'title' => $title, + 'payroll_frequency' => 'monthly', + 'pay_period_start' => $startDate->format('Y-m-d'), + 'pay_period_end' => $endDate->format('Y-m-d'), + 'pay_date' => $payDate->format('Y-m-d'), + 'total_gross_pay' => 0.00, + 'total_deductions' => 0.00, + 'total_net_pay' => 0.00, + 'status' => 'draft', + 'notes' => "Monthly payroll for {$monthName} {$year}", + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create payroll run: ' . $title . ' for company: ' . $company->name); + } + } +} diff --git a/database/seeders/PayslipSeeder.php b/database/seeders/PayslipSeeder.php new file mode 100644 index 000000000..128f23d29 --- /dev/null +++ b/database/seeders/PayslipSeeder.php @@ -0,0 +1,45 @@ +where('status', 'completed'); + })->get(); + + foreach ($payrollEntries as $entry) { + // Check if payslip already exists + $exists = Payslip::where('payroll_entry_id', $entry->id)->exists(); + + if (!$exists) { + $payslipNumber = Payslip::generatePayslipNumber( + $entry->employee_id, + $entry->payrollRun->pay_date + ); + + Payslip::create([ + 'payroll_entry_id' => $entry->id, + 'employee_id' => $entry->employee_id, + 'payslip_number' => $payslipNumber, + 'pay_period_start' => $entry->payrollRun->pay_period_start, + 'pay_period_end' => $entry->payrollRun->pay_period_end, + 'pay_date' => $entry->payrollRun->pay_date, + 'status' => 'generated', + 'created_by' => $entry->created_by, + ]); + } + } + } +} \ No newline at end of file diff --git a/database/seeders/PerformanceIndicatorCategorySeeder.php b/database/seeders/PerformanceIndicatorCategorySeeder.php new file mode 100644 index 000000000..a5ff2cdb1 --- /dev/null +++ b/database/seeders/PerformanceIndicatorCategorySeeder.php @@ -0,0 +1,111 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed performance indicator categories for consistent data + $categories = [ + [ + 'name' => 'Job Performance', + 'description' => 'Measures employee effectiveness in completing assigned tasks, meeting deadlines, and achieving job-specific objectives', + 'status' => 'active' + ], + [ + 'name' => 'Quality of Work', + 'description' => 'Evaluates accuracy, attention to detail, and overall quality standards in work output and deliverables', + 'status' => 'active' + ], + [ + 'name' => 'Communication Skills', + 'description' => 'Assesses verbal and written communication abilities, listening skills, and effectiveness in conveying information', + 'status' => 'active' + ], + [ + 'name' => 'Teamwork and Collaboration', + 'description' => 'Measures ability to work effectively with colleagues, contribute to team goals, and maintain positive working relationships', + 'status' => 'active' + ], + [ + 'name' => 'Leadership and Initiative', + 'description' => 'Evaluates leadership qualities, proactive approach, decision-making skills, and ability to guide and motivate others', + 'status' => 'active' + ], + [ + 'name' => 'Problem Solving', + 'description' => 'Assesses analytical thinking, creativity in finding solutions, and ability to resolve challenges effectively', + 'status' => 'active' + ], + [ + 'name' => 'Adaptability and Flexibility', + 'description' => 'Measures ability to adapt to changes, learn new skills, and handle varying work demands and environments', + 'status' => 'active' + ], + [ + 'name' => 'Time Management', + 'description' => 'Evaluates efficiency in managing time, prioritizing tasks, and meeting deadlines consistently', + 'status' => 'active' + ], + [ + 'name' => 'Customer Service', + 'description' => 'Measures effectiveness in serving customers, handling inquiries, and maintaining positive customer relationships', + 'status' => 'active' + ], + [ + 'name' => 'Technical Competency', + 'description' => 'Assesses proficiency in job-related technical skills, software applications, and industry-specific knowledge', + 'status' => 'active' + ], + [ + 'name' => 'Professional Development', + 'description' => 'Evaluates commitment to continuous learning, skill enhancement, and career growth initiatives', + 'status' => 'active' + ], + [ + 'name' => 'Attendance and Punctuality', + 'description' => 'Measures reliability in attendance, punctuality, and adherence to work schedules and policies', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($categories as $categoryData) { + // Check if category already exists for this company + if (PerformanceIndicatorCategory::where('name', $categoryData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + PerformanceIndicatorCategory::create([ + 'name' => $categoryData['name'], + 'description' => $categoryData['description'], + 'status' => $categoryData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create performance indicator category: ' . $categoryData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('PerformanceIndicatorCategory seeder completed successfully!'); + } +} diff --git a/database/seeders/PerformanceIndicatorSeeder.php b/database/seeders/PerformanceIndicatorSeeder.php new file mode 100644 index 000000000..ece6d8d1c --- /dev/null +++ b/database/seeders/PerformanceIndicatorSeeder.php @@ -0,0 +1,127 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed performance indicators by category for consistent data + $indicatorsByCategory = [ + 'Job Performance' => [ + ['name' => 'Task Completion Rate', 'description' => 'Percentage of assigned tasks completed within specified timeframes', 'unit' => 'Percentage', 'target' => '95%'], + ['name' => 'Goal Achievement', 'description' => 'Success rate in achieving individual and team objectives', 'unit' => 'Percentage', 'target' => '90%'], + ['name' => 'Productivity Level', 'description' => 'Output efficiency compared to established benchmarks', 'unit' => 'Rating', 'target' => '4/5'] + ], + 'Quality of Work' => [ + ['name' => 'Error Rate', 'description' => 'Frequency of mistakes or defects in work output', 'unit' => 'Percentage', 'target' => '<5%'], + ['name' => 'Accuracy Level', 'description' => 'Precision and correctness in completing tasks and deliverables', 'unit' => 'Percentage', 'target' => '98%'], + ['name' => 'Attention to Detail', 'description' => 'Thoroughness and care in reviewing and completing work', 'unit' => 'Rating', 'target' => '4/5'] + ], + 'Communication Skills' => [ + ['name' => 'Verbal Communication', 'description' => 'Effectiveness in spoken communication and presentations', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Written Communication', 'description' => 'Clarity and professionalism in written correspondence', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Active Listening', 'description' => 'Ability to understand and respond appropriately to others', 'unit' => 'Rating', 'target' => '4/5'] + ], + 'Teamwork and Collaboration' => [ + ['name' => 'Team Contribution', 'description' => 'Active participation and valuable input in team activities', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Cooperation Level', 'description' => 'Willingness to work with others and support team goals', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Conflict Resolution', 'description' => 'Ability to handle disagreements constructively', 'unit' => 'Rating', 'target' => '3/5'] + ], + 'Leadership and Initiative' => [ + ['name' => 'Decision Making', 'description' => 'Quality and timeliness of decisions made independently', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Initiative Taking', 'description' => 'Proactive approach to identifying and addressing opportunities', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Mentoring Ability', 'description' => 'Effectiveness in guiding and developing junior team members', 'unit' => 'Rating', 'target' => '3/5'] + ], + 'Problem Solving' => [ + ['name' => 'Analytical Thinking', 'description' => 'Ability to break down complex problems and analyze components', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Creative Solutions', 'description' => 'Innovation and creativity in developing problem solutions', 'unit' => 'Rating', 'target' => '3/5'], + ['name' => 'Resolution Speed', 'description' => 'Efficiency in identifying and implementing solutions', 'unit' => 'Rating', 'target' => '4/5'] + ], + 'Adaptability and Flexibility' => [ + ['name' => 'Change Adaptation', 'description' => 'Ability to adjust to new processes, systems, or requirements', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Learning Agility', 'description' => 'Speed and effectiveness in acquiring new skills and knowledge', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Stress Management', 'description' => 'Ability to maintain performance under pressure', 'unit' => 'Rating', 'target' => '3/5'] + ], + 'Time Management' => [ + ['name' => 'Deadline Adherence', 'description' => 'Consistency in meeting project and task deadlines', 'unit' => 'Percentage', 'target' => '95%'], + ['name' => 'Priority Management', 'description' => 'Effectiveness in organizing and prioritizing multiple tasks', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Efficiency Rating', 'description' => 'Optimal use of time and resources to achieve objectives', 'unit' => 'Rating', 'target' => '4/5'] + ], + 'Customer Service' => [ + ['name' => 'Customer Satisfaction', 'description' => 'Level of customer satisfaction with service provided', 'unit' => 'Rating', 'target' => '4.5/5'], + ['name' => 'Response Time', 'description' => 'Speed in responding to customer inquiries and requests', 'unit' => 'Hours', 'target' => '<24'], + ['name' => 'Issue Resolution', 'description' => 'Effectiveness in resolving customer problems and complaints', 'unit' => 'Percentage', 'target' => '90%'] + ], + 'Technical Competency' => [ + ['name' => 'Skill Proficiency', 'description' => 'Level of expertise in job-related technical skills', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Tool Utilization', 'description' => 'Effective use of software, systems, and technical tools', 'unit' => 'Rating', 'target' => '4/5'], + ['name' => 'Knowledge Application', 'description' => 'Practical application of technical knowledge to solve problems', 'unit' => 'Rating', 'target' => '4/5'] + ], + 'Professional Development' => [ + ['name' => 'Training Completion', 'description' => 'Participation and completion rate in professional development programs', 'unit' => 'Percentage', 'target' => '100%'], + ['name' => 'Skill Enhancement', 'description' => 'Progress in developing new competencies and improving existing skills', 'unit' => 'Rating', 'target' => '3/5'], + ['name' => 'Certification Achievement', 'description' => 'Success in obtaining relevant professional certifications', 'unit' => 'Count', 'target' => '2/year'] + ], + 'Attendance and Punctuality' => [ + ['name' => 'Attendance Rate', 'description' => 'Percentage of scheduled work days attended', 'unit' => 'Percentage', 'target' => '98%'], + ['name' => 'Punctuality Score', 'description' => 'Consistency in arriving on time for work and meetings', 'unit' => 'Percentage', 'target' => '95%'], + ['name' => 'Leave Management', 'description' => 'Appropriate use of leave policies and advance notification', 'unit' => 'Rating', 'target' => '4/5'] + ] + ]; + + foreach ($companies as $company) { + // Get performance indicator categories for this company + $categories = PerformanceIndicatorCategory::where('created_by', $company->id)->get(); + + if ($categories->isEmpty()) { + $this->command->warn('No performance indicator categories found for company: ' . $company->name . '. Please run PerformanceIndicatorCategorySeeder first.'); + continue; + } + + foreach ($categories as $category) { + $categoryIndicators = $indicatorsByCategory[$category->name] ?? []; + + foreach ($categoryIndicators as $indicatorData) { + // Check if indicator already exists for this category + if (PerformanceIndicator::where('name', $indicatorData['name'])->where('category_id', $category->id)->exists()) { + continue; + } + + try { + PerformanceIndicator::create([ + 'category_id' => $category->id, + 'name' => $indicatorData['name'], + 'description' => $indicatorData['description'], + 'measurement_unit' => $indicatorData['unit'], + 'target_value' => $indicatorData['target'], + 'status' => 'active', + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create performance indicator: ' . $indicatorData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('PerformanceIndicator seeder completed successfully!'); + } +} diff --git a/database/seeders/PermissionSeeder.php b/database/seeders/PermissionSeeder.php new file mode 100644 index 000000000..6fbea330f --- /dev/null +++ b/database/seeders/PermissionSeeder.php @@ -0,0 +1,965 @@ + 'manage-dashboard', 'module' => 'dashboard', 'label' => 'Manage Dashboard', 'description' => 'Can view dashboard'], + + // User management + ['name' => 'manage-users', 'module' => 'users', 'label' => 'Manage Users', 'description' => 'Can manage users'], + ['name' => 'manage-any-users', 'module' => 'users', 'label' => 'Manage All Users', 'description' => 'Manage Any Users'], + ['name' => 'manage-own-users', 'module' => 'users', 'label' => 'Manage Own Users', 'description' => 'Manage Limited Users that is created by own'], + ['name' => 'view-users', 'module' => 'users', 'label' => 'Manage Users', 'description' => 'View Users'], + ['name' => 'create-users', 'module' => 'users', 'label' => 'Create Users', 'description' => 'Can create users'], + ['name' => 'edit-users', 'module' => 'users', 'label' => 'Edit Users', 'description' => 'Can edit users'], + ['name' => 'delete-users', 'module' => 'users', 'label' => 'Delete Users', 'description' => 'Can delete users'], + ['name' => 'reset-password-users', 'module' => 'users', 'label' => 'Reset Password Users', 'description' => 'Can reset password users'], + ['name' => 'toggle-status-users', 'module' => 'users', 'label' => 'Change Status Users', 'description' => 'Can change status users'], + + // Role management + ['name' => 'manage-roles', 'module' => 'roles', 'label' => 'Manage Roles', 'description' => 'Can manage roles'], + ['name' => 'manage-any-roles', 'module' => 'roles', 'label' => 'Manage All Roles', 'description' => 'Manage Any Roles'], + ['name' => 'manage-own-roles', 'module' => 'roles', 'label' => 'Manage Own Roles', 'description' => 'Manage Limited Roles that is created by own'], + ['name' => 'view-roles', 'module' => 'roles', 'label' => 'View Roles', 'description' => 'View Roles'], + ['name' => 'create-roles', 'module' => 'roles', 'label' => 'Create Roles', 'description' => 'Can create roles'], + ['name' => 'edit-roles', 'module' => 'roles', 'label' => 'Edit Roles', 'description' => 'Can edit roles'], + ['name' => 'delete-roles', 'module' => 'roles', 'label' => 'Delete Roles', 'description' => 'Can delete roles'], + + // Permission management + ['name' => 'manage-permissions', 'module' => 'permissions', 'label' => 'Manage Permissions', 'description' => 'Can manage permissions'], + ['name' => 'manage-any-permissions', 'module' => 'permissions', 'label' => 'Manage All Permissions', 'description' => 'Manage Any Permissions'], + ['name' => 'manage-own-permissions', 'module' => 'permissions', 'label' => 'Manage Own Permissions', 'description' => 'Manage Limited Permissions that is created by own'], + ['name' => 'view-permissions', 'module' => 'permissions', 'label' => 'View Permissions', 'description' => 'View Permissions'], + ['name' => 'create-permissions', 'module' => 'permissions', 'label' => 'Create Permissions', 'description' => 'Can create permissions'], + ['name' => 'edit-permissions', 'module' => 'permissions', 'label' => 'Edit Permissions', 'description' => 'Can edit permissions'], + ['name' => 'delete-permissions', 'module' => 'permissions', 'label' => 'Delete Permissions', 'description' => 'Can delete permissions'], + + // Company management + ['name' => 'manage-companies', 'module' => 'companies', 'label' => 'Manage Companies', 'description' => 'Can manage Companies'], + ['name' => 'manage-any-companies', 'module' => 'companies', 'label' => 'Manage All Companies', 'description' => 'Manage Any Companies'], + ['name' => 'manage-own-companies', 'module' => 'companies', 'label' => 'Manage Own Companies', 'description' => 'Manage Limited Companies that is created by own'], + ['name' => 'view-companies', 'module' => 'companies', 'label' => 'View Companies', 'description' => 'View Companies'], + ['name' => 'create-companies', 'module' => 'companies', 'label' => 'Create Companies', 'description' => 'Can create Companies'], + ['name' => 'edit-companies', 'module' => 'companies', 'label' => 'Edit Companies', 'description' => 'Can edit Companies'], + ['name' => 'delete-companies', 'module' => 'companies', 'label' => 'Delete Companies', 'description' => 'Can delete Companies'], + ['name' => 'reset-password-companies', 'module' => 'companies', 'label' => 'Reset Password Companies', 'description' => 'Can reset password Companies'], + ['name' => 'toggle-status-companies', 'module' => 'companies', 'label' => 'Change Status Companies', 'description' => 'Can change status companies'], + ['name' => 'manage-plans-companies', 'module' => 'companies', 'label' => 'Manage Plan Companies', 'description' => 'Can manage plans companies'], + ['name' => 'upgrade-plan-companies', 'module' => 'companies', 'label' => 'Upgrade Plan Companies', 'description' => 'Can upgrade plan of companies'], + + // Plan management + ['name' => 'manage-plans', 'module' => 'plans', 'label' => 'Manage Plans', 'description' => 'Can manage subscription plans'], + ['name' => 'manage-any-plans', 'module' => 'plans', 'label' => 'Manage All Plans', 'description' => 'Manage Any Plans'], + ['name' => 'manage-own-plans', 'module' => 'plans', 'label' => 'Manage Own Plans', 'description' => 'Manage Limited Plans that is created by own'], + ['name' => 'view-plans', 'module' => 'plans', 'label' => 'View Plans', 'description' => 'View Plans'], + ['name' => 'create-plans', 'module' => 'plans', 'label' => 'Create Plans', 'description' => 'Can create subscription plans'], + ['name' => 'edit-plans', 'module' => 'plans', 'label' => 'Edit Plans', 'description' => 'Can edit subscription plans'], + ['name' => 'delete-plans', 'module' => 'plans', 'label' => 'Delete Plans', 'description' => 'Can delete subscription plans'], + ['name' => 'request-plans', 'module' => 'plans', 'label' => 'Request Plans', 'description' => 'Can request subscription plans'], + ['name' => 'trial-plans', 'module' => 'plans', 'label' => 'Trial Plans', 'description' => 'Can start trial for subscription plans'], + ['name' => 'subscribe-plans', 'module' => 'plans', 'label' => 'Subscribe Plans', 'description' => 'Can subscribe to subscription plans'], + + + // Coupon management + ['name' => 'manage-coupons', 'module' => 'coupons', 'label' => 'Manage Coupons', 'description' => 'Can manage subscription Coupons'], + ['name' => 'manage-any-coupons', 'module' => 'coupons', 'label' => 'Manage All Coupons', 'description' => 'Manage Any Coupons'], + ['name' => 'manage-own-coupons', 'module' => 'coupons', 'label' => 'Manage Own Coupons', 'description' => 'Manage Limited Coupons that is created by own'], + ['name' => 'view-coupons', 'module' => 'coupons', 'label' => 'View Coupons', 'description' => 'View Coupons'], + ['name' => 'create-coupons', 'module' => 'coupons', 'label' => 'Create Coupons', 'description' => 'Can create subscription Coupons'], + ['name' => 'edit-coupons', 'module' => 'coupons', 'label' => 'Edit Coupons', 'description' => 'Can edit subscription Coupons'], + ['name' => 'delete-coupons', 'module' => 'coupons', 'label' => 'Delete Coupons', 'description' => 'Can delete subscription Coupons'], + ['name' => 'toggle-status-coupons', 'module' => 'coupons', 'label' => 'Change Status Coupons', 'description' => 'Can change status Coupons'], + + // Plan Requests management + ['name' => 'manage-plan-requests', 'module' => 'plan_requests', 'label' => 'Manage Plan Requests', 'description' => 'Can manage plan requests'], + ['name' => 'view-plan-requests', 'module' => 'plan_requests', 'label' => 'View Plan Requests', 'description' => 'View Plan Requests'], + ['name' => 'create-plan-requests', 'module' => 'plan_requests', 'label' => 'Create Plan Requests', 'description' => 'Can create plan requests'], + ['name' => 'edit-plan-requests', 'module' => 'plan_requests', 'label' => 'Edit Plan Requests', 'description' => 'Can edit plan requests'], + ['name' => 'delete-plan-requests', 'module' => 'plan_requests', 'label' => 'Delete Plan Requests', 'description' => 'Can delete plan requests'], + ['name' => 'approve-plan-requests', 'module' => 'plan_requests', 'label' => 'Approve plan requests', 'description' => 'Can approve plan requests'], + ['name' => 'reject-plan-requests', 'module' => 'plan_requests', 'label' => 'Reject plan requests', 'description' => 'Can reject plplan requests'], + + // Plan Orders management + ['name' => 'manage-plan-orders', 'module' => 'plan_orders', 'label' => 'Manage Plan Orders', 'description' => 'Can manage plan orders'], + ['name' => 'view-plan-orders', 'module' => 'plan_orders', 'label' => 'View Plan Orders', 'description' => 'View Plan Orders'], + ['name' => 'create-plan-orders', 'module' => 'plan_orders', 'label' => 'Create Plan Orders', 'description' => 'Can create plan orders'], + ['name' => 'edit-plan-orders', 'module' => 'plan_orders', 'label' => 'Edit Plan Orders', 'description' => 'Can edit plan orders'], + ['name' => 'delete-plan-orders', 'module' => 'plan_orders', 'label' => 'Delete Plan Orders', 'description' => 'Can delete plan orders'], + ['name' => 'approve-plan-orders', 'module' => 'plan_orders', 'label' => 'Approve Plan Orders', 'description' => 'Can approve plan orders'], + ['name' => 'reject-plan-orders', 'module' => 'plan_orders', 'label' => 'Reject Plan Orders', 'description' => 'Can reject plan orders'], + + + // Settings + ['name' => 'manage-settings', 'module' => 'settings', 'label' => 'Manage Settings', 'description' => 'Can manage All settings'], + ['name' => 'manage-system-settings', 'module' => 'settings', 'label' => 'Manage System Settings', 'description' => 'Can manage system settings'], + ['name' => 'manage-email-settings', 'module' => 'settings', 'label' => 'Manage Email Settings', 'description' => 'Can manage email settings'], + ['name' => 'manage-brand-settings', 'module' => 'settings', 'label' => 'Manage Brand Settings', 'description' => 'Can manage brand settings'], + ['name' => 'manage-company-settings', 'module' => 'settings', 'label' => 'Manage Company Settings', 'description' => 'Can manage Company settings'], + ['name' => 'manage-storage-settings', 'module' => 'settings', 'label' => 'Manage Storage Settings', 'description' => 'Can manage storage settings'], + ['name' => 'manage-payment-settings', 'module' => 'settings', 'label' => 'Manage Payment Settings', 'description' => 'Can manage payment settings'], + ['name' => 'manage-currency-settings', 'module' => 'settings', 'label' => 'Manage Currency Settings', 'description' => 'Can manage currency settings'], + ['name' => 'manage-recaptcha-settings', 'module' => 'settings', 'label' => 'Manage ReCaptch Settings', 'description' => 'Can manage recaptcha settings'], + ['name' => 'manage-chatgpt-settings', 'module' => 'settings', 'label' => 'Manage ChatGpt Settings', 'description' => 'Can manage chatgpt settings'], + ['name' => 'manage-cookie-settings', 'module' => 'settings', 'label' => 'Manage Cookie(GDPR) Settings', 'description' => 'Can manage cookie settings'], + ['name' => 'manage-seo-settings', 'module' => 'settings', 'label' => 'Manage Seo Settings', 'description' => 'Can manage seo settings'], + ['name' => 'manage-cache-settings', 'module' => 'settings', 'label' => 'Manage Cache Settings', 'description' => 'Can manage cache settings'], + ['name' => 'manage-account-settings', 'module' => 'settings', 'label' => 'Manage Account Settings', 'description' => 'Can manage account settings'], + + + // Currency management + ['name' => 'manage-currencies', 'module' => 'currencies', 'label' => 'Manage Currencies', 'description' => 'Can manage currencies'], + ['name' => 'manage-any-currencies', 'module' => 'currencies', 'label' => 'Manage All currencies', 'description' => 'Manage Any currencies'], + ['name' => 'manage-own-currencies', 'module' => 'currencies', 'label' => 'Manage Own currencies', 'description' => 'Manage Limited currencies that is created by own'], + ['name' => 'view-currencies', 'module' => 'currencies', 'label' => 'View Currencies', 'description' => 'View Currencies'], + ['name' => 'create-currencies', 'module' => 'currencies', 'label' => 'Create Currencies', 'description' => 'Can create currencies'], + ['name' => 'edit-currencies', 'module' => 'currencies', 'label' => 'Edit Currencies', 'description' => 'Can edit currencies'], + ['name' => 'delete-currencies', 'module' => 'currencies', 'label' => 'Delete Currencies', 'description' => 'Can delete currencies'], + + + + // Referral management + ['name' => 'manage-referral', 'module' => 'referral', 'label' => 'Manage Referral', 'description' => 'Can manage referral program'], + ['name' => 'manage-users-referral', 'module' => 'referral', 'label' => 'Manage User Referral', 'description' => 'Can manage user referral program'], + ['name' => 'manage-setting-referral', 'module' => 'referral', 'label' => 'Manage Referral Setting', 'description' => 'Can manage Referral Setting'], + ['name' => 'manage-payout-referral', 'module' => 'referral', 'label' => 'Manage Referral Payout', 'description' => 'Can manage Referral Payout program'], + ['name' => 'approve-payout-referral', 'module' => 'referral', 'label' => 'Manage Referral', 'description' => 'Can approve payout request'], + ['name' => 'reject-payout-referral', 'module' => 'referral', 'label' => 'Manage Referral', 'description' => 'Can approve payout request'], + + // Language management + ['name' => 'manage-language', 'module' => 'language', 'label' => 'Manage Language', 'description' => 'Can manage language'], + ['name' => 'edit-language', 'module' => 'language', 'label' => 'Edit Language', 'description' => 'Edit Language'], + ['name' => 'view-language', 'module' => 'language', 'label' => 'View Language', 'description' => 'View Language'], + + // Media management + ['name' => 'manage-media', 'module' => 'media', 'label' => 'Manage Media', 'description' => 'Can manage media'], + ['name' => 'manage-any-media', 'module' => 'media', 'label' => 'Manage All Media', 'description' => 'Manage Any media'], + ['name' => 'manage-own-media', 'module' => 'media', 'label' => 'Manage Own Media', 'description' => 'Manage Limited media that is created by own'], + ['name' => 'create-media', 'module' => 'media', 'label' => 'Create media', 'description' => 'Create media'], + ['name' => 'edit-media', 'module' => 'media', 'label' => 'Edit media', 'description' => 'Edit media'], + ['name' => 'delete-media', 'module' => 'media', 'label' => 'Delete media', 'description' => 'Delete media'], + ['name' => 'view-media', 'module' => 'media', 'label' => 'View media', 'description' => 'View media'], + ['name' => 'download-media', 'module' => 'media', 'label' => 'Download media', 'description' => 'Download media'], + + // Media Directory management + + ['name' => 'manage-media-directories', 'module' => 'media_directories', 'label' => 'Manage Media Directory', 'description' => 'Can manage media directories'], + ['name' => 'manage-any-media-directories', 'module' => 'media_directories', 'label' => 'Manage All Media Directory', 'description' => 'Manage any media directories'], + ['name' => 'manage-own-media-directories', 'module' => 'media_directories', 'label' => 'Manage Own Media Directory', 'description' => 'Manage only media directories created by self'], + ['name' => 'create-media-directories', 'module' => 'media_directories', 'label' => 'Create Media Directory', 'description' => 'Create new media directories'], + ['name' => 'edit-media-directories', 'module' => 'media_directories', 'label' => 'Edit Media Directory', 'description' => 'Edit existing media directories'], + ['name' => 'delete-media-directories', 'module' => 'media_directories', 'label' => 'Delete Media Directory', 'description' => 'Delete media directories files'], + + + + // Webhook management + ['name' => 'manage-webhook-settings', 'module' => 'settings', 'label' => 'Manage Webhook Settings', 'description' => 'Can manage webhook settings'], + // Landing Page management + ['name' => 'manage-landing-page', 'module' => 'landing_page', 'label' => 'Manage Landing Page', 'description' => 'Can manage landing page'], + ['name' => 'view-landing-page', 'module' => 'landing_page', 'label' => 'View Landing Page', 'description' => 'View landing page'], + ['name' => 'edit-landing-page', 'module' => 'landing_page', 'label' => 'Edit Landing Page', 'description' => 'Edit landing page'], + + // Branch management + ['name' => 'manage-branches', 'module' => 'branches', 'label' => 'Manage Branches', 'description' => 'Can manage branches'], + ['name' => 'manage-any-branches', 'module' => 'branches', 'label' => 'Manage All Branches', 'description' => 'Manage Any Branches'], + ['name' => 'manage-own-branches', 'module' => 'branches', 'label' => 'Manage Own Branches', 'description' => 'Manage Limited Branches that is created by own'], + ['name' => 'view-branches', 'module' => 'branches', 'label' => 'View Branches', 'description' => 'View Branches'], + ['name' => 'create-branches', 'module' => 'branches', 'label' => 'Create Branches', 'description' => 'Can create branches'], + ['name' => 'edit-branches', 'module' => 'branches', 'label' => 'Edit Branches', 'description' => 'Can edit branches'], + ['name' => 'delete-branches', 'module' => 'branches', 'label' => 'Delete Branches', 'description' => 'Can delete branches'], + ['name' => 'toggle-status-branches', 'module' => 'branches', 'label' => 'Toggle Status Branches', 'description' => 'Can toggle status of branches'], + + // Department management + ['name' => 'manage-departments', 'module' => 'departments', 'label' => 'Manage Departments', 'description' => 'Can manage departments'], + ['name' => 'manage-any-departments', 'module' => 'departments', 'label' => 'Manage All Departments', 'description' => 'Manage Any Departments'], + ['name' => 'manage-own-departments', 'module' => 'departments', 'label' => 'Manage Own Departments', 'description' => 'Manage Limited Departments that is created by own'], + ['name' => 'view-departments', 'module' => 'departments', 'label' => 'View Departments', 'description' => 'View Departments'], + ['name' => 'create-departments', 'module' => 'departments', 'label' => 'Create Departments', 'description' => 'Can create departments'], + ['name' => 'edit-departments', 'module' => 'departments', 'label' => 'Edit Departments', 'description' => 'Can edit departments'], + ['name' => 'delete-departments', 'module' => 'departments', 'label' => 'Delete Departments', 'description' => 'Can delete departments'], + ['name' => 'toggle-status-departments', 'module' => 'departments', 'label' => 'Toggle Status Departments', 'description' => 'Can toggle status of departments'], + + // Designation management + ['name' => 'manage-designations', 'module' => 'designations', 'label' => 'Manage Designations', 'description' => 'Can manage designations'], + ['name' => 'manage-any-designations', 'module' => 'designations', 'label' => 'Manage All Designations', 'description' => 'Manage Any Designations'], + ['name' => 'manage-own-designations', 'module' => 'designations', 'label' => 'Manage Own Designations', 'description' => 'Manage Limited Designations that is created by own'], + ['name' => 'view-designations', 'module' => 'designations', 'label' => 'View Designations', 'description' => 'View Designations'], + ['name' => 'create-designations', 'module' => 'designations', 'label' => 'Create Designations', 'description' => 'Can create designations'], + ['name' => 'edit-designations', 'module' => 'designations', 'label' => 'Edit Designations', 'description' => 'Can edit designations'], + ['name' => 'delete-designations', 'module' => 'designations', 'label' => 'Delete Designations', 'description' => 'Can delete designations'], + ['name' => 'toggle-status-designations', 'module' => 'designations', 'label' => 'Toggle Status Designations', 'description' => 'Can toggle status of designations'], + + // Document Type management + ['name' => 'manage-document-types', 'module' => 'document_types', 'label' => 'Manage Document Types', 'description' => 'Can manage document types'], + ['name' => 'manage-any-document-types', 'module' => 'document_types', 'label' => 'Manage All Document Types', 'description' => 'Manage Any Document Types'], + ['name' => 'manage-own-document-types', 'module' => 'document_types', 'label' => 'Manage Own Document Types', 'description' => 'Manage Limited Document Types that is created by own'], + ['name' => 'view-document-types', 'module' => 'document_types', 'label' => 'View Document Types', 'description' => 'View Document Types'], + ['name' => 'create-document-types', 'module' => 'document_types', 'label' => 'Create Document Types', 'description' => 'Can create document types'], + ['name' => 'edit-document-types', 'module' => 'document_types', 'label' => 'Edit Document Types', 'description' => 'Can edit document types'], + ['name' => 'delete-document-types', 'module' => 'document_types', 'label' => 'Delete Document Types', 'description' => 'Can delete document types'], + + // Employee management + ['name' => 'manage-employees', 'module' => 'employees', 'label' => 'Manage Employees', 'description' => 'Can manage employees'], + ['name' => 'manage-any-employees', 'module' => 'employees', 'label' => 'Manage All Employees', 'description' => 'Manage Any Employees'], + ['name' => 'manage-own-employees', 'module' => 'employees', 'label' => 'Manage Own Employees', 'description' => 'Manage Limited Employees that is created by own'], + ['name' => 'view-employees', 'module' => 'employees', 'label' => 'View Employees', 'description' => 'View Employees'], + ['name' => 'create-employees', 'module' => 'employees', 'label' => 'Create Employees', 'description' => 'Can create employees'], + ['name' => 'edit-employees', 'module' => 'employees', 'label' => 'Edit Employees', 'description' => 'Can edit employees'], + ['name' => 'delete-employees', 'module' => 'employees', 'label' => 'Delete Employees', 'description' => 'Can delete employees'], + ['name' => 'download-joining-letter', 'module' => 'employees', 'label' => 'Download Joining Letter', 'description' => 'Can download joining letter'], + ['name' => 'download-experience-certificate', 'module' => 'employees', 'label' => 'Download Experience Certificate', 'description' => 'Can download experience certificate'], + ['name' => 'download-noc-certificate', 'module' => 'employees', 'label' => 'Download NOC Certificate', 'description' => 'Can download NOC certificate'], + ['name' => 'import-employee', 'module' => 'employees', 'label' => 'Import Employees', 'description' => 'Can Import Employees'], + ['name' => 'export-employee', 'module' => 'employees', 'label' => 'Export Employees', 'description' => 'Can Export Employees'], + + // Award Type management + ['name' => 'manage-award-types', 'module' => 'award_types', 'label' => 'Manage Award Types', 'description' => 'Can manage award types'], + ['name' => 'manage-any-award-types', 'module' => 'award_types', 'label' => 'Manage All Award Types', 'description' => 'Manage Any Award Types'], + ['name' => 'manage-own-award-types', 'module' => 'award_types', 'label' => 'Manage Own Award Types', 'description' => 'Manage Limited Award Types that is created by own'], + ['name' => 'view-award-types', 'module' => 'award_types', 'label' => 'View Award Types', 'description' => 'View Award Types'], + ['name' => 'create-award-types', 'module' => 'award_types', 'label' => 'Create Award Types', 'description' => 'Can create award types'], + ['name' => 'edit-award-types', 'module' => 'award_types', 'label' => 'Edit Award Types', 'description' => 'Can edit award types'], + ['name' => 'delete-award-types', 'module' => 'award_types', 'label' => 'Delete Award Types', 'description' => 'Can delete award types'], + + // Award management + ['name' => 'manage-awards', 'module' => 'awards', 'label' => 'Manage Awards', 'description' => 'Can manage awards'], + ['name' => 'manage-any-awards', 'module' => 'awards', 'label' => 'Manage All Awards', 'description' => 'Manage Any Awards'], + ['name' => 'manage-own-awards', 'module' => 'awards', 'label' => 'Manage Own Awards', 'description' => 'Manage Limited Awards that is created by own'], + ['name' => 'view-awards', 'module' => 'awards', 'label' => 'View Awards', 'description' => 'View Awards'], + ['name' => 'create-awards', 'module' => 'awards', 'label' => 'Create Awards', 'description' => 'Can create awards'], + ['name' => 'edit-awards', 'module' => 'awards', 'label' => 'Edit Awards', 'description' => 'Can edit awards'], + ['name' => 'delete-awards', 'module' => 'awards', 'label' => 'Delete Awards', 'description' => 'Can delete awards'], + + // Promotion management + ['name' => 'manage-promotions', 'module' => 'promotions', 'label' => 'Manage Promotions', 'description' => 'Can manage promotions'], + ['name' => 'manage-any-promotions', 'module' => 'promotions', 'label' => 'Manage All Promotions', 'description' => 'Manage Any Promotions'], + ['name' => 'manage-own-promotions', 'module' => 'promotions', 'label' => 'Manage Own Promotions', 'description' => 'Manage Limited Promotions that is created by own'], + ['name' => 'view-promotions', 'module' => 'promotions', 'label' => 'View Promotions', 'description' => 'View Promotions'], + ['name' => 'create-promotions', 'module' => 'promotions', 'label' => 'Create Promotions', 'description' => 'Can create promotions'], + ['name' => 'edit-promotions', 'module' => 'promotions', 'label' => 'Edit Promotions', 'description' => 'Can edit promotions'], + ['name' => 'delete-promotions', 'module' => 'promotions', 'label' => 'Delete Promotions', 'description' => 'Can delete promotions'], + ['name' => 'approve-promotions', 'module' => 'promotions', 'label' => 'Approve Promotions', 'description' => 'Can approve promotions'], + ['name' => 'reject-promotions', 'module' => 'promotions', 'label' => 'Reject Promotions', 'description' => 'Can reject promotions'], + + // Resignation management + ['name' => 'manage-resignations', 'module' => 'resignations', 'label' => 'Manage Resignations', 'description' => 'Can manage resignations'], + ['name' => 'manage-any-resignations', 'module' => 'resignations', 'label' => 'Manage All Resignations', 'description' => 'Manage Any Resignations'], + ['name' => 'manage-own-resignations', 'module' => 'resignations', 'label' => 'Manage Own Resignations', 'description' => 'Manage Limited Resignations that is created by own'], + ['name' => 'view-resignations', 'module' => 'resignations', 'label' => 'View Resignations', 'description' => 'View Resignations'], + ['name' => 'create-resignations', 'module' => 'resignations', 'label' => 'Create Resignations', 'description' => 'Can create resignations'], + ['name' => 'edit-resignations', 'module' => 'resignations', 'label' => 'Edit Resignations', 'description' => 'Can edit resignations'], + ['name' => 'delete-resignations', 'module' => 'resignations', 'label' => 'Delete Resignations', 'description' => 'Can delete resignations'], + ['name' => 'approve-resignations', 'module' => 'resignations', 'label' => 'Approve Resignations', 'description' => 'Can approve resignations'], + ['name' => 'reject-resignations', 'module' => 'resignations', 'label' => 'Reject Resignations', 'description' => 'Can reject resignations'], + + // Termination management + ['name' => 'manage-terminations', 'module' => 'terminations', 'label' => 'Manage Terminations', 'description' => 'Can manage terminations'], + ['name' => 'manage-any-terminations', 'module' => 'terminations', 'label' => 'Manage All Terminations', 'description' => 'Manage Any Terminations'], + ['name' => 'manage-own-terminations', 'module' => 'terminations', 'label' => 'Manage Own Terminations', 'description' => 'Manage Limited Terminations that is created by own'], + ['name' => 'view-terminations', 'module' => 'terminations', 'label' => 'View Terminations', 'description' => 'View Terminations'], + ['name' => 'create-terminations', 'module' => 'terminations', 'label' => 'Create Terminations', 'description' => 'Can create terminations'], + ['name' => 'edit-terminations', 'module' => 'terminations', 'label' => 'Edit Terminations', 'description' => 'Can edit terminations'], + ['name' => 'delete-terminations', 'module' => 'terminations', 'label' => 'Delete Terminations', 'description' => 'Can delete terminations'], + ['name' => 'approve-terminations', 'module' => 'terminations', 'label' => 'Approve Terminations', 'description' => 'Can approve terminations'], + ['name' => 'reject-terminations', 'module' => 'terminations', 'label' => 'Reject Terminations', 'description' => 'Can reject terminations'], + + // Warning management + ['name' => 'manage-warnings', 'module' => 'warnings', 'label' => 'Manage Warnings', 'description' => 'Can manage warnings'], + ['name' => 'manage-any-warnings', 'module' => 'warnings', 'label' => 'Manage All Warnings', 'description' => 'Manage Any Warnings'], + ['name' => 'manage-own-warnings', 'module' => 'warnings', 'label' => 'Manage Own Warnings', 'description' => 'Manage Limited Warnings that is created by own'], + ['name' => 'view-warnings', 'module' => 'warnings', 'label' => 'View Warnings', 'description' => 'View Warnings'], + ['name' => 'create-warnings', 'module' => 'warnings', 'label' => 'Create Warnings', 'description' => 'Can create warnings'], + ['name' => 'edit-warnings', 'module' => 'warnings', 'label' => 'Edit Warnings', 'description' => 'Can edit warnings'], + ['name' => 'delete-warnings', 'module' => 'warnings', 'label' => 'Delete Warnings', 'description' => 'Can delete warnings'], + ['name' => 'approve-warnings', 'module' => 'warnings', 'label' => 'Approve Warnings', 'description' => 'Can approve warnings'], + ['name' => 'acknowledge-warnings', 'module' => 'warnings', 'label' => 'Acknowledge Warnings', 'description' => 'Can acknowledge warnings'], + + // Trip management + ['name' => 'manage-trips', 'module' => 'trips', 'label' => 'Manage Trips', 'description' => 'Can manage trips'], + ['name' => 'manage-any-trips', 'module' => 'trips', 'label' => 'Manage All Trips', 'description' => 'Manage Any Trips'], + ['name' => 'manage-own-trips', 'module' => 'trips', 'label' => 'Manage Own Trips', 'description' => 'Manage Limited Trips that is created by own'], + ['name' => 'view-trips', 'module' => 'trips', 'label' => 'View Trips', 'description' => 'View Trips'], + ['name' => 'create-trips', 'module' => 'trips', 'label' => 'Create Trips', 'description' => 'Can create trips'], + ['name' => 'edit-trips', 'module' => 'trips', 'label' => 'Edit Trips', 'description' => 'Can edit trips'], + ['name' => 'delete-trips', 'module' => 'trips', 'label' => 'Delete Trips', 'description' => 'Can delete trips'], + ['name' => 'approve-trips', 'module' => 'trips', 'label' => 'Approve Trips', 'description' => 'Can approve trips'], + ['name' => 'manage-trip-expenses', 'module' => 'trips', 'label' => 'Manage Trip Expenses', 'description' => 'Can manage trip expenses'], + ['name' => 'approve-trip-expenses', 'module' => 'trips', 'label' => 'Approve Trip Expenses', 'description' => 'Can approve trip expenses'], + + // Complaint management + ['name' => 'manage-complaints', 'module' => 'complaints', 'label' => 'Manage Complaints', 'description' => 'Can manage complaints'], + ['name' => 'manage-any-complaints', 'module' => 'complaints', 'label' => 'Manage All Complaints', 'description' => 'Manage Any Complaints'], + ['name' => 'manage-own-complaints', 'module' => 'complaints', 'label' => 'Manage Own Complaints', 'description' => 'Manage Limited Complaints that is created by own'], + ['name' => 'view-complaints', 'module' => 'complaints', 'label' => 'View Complaints', 'description' => 'View Complaints'], + ['name' => 'create-complaints', 'module' => 'complaints', 'label' => 'Create Complaints', 'description' => 'Can create complaints'], + ['name' => 'edit-complaints', 'module' => 'complaints', 'label' => 'Edit Complaints', 'description' => 'Can edit complaints'], + ['name' => 'delete-complaints', 'module' => 'complaints', 'label' => 'Delete Complaints', 'description' => 'Can delete complaints'], + ['name' => 'assign-complaints', 'module' => 'complaints', 'label' => 'Assign Complaints', 'description' => 'Can assign complaints to HR personnel'], + ['name' => 'resolve-complaints', 'module' => 'complaints', 'label' => 'Resolve Complaints', 'description' => 'Can resolve complaints'], + + // Employee Transfer management + ['name' => 'manage-employee-transfers', 'module' => 'transfers', 'label' => 'Manage Transfers', 'description' => 'Can manage employee transfers'], + ['name' => 'manage-any-employee-transfers', 'module' => 'transfers', 'label' => 'Manage All Transfers', 'description' => 'Manage Any Transfers'], + ['name' => 'manage-own-employee-transfers', 'module' => 'transfers', 'label' => 'Manage Own Transfers', 'description' => 'Manage Limited Transfers that is created by own'], + ['name' => 'view-employee-transfers', 'module' => 'transfers', 'label' => 'View Transfers', 'description' => 'View employee transfers'], + ['name' => 'create-employee-transfers', 'module' => 'transfers', 'label' => 'Create Transfers', 'description' => 'Can create employee transfers'], + ['name' => 'edit-employee-transfers', 'module' => 'transfers', 'label' => 'Edit Transfers', 'description' => 'Can edit employee transfers'], + ['name' => 'delete-employee-transfers', 'module' => 'transfers', 'label' => 'Delete Transfers', 'description' => 'Can delete employee transfers'], + ['name' => 'approve-employee-transfers', 'module' => 'transfers', 'label' => 'Approve Transfers', 'description' => 'Can approve employee transfers'], + ['name' => 'reject-employee-transfers', 'module' => 'transfers', 'label' => 'Reject Transfers', 'description' => 'Can reject employee transfers'], + + // Holiday management + ['name' => 'manage-holidays', 'module' => 'holidays', 'label' => 'Manage Holidays', 'description' => 'Can manage holidays'], + ['name' => 'manage-any-holidays', 'module' => 'holidays', 'label' => 'Manage All Holidays', 'description' => 'Manage Any Holidays'], + ['name' => 'manage-own-holidays', 'module' => 'holidays', 'label' => 'Manage Own Holidays', 'description' => 'Manage Limited Holidays that is created by own'], + ['name' => 'view-holidays', 'module' => 'holidays', 'label' => 'View Holidays', 'description' => 'View holidays'], + ['name' => 'create-holidays', 'module' => 'holidays', 'label' => 'Create Holidays', 'description' => 'Can create holidays'], + ['name' => 'edit-holidays', 'module' => 'holidays', 'label' => 'Edit Holidays', 'description' => 'Can edit holidays'], + ['name' => 'delete-holidays', 'module' => 'holidays', 'label' => 'Delete Holidays', 'description' => 'Can delete holidays'], + + // Announcement management + ['name' => 'manage-announcements', 'module' => 'announcements', 'label' => 'Manage Announcements', 'description' => 'Can manage announcements'], + ['name' => 'manage-any-announcements', 'module' => 'announcements', 'label' => 'Manage All Announcements', 'description' => 'Manage Any Announcements'], + ['name' => 'manage-own-announcements', 'module' => 'announcements', 'label' => 'Manage Own Announcements', 'description' => 'Manage Limited Announcements that is created by own'], + ['name' => 'view-announcements', 'module' => 'announcements', 'label' => 'View Announcements', 'description' => 'View announcements'], + ['name' => 'create-announcements', 'module' => 'announcements', 'label' => 'Create Announcements', 'description' => 'Can create announcements'], + ['name' => 'edit-announcements', 'module' => 'announcements', 'label' => 'Edit Announcements', 'description' => 'Can edit announcements'], + ['name' => 'delete-announcements', 'module' => 'announcements', 'label' => 'Delete Announcements', 'description' => 'Can delete announcements'], + + // Asset Type management + ['name' => 'manage-asset-types', 'module' => 'asset-types', 'label' => 'Manage Asset Types', 'description' => 'Can manage asset types'], + ['name' => 'manage-any-asset-types', 'module' => 'asset-types', 'label' => 'Manage All Asset Types', 'description' => 'Manage Any Asset Types'], + ['name' => 'manage-own-asset-types', 'module' => 'asset-types', 'label' => 'Manage Own Asset Types', 'description' => 'Manage Limited Asset Types that is created by own'], + ['name' => 'view-asset-types', 'module' => 'asset-types', 'label' => 'View Asset Types', 'description' => 'View asset types'], + ['name' => 'create-asset-types', 'module' => 'asset-types', 'label' => 'Create Asset Types', 'description' => 'Can create asset types'], + ['name' => 'edit-asset-types', 'module' => 'asset-types', 'label' => 'Edit Asset Types', 'description' => 'Can edit asset types'], + ['name' => 'delete-asset-types', 'module' => 'asset-types', 'label' => 'Delete Asset Types', 'description' => 'Can delete asset types'], + + // Asset management + ['name' => 'manage-assets', 'module' => 'assets', 'label' => 'Manage Assets', 'description' => 'Can manage assets'], + ['name' => 'manage-any-assets', 'module' => 'assets', 'label' => 'Manage All Assets', 'description' => 'Manage Any Assets'], + ['name' => 'manage-own-assets', 'module' => 'assets', 'label' => 'Manage Own Assets', 'description' => 'Manage Limited Assets that is created by own'], + ['name' => 'view-assets', 'module' => 'assets', 'label' => 'View Assets', 'description' => 'View assets'], + ['name' => 'create-assets', 'module' => 'assets', 'label' => 'Create Assets', 'description' => 'Can create assets'], + ['name' => 'edit-assets', 'module' => 'assets', 'label' => 'Edit Assets', 'description' => 'Can edit assets'], + ['name' => 'delete-assets', 'module' => 'assets', 'label' => 'Delete Assets', 'description' => 'Can delete assets'], + ['name' => 'assign-assets', 'module' => 'assets', 'label' => 'Assign Assets', 'description' => 'Can assign assets to employees'], + ['name' => 'manage-asset-maintenance', 'module' => 'assets', 'label' => 'Manage Asset Maintenance', 'description' => 'Can manage asset maintenance'], + ['name' => 'export-assets', 'module' => 'assets', 'label' => 'Export Assets', 'description' => 'Can export assets to CSV'], + ['name' => 'import-assets', 'module' => 'assets', 'label' => 'Import Assets', 'description' => 'Can import assets from CSV/Excel'], + + // Training Type management + ['name' => 'manage-training-types', 'module' => 'training-types', 'label' => 'Manage Training Types', 'description' => 'Can manage training types'], + ['name' => 'manage-any-training-types', 'module' => 'training-types', 'label' => 'Manage All Training Types', 'description' => 'Manage Any Training Types'], + ['name' => 'manage-own-training-types', 'module' => 'training-types', 'label' => 'Manage Own Training Types', 'description' => 'Manage Limited Training Types that is created by own'], + ['name' => 'view-training-types', 'module' => 'training-types', 'label' => 'View Training Types', 'description' => 'View training types'], + ['name' => 'create-training-types', 'module' => 'training-types', 'label' => 'Create Training Types', 'description' => 'Can create training types'], + ['name' => 'edit-training-types', 'module' => 'training-types', 'label' => 'Edit Training Types', 'description' => 'Can edit training types'], + ['name' => 'delete-training-types', 'module' => 'training-types', 'label' => 'Delete Training Types', 'description' => 'Can delete training types'], + + // Training Program management + ['name' => 'manage-training-programs', 'module' => 'training-programs', 'label' => 'Manage Training Programs', 'description' => 'Can manage training programs'], + ['name' => 'manage-any-training-programs', 'module' => 'training-programs', 'label' => 'Manage All Training Programs', 'description' => 'Manage Any Training Programs'], + ['name' => 'manage-own-training-programs', 'module' => 'training-programs', 'label' => 'Manage Own Training Programs', 'description' => 'Manage Limited Training Programs that is created by own'], + ['name' => 'view-training-programs', 'module' => 'training-programs', 'label' => 'View Training Programs', 'description' => 'View training programs'], + ['name' => 'create-training-programs', 'module' => 'training-programs', 'label' => 'Create Training Programs', 'description' => 'Can create training programs'], + ['name' => 'edit-training-programs', 'module' => 'training-programs', 'label' => 'Edit Training Programs', 'description' => 'Can edit training programs'], + ['name' => 'delete-training-programs', 'module' => 'training-programs', 'label' => 'Delete Training Programs', 'description' => 'Can delete training programs'], + + // Training Session management + ['name' => 'manage-training-sessions', 'module' => 'training-sessions', 'label' => 'Manage Training Sessions', 'description' => 'Can manage training sessions'], + ['name' => 'manage-any-training-sessions', 'module' => 'training-sessions', 'label' => 'Manage All Training Sessions', 'description' => 'Manage Any Training Sessions'], + ['name' => 'manage-own-training-sessions', 'module' => 'training-sessions', 'label' => 'Manage Own Training Sessions', 'description' => 'Manage Limited Training Sessions that is created by own'], + ['name' => 'view-training-sessions', 'module' => 'training-sessions', 'label' => 'View Training Sessions', 'description' => 'View training sessions'], + ['name' => 'create-training-sessions', 'module' => 'training-sessions', 'label' => 'Create Training Sessions', 'description' => 'Can create training sessions'], + ['name' => 'edit-training-sessions', 'module' => 'training-sessions', 'label' => 'Edit Training Sessions', 'description' => 'Can edit training sessions'], + ['name' => 'delete-training-sessions', 'module' => 'training-sessions', 'label' => 'Delete Training Sessions', 'description' => 'Can delete training sessions'], + ['name' => 'manage-attendance', 'module' => 'training-sessions', 'label' => 'Manage Attendance', 'description' => 'Can manage training session attendance'], + + // Employee Training management + ['name' => 'manage-employee-trainings', 'module' => 'employee-trainings', 'label' => 'Manage Employee Trainings', 'description' => 'Can manage employee trainings'], + ['name' => 'manage-any-employee-trainings', 'module' => 'employee-trainings', 'label' => 'Manage All Employee Trainings', 'description' => 'Manage Any Employee Trainings'], + ['name' => 'manage-own-employee-trainings', 'module' => 'employee-trainings', 'label' => 'Manage Own Employee Trainings', 'description' => 'Manage Limited Employee Trainings that is created by own'], + ['name' => 'view-employee-trainings', 'module' => 'employee-trainings', 'label' => 'View Employee Trainings', 'description' => 'View employee trainings'], + ['name' => 'create-employee-trainings', 'module' => 'employee-trainings', 'label' => 'Create Employee Trainings', 'description' => 'Can create employee trainings'], + ['name' => 'edit-employee-trainings', 'module' => 'employee-trainings', 'label' => 'Edit Employee Trainings', 'description' => 'Can edit employee trainings'], + ['name' => 'delete-employee-trainings', 'module' => 'employee-trainings', 'label' => 'Delete Employee Trainings', 'description' => 'Can delete employee trainings'], + ['name' => 'assign-trainings', 'module' => 'employee-trainings', 'label' => 'Assign Trainings', 'description' => 'Can assign trainings to employees'], + ['name' => 'manage-assessments', 'module' => 'employee-trainings', 'label' => 'Manage Assessments', 'description' => 'Can manage training assessments'], + ['name' => 'record-assessment-results', 'module' => 'employee-trainings', 'label' => 'Record Assessment Results', 'description' => 'Can record training assessment results'], + + + // New Module Permissions + + ['name' => 'manage-performance-indicator-categories', 'module' => 'performance_indicator_categories', 'label' => 'Manage Performance Indicator Categories', 'description' => 'Can manage performance indicator categories'], + ['name' => 'manage-any-performance-indicator-categories', 'module' => 'performance_indicator_categories', 'label' => 'Manage All Performance Indicator Categories', 'description' => 'Manage Any Performance Indicator Categories'], + ['name' => 'manage-own-performance-indicator-categories', 'module' => 'performance_indicator_categories', 'label' => 'Manage Own Performance Indicator Categories', 'description' => 'Manage Limited Performance Indicator Categories that is created by own'], + ['name' => 'view-performance-indicator-categories', 'module' => 'performance_indicator_categories', 'label' => 'View Performance Indicator Categories', 'description' => 'View Performance Indicator Categories'], + ['name' => 'create-performance-indicator-categories', 'module' => 'performance_indicator_categories', 'label' => 'Create Performance Indicator Categories', 'description' => 'Can create performance indicator categories'], + ['name' => 'edit-performance-indicator-categories', 'module' => 'performance_indicator_categories', 'label' => 'Edit Performance Indicator Categories', 'description' => 'Can edit performance indicator categories'], + ['name' => 'delete-performance-indicator-categories', 'module' => 'performance_indicator_categories', 'label' => 'Delete Performance Indicator Categories', 'description' => 'Can delete performance indicator categories'], + + // Performance Indicators + ['name' => 'manage-performance-indicators', 'module' => 'performance_indicators', 'label' => 'Manage Performance Indicators', 'description' => 'Can manage performance indicators'], + ['name' => 'manage-any-performance-indicators', 'module' => 'performance_indicators', 'label' => 'Manage All Performance Indicators', 'description' => 'Manage Any Performance Indicators'], + ['name' => 'manage-own-performance-indicators', 'module' => 'performance_indicators', 'label' => 'Manage Own Performance Indicators', 'description' => 'Manage Limited Performance Indicators that is created by own'], + ['name' => 'view-performance-indicators', 'module' => 'performance_indicators', 'label' => 'View Performance Indicators', 'description' => 'View Performance Indicators'], + ['name' => 'create-performance-indicators', 'module' => 'performance_indicators', 'label' => 'Create Performance Indicators', 'description' => 'Can create performance indicators'], + ['name' => 'edit-performance-indicators', 'module' => 'performance_indicators', 'label' => 'Edit Performance Indicators', 'description' => 'Can edit performance indicators'], + ['name' => 'delete-performance-indicators', 'module' => 'performance_indicators', 'label' => 'Delete Performance Indicators', 'description' => 'Can delete performance indicators'], + + // Goal Types + ['name' => 'manage-goal-types', 'module' => 'goal_types', 'label' => 'Manage Goal Types', 'description' => 'Can manage goal types'], + ['name' => 'manage-any-goal-types', 'module' => 'goal_types', 'label' => 'Manage All Goal Types', 'description' => 'Manage Any Goal Types'], + ['name' => 'manage-own-goal-types', 'module' => 'goal_types', 'label' => 'Manage Own Goal Types', 'description' => 'Manage Limited Goal Types that is created by own'], + ['name' => 'view-goal-types', 'module' => 'goal_types', 'label' => 'View Goal Types', 'description' => 'View Goal Types'], + ['name' => 'create-goal-types', 'module' => 'goal_types', 'label' => 'Create Goal Types', 'description' => 'Can create goal types'], + ['name' => 'edit-goal-types', 'module' => 'goal_types', 'label' => 'Edit Goal Types', 'description' => 'Can edit goal types'], + ['name' => 'delete-goal-types', 'module' => 'goal_types', 'label' => 'Delete Goal Types', 'description' => 'Can delete goal types'], + + // Employee Goals + ['name' => 'manage-employee-goals', 'module' => 'employee_goals', 'label' => 'Manage Employee Goals', 'description' => 'Can manage employee goals'], + ['name' => 'manage-any-employee-goals', 'module' => 'employee_goals', 'label' => 'Manage All Employee Goals', 'description' => 'Manage Any Employee Goals'], + ['name' => 'manage-own-employee-goals', 'module' => 'employee_goals', 'label' => 'Manage Own Employee Goals', 'description' => 'Manage Limited Employee Goals that is created by own'], + ['name' => 'view-employee-goals', 'module' => 'employee_goals', 'label' => 'View Employee Goals', 'description' => 'View Employee Goals'], + ['name' => 'create-employee-goals', 'module' => 'employee_goals', 'label' => 'Create Employee Goals', 'description' => 'Can create employee goals'], + ['name' => 'edit-employee-goals', 'module' => 'employee_goals', 'label' => 'Edit Employee Goals', 'description' => 'Can edit employee goals'], + ['name' => 'delete-employee-goals', 'module' => 'employee_goals', 'label' => 'Delete Employee Goals', 'description' => 'Can delete employee goals'], + + // Review Cycles + ['name' => 'manage-review-cycles', 'module' => 'review_cycles', 'label' => 'Manage Review Cycles', 'description' => 'Can manage review cycles'], + ['name' => 'manage-any-review-cycles', 'module' => 'review_cycles', 'label' => 'Manage All Review Cycles', 'description' => 'Manage Any Review Cycles'], + ['name' => 'manage-own-review-cycles', 'module' => 'review_cycles', 'label' => 'Manage Own Review Cycles', 'description' => 'Manage Limited Review Cycles that is created by own'], + ['name' => 'view-review-cycles', 'module' => 'review_cycles', 'label' => 'View Review Cycles', 'description' => 'View Review Cycles'], + ['name' => 'create-review-cycles', 'module' => 'review_cycles', 'label' => 'Create Review Cycles', 'description' => 'Can create review cycles'], + ['name' => 'edit-review-cycles', 'module' => 'review_cycles', 'label' => 'Edit Review Cycles', 'description' => 'Can edit review cycles'], + ['name' => 'delete-review-cycles', 'module' => 'review_cycles', 'label' => 'Delete Review Cycles', 'description' => 'Can delete review cycles'], + + // Employee Reviews + ['name' => 'manage-employee-reviews', 'module' => 'employee_reviews', 'label' => 'Manage Employee Reviews', 'description' => 'Can manage employee reviews'], + ['name' => 'manage-any-employee-reviews', 'module' => 'employee_reviews', 'label' => 'Manage All Employee Reviews', 'description' => 'Manage Any Employee Reviews'], + ['name' => 'manage-own-employee-reviews', 'module' => 'employee_reviews', 'label' => 'Manage Own Employee Reviews', 'description' => 'Manage Limited Employee Reviews that is created by own'], + ['name' => 'view-employee-reviews', 'module' => 'employee_reviews', 'label' => 'View Employee Reviews', 'description' => 'View Employee Reviews'], + ['name' => 'create-employee-reviews', 'module' => 'employee_reviews', 'label' => 'Create Employee Reviews', 'description' => 'Can create employee reviews'], + ['name' => 'edit-employee-reviews', 'module' => 'employee_reviews', 'label' => 'Edit Employee Reviews', 'description' => 'Can edit employee reviews'], + ['name' => 'delete-employee-reviews', 'module' => 'employee_reviews', 'label' => 'Delete Employee Reviews', 'description' => 'Can delete employee reviews'], + + // Job Categories management + ['name' => 'manage-job-categories', 'module' => 'job_categories', 'label' => 'Manage Job Categories', 'description' => 'Can manage job categories'], + ['name' => 'manage-any-job-categories', 'module' => 'job_categories', 'label' => 'Manage All Job Categories', 'description' => 'Manage Any Job Categories'], + ['name' => 'manage-own-job-categories', 'module' => 'job_categories', 'label' => 'Manage Own Job Categories', 'description' => 'Manage Limited Job Categories that is created by own'], + ['name' => 'view-job-categories', 'module' => 'job_categories', 'label' => 'View Job Categories', 'description' => 'View Job Categories'], + ['name' => 'create-job-categories', 'module' => 'job_categories', 'label' => 'Create Job Categories', 'description' => 'Can create job categories'], + ['name' => 'edit-job-categories', 'module' => 'job_categories', 'label' => 'Edit Job Categories', 'description' => 'Can edit job categories'], + ['name' => 'delete-job-categories', 'module' => 'job_categories', 'label' => 'Delete Job Categories', 'description' => 'Can delete job categories'], + + // Job Requisitions management + ['name' => 'manage-job-requisitions', 'module' => 'job_requisitions', 'label' => 'Manage Job Requisitions', 'description' => 'Can manage job requisitions'], + ['name' => 'manage-any-job-requisitions', 'module' => 'job_requisitions', 'label' => 'Manage All Job Requisitions', 'description' => 'Manage Any Job Requisitions'], + ['name' => 'manage-own-job-requisitions', 'module' => 'job_requisitions', 'label' => 'Manage Own Job Requisitions', 'description' => 'Manage Limited Job Requisitions that is created by own'], + ['name' => 'view-job-requisitions', 'module' => 'job_requisitions', 'label' => 'View Job Requisitions', 'description' => 'View Job Requisitions'], + ['name' => 'create-job-requisitions', 'module' => 'job_requisitions', 'label' => 'Create Job Requisitions', 'description' => 'Can create job requisitions'], + ['name' => 'edit-job-requisitions', 'module' => 'job_requisitions', 'label' => 'Edit Job Requisitions', 'description' => 'Can edit job requisitions'], + ['name' => 'delete-job-requisitions', 'module' => 'job_requisitions', 'label' => 'Delete Job Requisitions', 'description' => 'Can delete job requisitions'], + ['name' => 'approve-job-requisitions', 'module' => 'job_requisitions', 'label' => 'Approve Job Requisitions', 'description' => 'Can approve job requisitions'], + + // Job Types management + ['name' => 'manage-job-types', 'module' => 'job_types', 'label' => 'Manage Job Types', 'description' => 'Can manage job types'], + ['name' => 'manage-any-job-types', 'module' => 'job_types', 'label' => 'Manage All Job Types', 'description' => 'Manage Any Job Types'], + ['name' => 'manage-own-job-types', 'module' => 'job_types', 'label' => 'Manage Own Job Types', 'description' => 'Manage Limited Job Types that is created by own'], + ['name' => 'view-job-types', 'module' => 'job_types', 'label' => 'View Job Types', 'description' => 'View Job Types'], + ['name' => 'create-job-types', 'module' => 'job_types', 'label' => 'Create Job Types', 'description' => 'Can create job types'], + ['name' => 'edit-job-types', 'module' => 'job_types', 'label' => 'Edit Job Types', 'description' => 'Can edit job types'], + ['name' => 'delete-job-types', 'module' => 'job_types', 'label' => 'Delete Job Types', 'description' => 'Can delete job types'], + + // Job Locations management + ['name' => 'manage-job-locations', 'module' => 'job_locations', 'label' => 'Manage Job Locations', 'description' => 'Can manage job locations'], + ['name' => 'manage-any-job-locations', 'module' => 'job_locations', 'label' => 'Manage All Job Locations', 'description' => 'Manage Any Job Locations'], + ['name' => 'manage-own-job-locations', 'module' => 'job_locations', 'label' => 'Manage Own Job Locations', 'description' => 'Manage Limited Job Locations that is created by own'], + ['name' => 'view-job-locations', 'module' => 'job_locations', 'label' => 'View Job Locations', 'description' => 'View Job Locations'], + ['name' => 'create-job-locations', 'module' => 'job_locations', 'label' => 'Create Job Locations', 'description' => 'Can create job locations'], + ['name' => 'edit-job-locations', 'module' => 'job_locations', 'label' => 'Edit Job Locations', 'description' => 'Can edit job locations'], + ['name' => 'delete-job-locations', 'module' => 'job_locations', 'label' => 'Delete Job Locations', 'description' => 'Can delete job locations'], + + // Job Postings management + ['name' => 'manage-job-postings', 'module' => 'job_postings', 'label' => 'Manage Job Postings', 'description' => 'Can manage job postings'], + ['name' => 'manage-any-job-postings', 'module' => 'job_postings', 'label' => 'Manage All Job Postings', 'description' => 'Manage Any Job Postings'], + ['name' => 'manage-own-job-postings', 'module' => 'job_postings', 'label' => 'Manage Own Job Postings', 'description' => 'Manage Limited Job Postings that is created by own'], + ['name' => 'view-job-postings', 'module' => 'job_postings', 'label' => 'View Job Postings', 'description' => 'View Job Postings'], + ['name' => 'create-job-postings', 'module' => 'job_postings', 'label' => 'Create Job Postings', 'description' => 'Can create job postings'], + ['name' => 'edit-job-postings', 'module' => 'job_postings', 'label' => 'Edit Job Postings', 'description' => 'Can edit job postings'], + ['name' => 'delete-job-postings', 'module' => 'job_postings', 'label' => 'Delete Job Postings', 'description' => 'Can delete job postings'], + ['name' => 'publish-job-postings', 'module' => 'job_postings', 'label' => 'Publish Job Postings', 'description' => 'Can publish job postings'], + + // Candidate Sources management + ['name' => 'manage-candidate-sources', 'module' => 'candidate_sources', 'label' => 'Manage Candidate Sources', 'description' => 'Can manage candidate sources'], + ['name' => 'manage-any-candidate-sources', 'module' => 'candidate_sources', 'label' => 'Manage All Candidate Sources', 'description' => 'Manage Any Candidate Sources'], + ['name' => 'manage-own-candidate-sources', 'module' => 'candidate_sources', 'label' => 'Manage Own Candidate Sources', 'description' => 'Manage Limited Candidate Sources that is created by own'], + ['name' => 'view-candidate-sources', 'module' => 'candidate_sources', 'label' => 'View Candidate Sources', 'description' => 'View Candidate Sources'], + ['name' => 'create-candidate-sources', 'module' => 'candidate_sources', 'label' => 'Create Candidate Sources', 'description' => 'Can create candidate sources'], + ['name' => 'edit-candidate-sources', 'module' => 'candidate_sources', 'label' => 'Edit Candidate Sources', 'description' => 'Can edit candidate sources'], + ['name' => 'delete-candidate-sources', 'module' => 'candidate_sources', 'label' => 'Delete Candidate Sources', 'description' => 'Can delete candidate sources'], + + // Candidates management + ['name' => 'manage-candidates', 'module' => 'candidates', 'label' => 'Manage Candidates', 'description' => 'Can manage candidates'], + ['name' => 'manage-any-candidates', 'module' => 'candidates', 'label' => 'Manage All Candidates', 'description' => 'Manage Any Candidates'], + ['name' => 'manage-own-candidates', 'module' => 'candidates', 'label' => 'Manage Own Candidates', 'description' => 'Manage Limited Candidates that is created by own'], + ['name' => 'view-candidates', 'module' => 'candidates', 'label' => 'View Candidates', 'description' => 'View Candidates'], + ['name' => 'convert-to-employee', 'module' => 'candidates', 'label' => 'Convert to Employee', 'description' => 'Convert Candidate to Employee'], + // ['name' => 'create-candidates', 'module' => 'candidates', 'label' => 'Create Candidates', 'description' => 'Can create candidates'], + ['name' => 'edit-candidates', 'module' => 'candidates', 'label' => 'Edit Candidates', 'description' => 'Can edit candidates'], + ['name' => 'delete-candidates', 'module' => 'candidates', 'label' => 'Delete Candidates', 'description' => 'Can delete candidates'], + + // Interview Types management + ['name' => 'manage-interview-types', 'module' => 'interview_types', 'label' => 'Manage Interview Types', 'description' => 'Can manage interview types'], + ['name' => 'manage-any-interview-types', 'module' => 'interview_types', 'label' => 'Manage All Interview Types', 'description' => 'Manage Any Interview Types'], + ['name' => 'manage-own-interview-types', 'module' => 'interview_types', 'label' => 'Manage Own Interview Types', 'description' => 'Manage Limited Interview Types that is created by own'], + ['name' => 'view-interview-types', 'module' => 'interview_types', 'label' => 'View Interview Types', 'description' => 'View Interview Types'], + ['name' => 'create-interview-types', 'module' => 'interview_types', 'label' => 'Create Interview Types', 'description' => 'Can create interview types'], + ['name' => 'edit-interview-types', 'module' => 'interview_types', 'label' => 'Edit Interview Types', 'description' => 'Can edit interview types'], + ['name' => 'delete-interview-types', 'module' => 'interview_types', 'label' => 'Delete Interview Types', 'description' => 'Can delete interview types'], + + // Interview Rounds management + ['name' => 'manage-interview-rounds', 'module' => 'interview_rounds', 'label' => 'Manage Interview Rounds', 'description' => 'Can manage interview rounds'], + ['name' => 'manage-any-interview-rounds', 'module' => 'interview_rounds', 'label' => 'Manage All Interview Rounds', 'description' => 'Manage Any Interview Rounds'], + ['name' => 'manage-own-interview-rounds', 'module' => 'interview_rounds', 'label' => 'Manage Own Interview Rounds', 'description' => 'Manage Limited Interview Rounds that is created by own'], + ['name' => 'view-interview-rounds', 'module' => 'interview_rounds', 'label' => 'View Interview Rounds', 'description' => 'View Interview Rounds'], + ['name' => 'create-interview-rounds', 'module' => 'interview_rounds', 'label' => 'Create Interview Rounds', 'description' => 'Can create interview rounds'], + ['name' => 'edit-interview-rounds', 'module' => 'interview_rounds', 'label' => 'Edit Interview Rounds', 'description' => 'Can edit interview rounds'], + ['name' => 'delete-interview-rounds', 'module' => 'interview_rounds', 'label' => 'Delete Interview Rounds', 'description' => 'Can delete interview rounds'], + + // Interviews management + ['name' => 'manage-interviews', 'module' => 'interviews', 'label' => 'Manage Interviews', 'description' => 'Can manage interviews'], + ['name' => 'manage-any-interviews', 'module' => 'interviews', 'label' => 'Manage All Interviews', 'description' => 'Manage Any Interviews'], + ['name' => 'manage-own-interviews', 'module' => 'interviews', 'label' => 'Manage Own Interviews', 'description' => 'Manage Limited Interviews that is created by own'], + ['name' => 'view-interviews', 'module' => 'interviews', 'label' => 'View Interviews', 'description' => 'View Interviews'], + ['name' => 'create-interviews', 'module' => 'interviews', 'label' => 'Create Interviews', 'description' => 'Can create interviews'], + ['name' => 'edit-interviews', 'module' => 'interviews', 'label' => 'Edit Interviews', 'description' => 'Can edit interviews'], + ['name' => 'delete-interviews', 'module' => 'interviews', 'label' => 'Delete Interviews', 'description' => 'Can delete interviews'], + + // Interview Feedback management + ['name' => 'manage-interview-feedback', 'module' => 'interview_feedback', 'label' => 'Manage Interview Feedback', 'description' => 'Can manage interview feedback'], + ['name' => 'manage-any-interview-feedback', 'module' => 'interview_feedback', 'label' => 'Manage All Interview Feedback', 'description' => 'Manage Any Interview Feedback'], + ['name' => 'manage-own-interview-feedback', 'module' => 'interview_feedback', 'label' => 'Manage Own Interview Feedback', 'description' => 'Manage Limited Interview Feedback that is created by own'], + ['name' => 'view-interview-feedback', 'module' => 'interview_feedback', 'label' => 'View Interview Feedback', 'description' => 'View Interview Feedback'], + ['name' => 'create-interview-feedback', 'module' => 'interview_feedback', 'label' => 'Create Interview Feedback', 'description' => 'Can create interview feedback'], + ['name' => 'edit-interview-feedback', 'module' => 'interview_feedback', 'label' => 'Edit Interview Feedback', 'description' => 'Can edit interview feedback'], + ['name' => 'delete-interview-feedback', 'module' => 'interview_feedback', 'label' => 'Delete Interview Feedback', 'description' => 'Can delete interview feedback'], + + // Custom Questions management + ['name' => 'manage-custom-questions', 'module' => 'custom_questions', 'label' => 'Manage Custom Questions', 'description' => 'Can manage custom questions'], + ['name' => 'manage-any-custom-questions', 'module' => 'custom_questions', 'label' => 'Manage All Custom Questions', 'description' => 'Manage Any Custom Questions'], + ['name' => 'manage-own-custom-questions', 'module' => 'custom_questions', 'label' => 'Manage Own Custom Questions', 'description' => 'Manage Limited Custom Questions that is created by own'], + ['name' => 'view-custom-questions', 'module' => 'custom_questions', 'label' => 'View Custom Questions', 'description' => 'View Custom Questions'], + ['name' => 'create-custom-questions', 'module' => 'custom_questions', 'label' => 'Create Custom Questions', 'description' => 'Can create custom questions'], + ['name' => 'edit-custom-questions', 'module' => 'custom_questions', 'label' => 'Edit Custom Questions', 'description' => 'Can edit custom questions'], + ['name' => 'delete-custom-questions', 'module' => 'custom_questions', 'label' => 'Delete Custom Questions', 'description' => 'Can delete custom questions'], + + // Candidate Assessments management + ['name' => 'manage-candidate-assessments', 'module' => 'candidate_assessments', 'label' => 'Manage Candidate Assessments', 'description' => 'Can manage candidate assessments'], + ['name' => 'manage-any-candidate-assessments', 'module' => 'candidate_assessments', 'label' => 'Manage All Candidate Assessments', 'description' => 'Manage Any Candidate Assessments'], + ['name' => 'manage-own-candidate-assessments', 'module' => 'candidate_assessments', 'label' => 'Manage Own Candidate Assessments', 'description' => 'Manage Limited Candidate Assessments that is created by own'], + ['name' => 'view-candidate-assessments', 'module' => 'candidate_assessments', 'label' => 'View Candidate Assessments', 'description' => 'View Candidate Assessments'], + ['name' => 'create-candidate-assessments', 'module' => 'candidate_assessments', 'label' => 'Create Candidate Assessments', 'description' => 'Can create candidate assessments'], + ['name' => 'edit-candidate-assessments', 'module' => 'candidate_assessments', 'label' => 'Edit Candidate Assessments', 'description' => 'Can edit candidate assessments'], + ['name' => 'delete-candidate-assessments', 'module' => 'candidate_assessments', 'label' => 'Delete Candidate Assessments', 'description' => 'Can delete candidate assessments'], + + // Offer Templates management + ['name' => 'manage-offer-templates', 'module' => 'offer_templates', 'label' => 'Manage Offer Templates', 'description' => 'Can manage offer templates'], + ['name' => 'manage-any-offer-templates', 'module' => 'offer_templates', 'label' => 'Manage All Offer Templates', 'description' => 'Manage Any Offer Templates'], + ['name' => 'manage-own-offer-templates', 'module' => 'offer_templates', 'label' => 'Manage Own Offer Templates', 'description' => 'Manage Limited Offer Templates that is created by own'], + ['name' => 'view-offer-templates', 'module' => 'offer_templates', 'label' => 'View Offer Templates', 'description' => 'View Offer Templates'], + ['name' => 'create-offer-templates', 'module' => 'offer_templates', 'label' => 'Create Offer Templates', 'description' => 'Can create offer templates'], + ['name' => 'edit-offer-templates', 'module' => 'offer_templates', 'label' => 'Edit Offer Templates', 'description' => 'Can edit offer templates'], + ['name' => 'delete-offer-templates', 'module' => 'offer_templates', 'label' => 'Delete Offer Templates', 'description' => 'Can delete offer templates'], + + // Offers management + ['name' => 'manage-offers', 'module' => 'offers', 'label' => 'Manage Offers', 'description' => 'Can manage offers'], + ['name' => 'manage-any-offers', 'module' => 'offers', 'label' => 'Manage All Offers', 'description' => 'Manage Any Offers'], + ['name' => 'manage-own-offers', 'module' => 'offers', 'label' => 'Manage Own Offers', 'description' => 'Manage Limited Offers that is created by own'], + ['name' => 'view-offers', 'module' => 'offers', 'label' => 'View Offers', 'description' => 'View Offers'], + ['name' => 'create-offers', 'module' => 'offers', 'label' => 'Create Offers', 'description' => 'Can create offers'], + ['name' => 'edit-offers', 'module' => 'offers', 'label' => 'Edit Offers', 'description' => 'Can edit offers'], + ['name' => 'delete-offers', 'module' => 'offers', 'label' => 'Delete Offers', 'description' => 'Can delete offers'], + ['name' => 'approve-offers', 'module' => 'offers', 'label' => 'Approve Offers', 'description' => 'Can approve offers'], + + // Onboarding Checklists management + ['name' => 'manage-onboarding-checklists', 'module' => 'onboarding_checklists', 'label' => 'Manage Onboarding Checklists', 'description' => 'Can manage onboarding checklists'], + ['name' => 'manage-any-onboarding-checklists', 'module' => 'onboarding_checklists', 'label' => 'Manage All Onboarding Checklists', 'description' => 'Manage Any Onboarding Checklists'], + ['name' => 'manage-own-onboarding-checklists', 'module' => 'onboarding_checklists', 'label' => 'Manage Own Onboarding Checklists', 'description' => 'Manage Limited Onboarding Checklists that is created by own'], + ['name' => 'view-onboarding-checklists', 'module' => 'onboarding_checklists', 'label' => 'View Onboarding Checklists', 'description' => 'View Onboarding Checklists'], + ['name' => 'create-onboarding-checklists', 'module' => 'onboarding_checklists', 'label' => 'Create Onboarding Checklists', 'description' => 'Can create onboarding checklists'], + ['name' => 'edit-onboarding-checklists', 'module' => 'onboarding_checklists', 'label' => 'Edit Onboarding Checklists', 'description' => 'Can edit onboarding checklists'], + ['name' => 'delete-onboarding-checklists', 'module' => 'onboarding_checklists', 'label' => 'Delete Onboarding Checklists', 'description' => 'Can delete onboarding checklists'], + + // Checklist Items management + ['name' => 'manage-checklist-items', 'module' => 'checklist_items', 'label' => 'Manage Checklist Items', 'description' => 'Can manage checklist items'], + ['name' => 'manage-any-checklist-items', 'module' => 'checklist_items', 'label' => 'Manage All Checklist Items', 'description' => 'Manage Any Checklist Items'], + ['name' => 'manage-own-checklist-items', 'module' => 'checklist_items', 'label' => 'Manage Own Checklist Items', 'description' => 'Manage Limited Checklist Items that is created by own'], + ['name' => 'view-checklist-items', 'module' => 'checklist_items', 'label' => 'View Checklist Items', 'description' => 'View Checklist Items'], + ['name' => 'create-checklist-items', 'module' => 'checklist_items', 'label' => 'Create Checklist Items', 'description' => 'Can create checklist items'], + ['name' => 'edit-checklist-items', 'module' => 'checklist_items', 'label' => 'Edit Checklist Items', 'description' => 'Can edit checklist items'], + ['name' => 'delete-checklist-items', 'module' => 'checklist_items', 'label' => 'Delete Checklist Items', 'description' => 'Can delete checklist items'], + + // Candidate Onboarding management + ['name' => 'manage-candidate-onboarding', 'module' => 'candidate_onboarding', 'label' => 'Manage Candidate Onboarding', 'description' => 'Can manage candidate onboarding'], + ['name' => 'manage-any-candidate-onboarding', 'module' => 'candidate_onboarding', 'label' => 'Manage All Candidate Onboarding', 'description' => 'Manage Any Candidate Onboarding'], + ['name' => 'manage-own-candidate-onboarding', 'module' => 'candidate_onboarding', 'label' => 'Manage Own Candidate Onboarding', 'description' => 'Manage Limited Candidate Onboarding that is created by own'], + ['name' => 'view-candidate-onboarding', 'module' => 'candidate_onboarding', 'label' => 'View Candidate Onboarding', 'description' => 'View Candidate Onboarding'], + ['name' => 'manage-candidate-onboarding-status', 'module' => 'candidate_onboarding', 'label' => 'Manage Candidate Onboarding status', 'description' => 'Manage Candidate Onboarding Status'], + ['name' => 'create-candidate-onboarding', 'module' => 'candidate_onboarding', 'label' => 'Create Candidate Onboarding', 'description' => 'Can create candidate onboarding'], + ['name' => 'edit-candidate-onboarding', 'module' => 'candidate_onboarding', 'label' => 'Edit Candidate Onboarding', 'description' => 'Can edit candidate onboarding'], + ['name' => 'delete-candidate-onboarding', 'module' => 'candidate_onboarding', 'label' => 'Delete Candidate Onboarding', 'description' => 'Can delete candidate onboarding'], + + // Meeting Types management + ['name' => 'manage-meeting-types', 'module' => 'meeting_types', 'label' => 'Manage Meeting Types', 'description' => 'Can manage meeting types'], + ['name' => 'manage-any-meeting-types', 'module' => 'meeting_types', 'label' => 'Manage All Meeting Types', 'description' => 'Manage Any Meeting Types'], + ['name' => 'manage-own-meeting-types', 'module' => 'meeting_types', 'label' => 'Manage Own Meeting Types', 'description' => 'Manage Limited Meeting Types that is created by own'], + ['name' => 'view-meeting-types', 'module' => 'meeting_types', 'label' => 'View Meeting Types', 'description' => 'View Meeting Types'], + ['name' => 'create-meeting-types', 'module' => 'meeting_types', 'label' => 'Create Meeting Types', 'description' => 'Can create meeting types'], + ['name' => 'edit-meeting-types', 'module' => 'meeting_types', 'label' => 'Edit Meeting Types', 'description' => 'Can edit meeting types'], + ['name' => 'delete-meeting-types', 'module' => 'meeting_types', 'label' => 'Delete Meeting Types', 'description' => 'Can delete meeting types'], + + // Meeting Rooms management + ['name' => 'manage-meeting-rooms', 'module' => 'meeting_rooms', 'label' => 'Manage Meeting Rooms', 'description' => 'Can manage meeting rooms'], + ['name' => 'manage-any-meeting-rooms', 'module' => 'meeting_rooms', 'label' => 'Manage All Meeting Rooms', 'description' => 'Manage Any Meeting Rooms'], + ['name' => 'manage-own-meeting-rooms', 'module' => 'meeting_rooms', 'label' => 'Manage Own Meeting Rooms', 'description' => 'Manage Limited Meeting Rooms that is created by own'], + ['name' => 'view-meeting-rooms', 'module' => 'meeting_rooms', 'label' => 'View Meeting Rooms', 'description' => 'View Meeting Rooms'], + ['name' => 'create-meeting-rooms', 'module' => 'meeting_rooms', 'label' => 'Create Meeting Rooms', 'description' => 'Can create meeting rooms'], + ['name' => 'edit-meeting-rooms', 'module' => 'meeting_rooms', 'label' => 'Edit Meeting Rooms', 'description' => 'Can edit meeting rooms'], + ['name' => 'delete-meeting-rooms', 'module' => 'meeting_rooms', 'label' => 'Delete Meeting Rooms', 'description' => 'Can delete meeting rooms'], + + // Meetings management + ['name' => 'manage-meetings', 'module' => 'meetings', 'label' => 'Manage Meetings', 'description' => 'Can manage meetings'], + ['name' => 'manage-any-meetings', 'module' => 'meetings', 'label' => 'Manage All Meetings', 'description' => 'Manage Any Meetings'], + ['name' => 'manage-own-meetings', 'module' => 'meetings', 'label' => 'Manage Own Meetings', 'description' => 'Manage Limited Meetings that is created by own'], + ['name' => 'view-meetings', 'module' => 'meetings', 'label' => 'View Meetings', 'description' => 'View Meetings'], + ['name' => 'create-meetings', 'module' => 'meetings', 'label' => 'Create Meetings', 'description' => 'Can create meetings'], + ['name' => 'edit-meetings', 'module' => 'meetings', 'label' => 'Edit Meetings', 'description' => 'Can edit meetings'], + ['name' => 'delete-meetings', 'module' => 'meetings', 'label' => 'Delete Meetings', 'description' => 'Can delete meetings'], + ['name' => 'manage-meeting-status', 'module' => 'meetings', 'label' => 'Manage Meeting Status', 'description' => 'Can manage meeting status'], + + // Meeting Attendees management + ['name' => 'manage-meeting-attendees', 'module' => 'meeting_attendees', 'label' => 'Manage Meeting Attendees', 'description' => 'Can manage meeting attendees'], + ['name' => 'manage-any-meeting-attendees', 'module' => 'meeting_attendees', 'label' => 'Manage All Meeting Attendees', 'description' => 'Manage Any Meeting Attendees'], + ['name' => 'manage-own-meeting-attendees', 'module' => 'meeting_attendees', 'label' => 'Manage Own Meeting Attendees', 'description' => 'Manage Limited Meeting Attendees that is created by own'], + ['name' => 'view-meeting-attendees', 'module' => 'meeting_attendees', 'label' => 'View Meeting Attendees', 'description' => 'View Meeting Attendees'], + ['name' => 'create-meeting-attendees', 'module' => 'meeting_attendees', 'label' => 'Create Meeting Attendees', 'description' => 'Can create meeting attendees'], + ['name' => 'edit-meeting-attendees', 'module' => 'meeting_attendees', 'label' => 'Edit Meeting Attendees', 'description' => 'Can edit meeting attendees'], + ['name' => 'delete-meeting-attendees', 'module' => 'meeting_attendees', 'label' => 'Delete Meeting Attendees', 'description' => 'Can delete meeting attendees'], + ['name' => 'manage-meeting-rsvp-status', 'module' => 'meeting_attendees', 'label' => 'Manage Meeting RSVP Status', 'description' => 'Can manage meeting RSVP status'], + ['name' => 'manage-meeting-attendance', 'module' => 'meeting_attendees', 'label' => 'Manage Meeting Attendance', 'description' => 'Can manage meeting attendance'], + + // Meeting Minutes management + ['name' => 'manage-meeting-minutes', 'module' => 'meeting_minutes', 'label' => 'Manage Meeting Minutes', 'description' => 'Can manage meeting minutes'], + ['name' => 'manage-any-meeting-minutes', 'module' => 'meeting_minutes', 'label' => 'Manage All Meeting Minutes', 'description' => 'Manage Any Meeting Minutes'], + ['name' => 'manage-own-meeting-minutes', 'module' => 'meeting_minutes', 'label' => 'Manage Own Meeting Minutes', 'description' => 'Manage Limited Meeting Minutes that is created by own'], + ['name' => 'view-meeting-minutes', 'module' => 'meeting_minutes', 'label' => 'View Meeting Minutes', 'description' => 'View Meeting Minutes'], + ['name' => 'create-meeting-minutes', 'module' => 'meeting_minutes', 'label' => 'Create Meeting Minutes', 'description' => 'Can create meeting minutes'], + ['name' => 'edit-meeting-minutes', 'module' => 'meeting_minutes', 'label' => 'Edit Meeting Minutes', 'description' => 'Can edit meeting minutes'], + ['name' => 'delete-meeting-minutes', 'module' => 'meeting_minutes', 'label' => 'Delete Meeting Minutes', 'description' => 'Can delete meeting minutes'], + + // Action Items management + ['name' => 'manage-action-items', 'module' => 'action_items', 'label' => 'Manage Action Items', 'description' => 'Can manage action items'], + ['name' => 'manage-any-action-items', 'module' => 'action_items', 'label' => 'Manage All Action Items', 'description' => 'Manage Any Action Items'], + ['name' => 'manage-own-action-items', 'module' => 'action_items', 'label' => 'Manage Own Action Items', 'description' => 'Manage Limited Action Items that is created by own'], + ['name' => 'view-action-items', 'module' => 'action_items', 'label' => 'View Action Items', 'description' => 'View Action Items'], + ['name' => 'create-action-items', 'module' => 'action_items', 'label' => 'Create Action Items', 'description' => 'Can create action items'], + ['name' => 'edit-action-items', 'module' => 'action_items', 'label' => 'Edit Action Items', 'description' => 'Can edit action items'], + ['name' => 'delete-action-items', 'module' => 'action_items', 'label' => 'Delete Action Items', 'description' => 'Can delete action items'], + + + + // Contract Types management + ['name' => 'manage-contract-types', 'module' => 'contract_types', 'label' => 'Manage Contract Types', 'description' => 'Can manage contract types'], + ['name' => 'manage-any-contract-types', 'module' => 'contract_types', 'label' => 'Manage All Contract Types', 'description' => 'Manage Any Contract Types'], + ['name' => 'manage-own-contract-types', 'module' => 'contract_types', 'label' => 'Manage Own Contract Types', 'description' => 'Manage Limited Contract Types that is created by own'], + ['name' => 'view-contract-types', 'module' => 'contract_types', 'label' => 'View Contract Types', 'description' => 'View Contract Types'], + ['name' => 'create-contract-types', 'module' => 'contract_types', 'label' => 'Create Contract Types', 'description' => 'Can create contract types'], + ['name' => 'edit-contract-types', 'module' => 'contract_types', 'label' => 'Edit Contract Types', 'description' => 'Can edit contract types'], + ['name' => 'delete-contract-types', 'module' => 'contract_types', 'label' => 'Delete Contract Types', 'description' => 'Can delete contract types'], + + // Employee Contracts management + ['name' => 'manage-employee-contracts', 'module' => 'employee_contracts', 'label' => 'Manage Employee Contracts', 'description' => 'Can manage employee contracts'], + ['name' => 'manage-any-employee-contracts', 'module' => 'employee_contracts', 'label' => 'Manage All Employee Contracts', 'description' => 'Manage Any Employee Contracts'], + ['name' => 'manage-own-employee-contracts', 'module' => 'employee_contracts', 'label' => 'Manage Own Employee Contracts', 'description' => 'Manage Limited Employee Contracts that is created by own'], + ['name' => 'view-employee-contracts', 'module' => 'employee_contracts', 'label' => 'View Employee Contracts', 'description' => 'View Employee Contracts'], + ['name' => 'create-employee-contracts', 'module' => 'employee_contracts', 'label' => 'Create Employee Contracts', 'description' => 'Can create employee contracts'], + ['name' => 'edit-employee-contracts', 'module' => 'employee_contracts', 'label' => 'Edit Employee Contracts', 'description' => 'Can edit employee contracts'], + ['name' => 'delete-employee-contracts', 'module' => 'employee_contracts', 'label' => 'Delete Employee Contracts', 'description' => 'Can delete employee contracts'], + ['name' => 'approve-employee-contracts', 'module' => 'employee_contracts', 'label' => 'Approve Employee Contracts', 'description' => 'Can approve employee contracts'], + ['name' => 'reject-employee-contracts', 'module' => 'employee_contracts', 'label' => 'Reject Employee Contracts', 'description' => 'Can reject employee contracts'], + + + + // Contract Renewals management + ['name' => 'manage-contract-renewals', 'module' => 'contract_renewals', 'label' => 'Manage Contract Renewals', 'description' => 'Can manage contract renewals'], + ['name' => 'manage-any-contract-renewals', 'module' => 'contract_renewals', 'label' => 'Manage All Contract Renewals', 'description' => 'Manage Any Contract Renewals'], + ['name' => 'manage-own-contract-renewals', 'module' => 'contract_renewals', 'label' => 'Manage Own Contract Renewals', 'description' => 'Manage Limited Contract Renewals that is created by own'], + ['name' => 'view-contract-renewals', 'module' => 'contract_renewals', 'label' => 'View Contract Renewals', 'description' => 'View Contract Renewals'], + ['name' => 'create-contract-renewals', 'module' => 'contract_renewals', 'label' => 'Create Contract Renewals', 'description' => 'Can create contract renewals'], + ['name' => 'edit-contract-renewals', 'module' => 'contract_renewals', 'label' => 'Edit Contract Renewals', 'description' => 'Can edit contract renewals'], + ['name' => 'delete-contract-renewals', 'module' => 'contract_renewals', 'label' => 'Delete Contract Renewals', 'description' => 'Can delete contract renewals'], + ['name' => 'approve-contract-renewals', 'module' => 'contract_renewals', 'label' => 'Approve Contract Renewals', 'description' => 'Can approve contract renewals'], + ['name' => 'reject-contract-renewals', 'module' => 'contract_renewals', 'label' => 'Reject Contract Renewals', 'description' => 'Can reject contract renewals'], + + // Contract Templates management + ['name' => 'manage-contract-templates', 'module' => 'contract_templates', 'label' => 'Manage Contract Templates', 'description' => 'Can manage contract templates'], + ['name' => 'manage-any-contract-templates', 'module' => 'contract_templates', 'label' => 'Manage All Contract Templates', 'description' => 'Manage Any Contract Templates'], + ['name' => 'manage-own-contract-templates', 'module' => 'contract_templates', 'label' => 'Manage Own Contract Templates', 'description' => 'Manage Limited Contract Templates that is created by own'], + ['name' => 'view-contract-templates', 'module' => 'contract_templates', 'label' => 'View Contract Templates', 'description' => 'View Contract Templates'], + ['name' => 'create-contract-templates', 'module' => 'contract_templates', 'label' => 'Create Contract Templates', 'description' => 'Can create contract templates'], + ['name' => 'edit-contract-templates', 'module' => 'contract_templates', 'label' => 'Edit Contract Templates', 'description' => 'Can edit contract templates'], + ['name' => 'delete-contract-templates', 'module' => 'contract_templates', 'label' => 'Delete Contract Templates', 'description' => 'Can delete contract templates'], + + // Document Categories management + ['name' => 'manage-document-categories', 'module' => 'document_categories', 'label' => 'Manage Document Categories', 'description' => 'Can manage document categories'], + ['name' => 'manage-any-document-categories', 'module' => 'document_categories', 'label' => 'Manage All Document Categories', 'description' => 'Manage Any Document Categories'], + ['name' => 'manage-own-document-categories', 'module' => 'document_categories', 'label' => 'Manage Own Document Categories', 'description' => 'Manage Limited Document Categories that is created by own'], + ['name' => 'view-document-categories', 'module' => 'document_categories', 'label' => 'View Document Categories', 'description' => 'View Document Categories'], + ['name' => 'create-document-categories', 'module' => 'document_categories', 'label' => 'Create Document Categories', 'description' => 'Can create document categories'], + ['name' => 'edit-document-categories', 'module' => 'document_categories', 'label' => 'Edit Document Categories', 'description' => 'Can edit document categories'], + ['name' => 'delete-document-categories', 'module' => 'document_categories', 'label' => 'Delete Document Categories', 'description' => 'Can delete document categories'], + + // HR Documents management + ['name' => 'manage-hr-documents', 'module' => 'hr_documents', 'label' => 'Manage HR Documents', 'description' => 'Can manage HR documents'], + ['name' => 'manage-any-hr-documents', 'module' => 'hr_documents', 'label' => 'Manage All HR Documents', 'description' => 'Manage Any HR Documents'], + ['name' => 'manage-own-hr-documents', 'module' => 'hr_documents', 'label' => 'Manage Own HR Documents', 'description' => 'Manage Limited HR Documents that is created by own'], + ['name' => 'view-hr-documents', 'module' => 'hr_documents', 'label' => 'View HR Documents', 'description' => 'View HR Documents'], + ['name' => 'create-hr-documents', 'module' => 'hr_documents', 'label' => 'Create HR Documents', 'description' => 'Can create HR documents'], + ['name' => 'edit-hr-documents', 'module' => 'hr_documents', 'label' => 'Edit HR Documents', 'description' => 'Can edit HR documents'], + ['name' => 'delete-hr-documents', 'module' => 'hr_documents', 'label' => 'Delete HR Documents', 'description' => 'Can delete HR documents'], + + + + // Document Acknowledgments management + ['name' => 'manage-document-acknowledgments', 'module' => 'document_acknowledgments', 'label' => 'Manage Document Acknowledgments', 'description' => 'Can manage document acknowledgments'], + ['name' => 'manage-any-document-acknowledgments', 'module' => 'document_acknowledgments', 'label' => 'Manage All Document Acknowledgments', 'description' => 'Manage Any Document Acknowledgments'], + ['name' => 'manage-own-document-acknowledgments', 'module' => 'document_acknowledgments', 'label' => 'Manage Own Document Acknowledgments', 'description' => 'Manage Limited Document Acknowledgments that is created by own'], + ['name' => 'view-document-acknowledgments', 'module' => 'document_acknowledgments', 'label' => 'View Document Acknowledgments', 'description' => 'View Document Acknowledgments'], + ['name' => 'create-document-acknowledgments', 'module' => 'document_acknowledgments', 'label' => 'Create Document Acknowledgments', 'description' => 'Can create document acknowledgments'], + ['name' => 'edit-document-acknowledgments', 'module' => 'document_acknowledgments', 'label' => 'Edit Document Acknowledgments', 'description' => 'Can edit document acknowledgments'], + ['name' => 'delete-document-acknowledgments', 'module' => 'document_acknowledgments', 'label' => 'Delete Document Acknowledgments', 'description' => 'Can delete document acknowledgments'], + ['name' => 'acknowledge-document-acknowledgments', 'module' => 'document_acknowledgments', 'label' => 'Acknowledge Document Acknowledgments', 'description' => 'Can acknowledge document acknowledgments'], + + // Document Templates management + ['name' => 'manage-document-templates', 'module' => 'document_templates', 'label' => 'Manage Document Templates', 'description' => 'Can manage document templates'], + ['name' => 'manage-any-document-templates', 'module' => 'document_templates', 'label' => 'Manage All Document Templates', 'description' => 'Manage Any Document Templates'], + ['name' => 'manage-own-document-templates', 'module' => 'document_templates', 'label' => 'Manage Own Document Templates', 'description' => 'Manage Limited Document Templates that is created by own'], + ['name' => 'view-document-templates', 'module' => 'document_templates', 'label' => 'View Document Templates', 'description' => 'View Document Templates'], + ['name' => 'create-document-templates', 'module' => 'document_templates', 'label' => 'Create Document Templates', 'description' => 'Can create document templates'], + ['name' => 'edit-document-templates', 'module' => 'document_templates', 'label' => 'Edit Document Templates', 'description' => 'Can edit document templates'], + ['name' => 'delete-document-templates', 'module' => 'document_templates', 'label' => 'Delete Document Templates', 'description' => 'Can delete document templates'], + + // Leave Types management + ['name' => 'manage-leave-types', 'module' => 'leave_types', 'label' => 'Manage Leave Types', 'description' => 'Can manage leave types'], + ['name' => 'manage-any-leave-types', 'module' => 'leave_types', 'label' => 'Manage All Leave Types', 'description' => 'Manage Any Leave Types'], + ['name' => 'manage-own-leave-types', 'module' => 'leave_types', 'label' => 'Manage Own Leave Types', 'description' => 'Manage Limited Leave Types that is created by own'], + ['name' => 'view-leave-types', 'module' => 'leave_types', 'label' => 'View Leave Types', 'description' => 'View Leave Types'], + ['name' => 'create-leave-types', 'module' => 'leave_types', 'label' => 'Create Leave Types', 'description' => 'Can create leave types'], + ['name' => 'edit-leave-types', 'module' => 'leave_types', 'label' => 'Edit Leave Types', 'description' => 'Can edit leave types'], + ['name' => 'delete-leave-types', 'module' => 'leave_types', 'label' => 'Delete Leave Types', 'description' => 'Can delete leave types'], + + // Leave Policies management + ['name' => 'manage-leave-policies', 'module' => 'leave_policies', 'label' => 'Manage Leave Policies', 'description' => 'Can manage leave policies'], + ['name' => 'manage-any-leave-policies', 'module' => 'leave_policies', 'label' => 'Manage All Leave Policies', 'description' => 'Manage Any Leave Policies'], + ['name' => 'manage-own-leave-policies', 'module' => 'leave_policies', 'label' => 'Manage Own Leave Policies', 'description' => 'Manage Limited Leave Policies that is created by own'], + ['name' => 'view-leave-policies', 'module' => 'leave_policies', 'label' => 'View Leave Policies', 'description' => 'View Leave Policies'], + ['name' => 'create-leave-policies', 'module' => 'leave_policies', 'label' => 'Create Leave Policies', 'description' => 'Can create leave policies'], + ['name' => 'edit-leave-policies', 'module' => 'leave_policies', 'label' => 'Edit Leave Policies', 'description' => 'Can edit leave policies'], + ['name' => 'delete-leave-policies', 'module' => 'leave_policies', 'label' => 'Delete Leave Policies', 'description' => 'Can delete leave policies'], + + // Leave Applications management + ['name' => 'manage-leave-applications', 'module' => 'leave_applications', 'label' => 'Manage Leave Applications', 'description' => 'Can manage leave applications'], + ['name' => 'manage-any-leave-applications', 'module' => 'leave_applications', 'label' => 'Manage All Leave Applications', 'description' => 'Manage Any Leave Applications'], + ['name' => 'manage-own-leave-applications', 'module' => 'leave_applications', 'label' => 'Manage Own Leave Applications', 'description' => 'Manage Limited Leave Applications that is created by own'], + ['name' => 'view-leave-applications', 'module' => 'leave_applications', 'label' => 'View Leave Applications', 'description' => 'View Leave Applications'], + ['name' => 'create-leave-applications', 'module' => 'leave_applications', 'label' => 'Create Leave Applications', 'description' => 'Can create leave applications'], + ['name' => 'edit-leave-applications', 'module' => 'leave_applications', 'label' => 'Edit Leave Applications', 'description' => 'Can edit leave applications'], + ['name' => 'delete-leave-applications', 'module' => 'leave_applications', 'label' => 'Delete Leave Applications', 'description' => 'Can delete leave applications'], + ['name' => 'export-leave-applications', 'module' => 'leave_applications', 'label' => 'Export Leave Applications', 'description' => 'Can Export leave applications'], + ['name' => 'approve-leave-applications', 'module' => 'leave_applications', 'label' => 'Approve Leave Applications', 'description' => 'Can approve leave applications'], + ['name' => 'reject-leave-applications', 'module' => 'leave_applications', 'label' => 'Reject Leave Applications', 'description' => 'Can reject leave applications'], + + // Leave Balances management + ['name' => 'manage-leave-balances', 'module' => 'leave_balances', 'label' => 'Manage Leave Balances', 'description' => 'Can manage leave balances'], + ['name' => 'manage-any-leave-balances', 'module' => 'leave_balances', 'label' => 'Manage All Leave Balances', 'description' => 'Manage Any Leave Balances'], + ['name' => 'manage-own-leave-balances', 'module' => 'leave_balances', 'label' => 'Manage Own Leave Balances', 'description' => 'Manage Limited Leave Balances that is created by own'], + ['name' => 'view-leave-balances', 'module' => 'leave_balances', 'label' => 'View Leave Balances', 'description' => 'View Leave Balances'], + ['name' => 'create-leave-balances', 'module' => 'leave_balances', 'label' => 'Create Leave Balances', 'description' => 'Can create leave balances'], + ['name' => 'edit-leave-balances', 'module' => 'leave_balances', 'label' => 'Edit Leave Balances', 'description' => 'Can edit leave balances'], + ['name' => 'delete-leave-balances', 'module' => 'leave_balances', 'label' => 'Delete Leave Balances', 'description' => 'Can delete leave balances'], + ['name' => 'adjust-leave-balances', 'module' => 'leave_balances', 'label' => 'Adjust Leave Balances', 'description' => 'Can make manual adjustments to leave balances'], + + // Shifts management + ['name' => 'manage-shifts', 'module' => 'shifts', 'label' => 'Manage Shifts', 'description' => 'Can manage shifts'], + ['name' => 'manage-any-shifts', 'module' => 'shifts', 'label' => 'Manage All Shifts', 'description' => 'Manage Any Shifts'], + ['name' => 'manage-own-shifts', 'module' => 'shifts', 'label' => 'Manage Own Shifts', 'description' => 'Manage Limited Shifts that is created by own'], + ['name' => 'view-shifts', 'module' => 'shifts', 'label' => 'View Shifts', 'description' => 'View Shifts'], + ['name' => 'create-shifts', 'module' => 'shifts', 'label' => 'Create Shifts', 'description' => 'Can create shifts'], + ['name' => 'edit-shifts', 'module' => 'shifts', 'label' => 'Edit Shifts', 'description' => 'Can edit shifts'], + ['name' => 'delete-shifts', 'module' => 'shifts', 'label' => 'Delete Shifts', 'description' => 'Can delete shifts'], + + // Attendance Policies management + ['name' => 'manage-attendance-policies', 'module' => 'attendance_policies', 'label' => 'Manage Attendance Policies', 'description' => 'Can manage attendance policies'], + ['name' => 'manage-any-attendance-policies', 'module' => 'attendance_policies', 'label' => 'Manage All Attendance Policies', 'description' => 'Manage Any Attendance Policies'], + ['name' => 'manage-own-attendance-policies', 'module' => 'attendance_policies', 'label' => 'Manage Own Attendance Policies', 'description' => 'Manage Limited Attendance Policies that is created by own'], + ['name' => 'view-attendance-policies', 'module' => 'attendance_policies', 'label' => 'View Attendance Policies', 'description' => 'View Attendance Policies'], + ['name' => 'create-attendance-policies', 'module' => 'attendance_policies', 'label' => 'Create Attendance Policies', 'description' => 'Can create attendance policies'], + ['name' => 'edit-attendance-policies', 'module' => 'attendance_policies', 'label' => 'Edit Attendance Policies', 'description' => 'Can edit attendance policies'], + ['name' => 'delete-attendance-policies', 'module' => 'attendance_policies', 'label' => 'Delete Attendance Policies', 'description' => 'Can delete attendance policies'], + + // Attendance Records management + ['name' => 'manage-attendance-records', 'module' => 'attendance_records', 'label' => 'Manage Attendance Records', 'description' => 'Can manage attendance records'], + ['name' => 'manage-any-attendance-records', 'module' => 'attendance_records', 'label' => 'Manage All Attendance Records', 'description' => 'Manage Any Attendance Records'], + ['name' => 'manage-own-attendance-records', 'module' => 'attendance_records', 'label' => 'Manage Own Attendance Records', 'description' => 'Manage Limited Attendance Records that is created by own'], + ['name' => 'view-attendance-records', 'module' => 'attendance_records', 'label' => 'View Attendance Records', 'description' => 'View Attendance Records'], + ['name' => 'create-attendance-records', 'module' => 'attendance_records', 'label' => 'Create Attendance Records', 'description' => 'Can create attendance records'], + ['name' => 'edit-attendance-records', 'module' => 'attendance_records', 'label' => 'Edit Attendance Records', 'description' => 'Can edit attendance records'], + ['name' => 'delete-attendance-records', 'module' => 'attendance_records', 'label' => 'Delete Attendance Records', 'description' => 'Can delete attendance records'], + ['name' => 'import-attendance-record', 'module' => 'attendance_records', 'label' => 'Import Attendance Records', 'description' => 'Can Import Attendance Records'], + ['name' => 'export-attendance-record', 'module' => 'attendance_records', 'label' => 'Export Attendance Records', 'description' => 'Can Export Attendance Records'], + ['name' => 'clock-in-out', 'module' => 'attendance_records', 'label' => 'Clock In/Out', 'description' => 'Can clock in and out'], + + // Attendance Regularizations management + ['name' => 'manage-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'Manage Attendance Regularizations', 'description' => 'Can manage attendance regularizations'], + ['name' => 'manage-any-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'Manage All Attendance Regularizations', 'description' => 'Manage Any Attendance Regularizations'], + ['name' => 'manage-own-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'Manage Own Attendance Regularizations', 'description' => 'Manage Limited Attendance Regularizations that is created by own'], + ['name' => 'view-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'View Attendance Regularizations', 'description' => 'View Attendance Regularizations'], + ['name' => 'create-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'Create Attendance Regularizations', 'description' => 'Can create attendance regularizations'], + ['name' => 'edit-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'Edit Attendance Regularizations', 'description' => 'Can edit attendance regularizations'], + ['name' => 'delete-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'Delete Attendance Regularizations', 'description' => 'Can delete attendance regularizations'], + ['name' => 'approve-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'Approve Attendance Regularizations', 'description' => 'Can approve attendance regularizations'], + ['name' => 'reject-attendance-regularizations', 'module' => 'attendance_regularizations', 'label' => 'Reject Attendance Regularizations', 'description' => 'Can reject attendance regularizations'], + + // Time Entries management + ['name' => 'manage-time-entries', 'module' => 'time_entries', 'label' => 'Manage Time Entries', 'description' => 'Can manage time entries'], + ['name' => 'manage-any-time-entries', 'module' => 'time_entries', 'label' => 'Manage All Time Entries', 'description' => 'Manage Any Time Entries'], + ['name' => 'manage-own-time-entries', 'module' => 'time_entries', 'label' => 'Manage Own Time Entries', 'description' => 'Manage Limited Time Entries that is created by own'], + ['name' => 'view-time-entries', 'module' => 'time_entries', 'label' => 'View Time Entries', 'description' => 'View Time Entries'], + ['name' => 'create-time-entries', 'module' => 'time_entries', 'label' => 'Create Time Entries', 'description' => 'Can create time entries'], + ['name' => 'edit-time-entries', 'module' => 'time_entries', 'label' => 'Edit Time Entries', 'description' => 'Can edit time entries'], + ['name' => 'delete-time-entries', 'module' => 'time_entries', 'label' => 'Delete Time Entries', 'description' => 'Can delete time entries'], + ['name' => 'import-time-entry', 'module' => 'time_entries', 'label' => 'Import Time Entries', 'description' => 'Can Import Time Entries'], + ['name' => 'export-time-entry', 'module' => 'time_entries', 'label' => 'Export Time Entries', 'description' => 'Can Export Time Entries'], + ['name' => 'approve-time-entries', 'module' => 'time_entries', 'label' => 'Approve Time Entries', 'description' => 'Can approve time entries'], + ['name' => 'reject-time-entries', 'module' => 'time_entries', 'label' => 'Reject Time Entries', 'description' => 'Can reject time entries'], + + // Salary Components management + ['name' => 'manage-salary-components', 'module' => 'salary_components', 'label' => 'Manage Salary Components', 'description' => 'Can manage salary components'], + ['name' => 'manage-any-salary-components', 'module' => 'salary_components', 'label' => 'Manage All Salary Components', 'description' => 'Manage Any Salary Components'], + ['name' => 'manage-own-salary-components', 'module' => 'salary_components', 'label' => 'Manage Own Salary Components', 'description' => 'Manage Limited Salary Components that is created by own'], + ['name' => 'view-salary-components', 'module' => 'salary_components', 'label' => 'View Salary Components', 'description' => 'View Salary Components'], + ['name' => 'create-salary-components', 'module' => 'salary_components', 'label' => 'Create Salary Components', 'description' => 'Can create salary components'], + ['name' => 'edit-salary-components', 'module' => 'salary_components', 'label' => 'Edit Salary Components', 'description' => 'Can edit salary components'], + ['name' => 'delete-salary-components', 'module' => 'salary_components', 'label' => 'Delete Salary Components', 'description' => 'Can delete salary components'], + + // Employee Salaries management + ['name' => 'manage-employee-salaries', 'module' => 'employee_salaries', 'label' => 'Manage Employee Salaries', 'description' => 'Can manage employee salaries'], + ['name' => 'manage-any-employee-salaries', 'module' => 'employee_salaries', 'label' => 'Manage All Employee Salaries', 'description' => 'Manage Any Employee Salaries'], + ['name' => 'manage-own-employee-salaries', 'module' => 'employee_salaries', 'label' => 'Manage Own Employee Salaries', 'description' => 'Manage Limited Employee Salaries that is created by own'], + ['name' => 'view-employee-salaries', 'module' => 'employee_salaries', 'label' => 'View Employee Salaries', 'description' => 'View Employee Salaries'], + ['name' => 'create-employee-salaries', 'module' => 'employee_salaries', 'label' => 'Create Employee Salaries', 'description' => 'Can create employee salaries'], + ['name' => 'edit-employee-salaries', 'module' => 'employee_salaries', 'label' => 'Edit Employee Salaries', 'description' => 'Can edit employee salaries'], + ['name' => 'delete-employee-salaries', 'module' => 'employee_salaries', 'label' => 'Delete Employee Salaries', 'description' => 'Can delete employee salaries'], + + // Payroll Runs management + ['name' => 'manage-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Manage Payroll Runs', 'description' => 'Can manage payroll runs'], + ['name' => 'manage-any-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Manage All Payroll Runs', 'description' => 'Manage Any Payroll Runs'], + ['name' => 'manage-own-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Manage Own Payroll Runs', 'description' => 'Manage Limited Payroll Runs that is created by own'], + ['name' => 'view-payroll-runs', 'module' => 'payroll_runs', 'label' => 'View Payroll Runs', 'description' => 'View Payroll Runs'], + ['name' => 'create-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Create Payroll Runs', 'description' => 'Can create payroll runs'], + ['name' => 'edit-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Edit Payroll Runs', 'description' => 'Can edit payroll runs'], + ['name' => 'delete-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Delete Payroll Runs', 'description' => 'Can delete payroll runs'], + ['name' => 'process-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Process Payroll Runs', 'description' => 'Can process payroll runs'], + ['name' => 'import-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Import Payroll Runs', 'description' => 'Can Import Payroll Runs'], + ['name' => 'export-payroll-runs', 'module' => 'payroll_runs', 'label' => 'Export Payroll Runs', 'description' => 'Can Export Payroll Runs'], + + ['name' => 'delete-payroll-entries', 'module' => 'payroll_entries', 'label' => 'Delete Payroll Entries', 'description' => 'Can delete payroll entries'], + + // Payslips management + ['name' => 'manage-payslips', 'module' => 'payslips', 'label' => 'Manage Payslips', 'description' => 'Can manage payslips'], + ['name' => 'manage-any-payslips', 'module' => 'payslips', 'label' => 'Manage All Payslips', 'description' => 'Manage Any Payslips'], + ['name' => 'manage-own-payslips', 'module' => 'payslips', 'label' => 'Manage Own Payslips', 'description' => 'Manage Limited Payslips that is created by own'], + ['name' => 'view-payslips', 'module' => 'payslips', 'label' => 'View Payslips', 'description' => 'View Payslips'], + ['name' => 'create-payslips', 'module' => 'payslips', 'label' => 'Create Payslips', 'description' => 'Can create payslips'], + ['name' => 'download-payslips', 'module' => 'payslips', 'label' => 'Download Payslips', 'description' => 'Can download payslips'], + ['name' => 'send-payslips', 'module' => 'payslips', 'label' => 'Send Payslips', 'description' => 'Can send payslips via email'], + + // Calendar permissions + ['name' => 'manage-calendar', 'module' => 'calendar', 'label' => 'Manage Calendar', 'description' => 'Can manage calendar'], + ['name' => 'view-calendar', 'module' => 'calendar', 'label' => 'View Calendar', 'description' => 'Can view calendar'], + + // Manage Working Days + ['name' => 'manage-working-days-settings', 'module' => 'working_days', 'label' => 'Manage Working Days', 'description' => 'Manage Working Days'], + ['name' => 'update-working-days-settings', 'module' => 'working_days', 'label' => 'Manage Working Days', 'description' => 'Manage Working Days'], + + // Biometric Attendance + ['name' => 'manage-biometric-attendance', 'module' => 'biometric_attendance', 'label' => 'Manage Biometric Attendance', 'description' => 'Can manage biometric attendance data'], + ['name' => 'manage-any-biometric-attendance', 'module' => 'biometric_attendance', 'label' => 'Manage All Biometric Attendance', 'description' => 'Manage Any Biometric Attendance'], + ['name' => 'manage-own-biometric-attendance', 'module' => 'biometric_attendance', 'label' => 'Manage Own Biometric Attendance', 'description' => 'Manage Limited Biometric Attendance that is created by own'], + ['name' => 'view-biometric-attendance', 'module' => 'biometric_attendance', 'label' => 'View Biometric Attendance', 'description' => 'View Biometric Attendance'], + ['name' => 'sync-biometric-attendance', 'module' => 'biometric_attendance', 'label' => 'Sync Biometric Attendance', 'description' => 'Can sync biometric attendance data'], + ['name' => 'manage-biomatric-attedance-settings', 'module' => 'biometric_attendance', 'label' => 'Manage Biometric Attendance Setting', 'description' => 'Manage Biometric Attendance Setting'], + + // Ip Restriction + ['name' => 'manage-ip-restriction-settings', 'module' => 'ip_restriction', 'label' => 'Manage Ip Restriction Settings', 'description' => 'Manage Ip Restriction Settings'], + ['name' => 'create-ip-restriction', 'module' => 'ip_restriction', 'label' => 'Create Ip Restriction', 'description' => 'Create Ip Restriction'], + ['name' => 'edit-ip-restriction', 'module' => 'ip_restriction', 'label' => 'Edit Ip Restriction', 'description' => 'Edit Ip Restriction'], + ['name' => 'delete-ip-restriction', 'module' => 'ip_restriction', 'label' => 'Delete Ip Restriction', 'description' => 'Delete Ip Restriction'], + + // Manage Career Page + ['name' => 'manage-career-page', 'module' => 'career', 'label' => 'Manage Career Page', 'description' => 'Manage Career Page'], + + // NOC Settings + ['name' => 'manage-noc', 'module' => 'settings', 'label' => 'Manage NOC', 'description' => 'Can manage NOC settings'], + ['name' => 'update-noc', 'module' => 'settings', 'label' => 'Update NOC', 'description' => 'Can update NOC templates'], + + // Joining Letter Settings + ['name' => 'manage-joining-letter', 'module' => 'settings', 'label' => 'Manage Joining Letter', 'description' => 'Can manage Joining Letter settings'], + ['name' => 'update-joining-letter', 'module' => 'settings', 'label' => 'Update Joining Letter', 'description' => 'Can update Joining Letter templates'], + + // Experience Certificate Settings + ['name' => 'manage-experience-certificate', 'module' => 'settings', 'label' => 'Manage Experience Certificate', 'description' => 'Can manage Experience Certificate settings'], + ['name' => 'update-experience-certificate', 'module' => 'settings', 'label' => 'Update Experience Certificate', 'description' => 'Can update Experience Certificate templates'], + + // Contact management + ['name' => 'manage-contacts', 'module' => 'contacts', 'label' => 'Manage Contacts', 'description' => 'Can manage contacts'], + ['name' => 'view-contacts', 'module' => 'contacts', 'label' => 'View Contacts', 'description' => 'Can view contacts'], + ['name' => 'delete-contacts', 'module' => 'contacts', 'label' => 'Delete Contacts', 'description' => 'Can delete contacts'], + ['name' => 'update-contact-status', 'module' => 'contacts', 'label' => 'Update Status Contacts', 'description' => 'Can update contact status'], + ['name' => 'send-reply-contacts', 'module' => 'contacts', 'label' => 'Send Reply Contacts', 'description' => 'Can send reply to contacts'], + + // Newsletter management + ['name' => 'manage-newsletters', 'module' => 'newsletters', 'label' => 'Manage Newsletters', 'description' => 'Can manage newsletter subscriptions'], + ['name' => 'delete-newsletters', 'module' => 'newsletters', 'label' => 'Delete Newsletters', 'description' => 'Can delete newsletter subscriptions'], + + // Login History management + ['name' => 'manage-login-history', 'module' => 'login_history', 'label' => 'Manage Login History', 'description' => 'Can manage login history'], + ['name' => 'show-login-history', 'module' => 'login_history', 'label' => 'Show Login History', 'description' => 'Can view login history'], + ['name' => 'delete-login-history', 'module' => 'login_history', 'label' => 'Delete Login History', 'description' => 'Can delete login history'], + + ]; + + foreach ($permissions as $permission) { + Permission::firstOrCreate( + ['name' => $permission['name'], 'guard_name' => 'web'], + [ + 'module' => $permission['module'], + 'label' => $permission['label'], + 'description' => $permission['description'], + ] + ); + } + } +} diff --git a/database/seeders/PlanOrderSeeder.php b/database/seeders/PlanOrderSeeder.php new file mode 100644 index 000000000..52b04acfd --- /dev/null +++ b/database/seeders/PlanOrderSeeder.php @@ -0,0 +1,55 @@ +take(8)->get(); + $plans = \App\Models\Plan::take(3)->get(); + $coupons = \App\Models\Coupon::where('status', true)->take(2)->get(); + + if ($users->isEmpty() || $plans->isEmpty()) { + return; + } + + foreach ($users as $index => $user) { + $plan = $plans->random(); + $coupon = $index % 3 === 0 ? $coupons->random() : null; + + $planOrder = new \App\Models\PlanOrder(); + $planOrder->user_id = $user->id; + $planOrder->plan_id = $plan->id; + $planOrder->calculatePrices($plan->price, $coupon); + $planOrder->ordered_at = now()->subDays(rand(1, 30)); + + // First 5 companies get approved status, others get pending/rejected + if ($index < 5) { + $planOrder->status = 'approved'; + $planOrder->processed_at = $planOrder->ordered_at->addHours(rand(1, 48)); + $planOrder->processed_by = \App\Models\User::where('type', 'superadmin')->first()?->id; + + // Set the plan for approved companies + $user->plan_id = $plan->id; + $user->plan_expire_date = now()->addDays($plan->duration); + $user->save(); + } else { + $planOrder->status = ['pending', 'rejected'][array_rand(['pending', 'rejected'])]; + if ($planOrder->status === 'rejected') { + $planOrder->processed_at = $planOrder->ordered_at->addHours(rand(1, 48)); + $planOrder->processed_by = \App\Models\User::where('type', 'superadmin')->first()?->id; + } + } + $planOrder->save(); + } + } + } +} diff --git a/database/seeders/PlanRequestSeeder.php b/database/seeders/PlanRequestSeeder.php new file mode 100644 index 000000000..825163512 --- /dev/null +++ b/database/seeders/PlanRequestSeeder.php @@ -0,0 +1,34 @@ +take(5)->get(); + $plans = Plan::take(3)->get(); + + if ($users->count() > 0 && $plans->count() > 0) { + foreach ($users as $user) { + PlanRequest::create([ + 'user_id' => $user->id, + 'plan_id' => $plans->random()->id, + 'status' => collect(['pending', 'approved', 'rejected'])->random(), + 'message' => 'I would like to upgrade my plan to access more features.', + ]); + } + } + } + } +} diff --git a/database/seeders/PlanSeeder.php b/database/seeders/PlanSeeder.php new file mode 100644 index 000000000..d6242152f --- /dev/null +++ b/database/seeders/PlanSeeder.php @@ -0,0 +1,75 @@ + 'Free', + 'price' => 0, + 'yearly_price' => 0, + 'duration' => 'monthly', + 'description' => 'Basic plan for small businesses just getting started.', + 'max_users' => 2, + 'max_employees' => 5, + 'enable_chatgpt' => 'off', + 'storage_limit' => 1, + 'is_trial' => null, + 'trial_day' => 0, + 'is_plan_enable' => 'on', + 'is_default' => true + ], + [ + 'name' => 'Starter', + 'price' => 19.99, + 'yearly_price' => 191.90, + 'duration' => 'monthly', + 'description' => 'Perfect for small businesses looking to grow their online presence.', + 'max_users' => 10, + 'max_employees' => 25, + 'enable_chatgpt' => 'off', + 'storage_limit' => 5, + 'is_trial' => 'on', + 'trial_day' => 7, + 'is_plan_enable' => 'on', + 'is_default' => false + ], + [ + 'name' => 'Pro', + 'price' => 49.99, + 'yearly_price' => 479.90, + 'duration' => 'monthly', + 'description' => 'Ideal for growing businesses with multiple stores and advanced needs.', + 'max_users' => 50, + 'max_employees' => 100, + 'enable_chatgpt' => 'on', + 'storage_limit' => 50, + 'is_trial' => 'on', + 'trial_day' => 14, + 'is_plan_enable' => 'on', + 'is_default' => false + ] + ]; + + foreach ($plans as $planData) { + // Check if plan with this name already exists + $existingPlan = Plan::where('name', $planData['name'])->first(); + + if (!$existingPlan) { + Plan::create($planData); + } + } + } + } +} diff --git a/database/seeders/PromotionSeeder.php b/database/seeders/PromotionSeeder.php new file mode 100644 index 000000000..34dc3eb71 --- /dev/null +++ b/database/seeders/PromotionSeeder.php @@ -0,0 +1,107 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Promotion reasons based on performance + $promotionReasons = [ + 'Exceptional performance and consistent achievement of targets over the past year', + 'Demonstrated strong leadership qualities and successfully managed team projects', + 'Outstanding contribution to company growth and revenue generation', + 'Excellent technical skills and innovative approach to problem-solving', + 'Consistent high-quality work delivery and meeting all project deadlines', + 'Strong communication skills and effective collaboration across departments', + 'Proactive approach in identifying process improvements and cost-saving initiatives', + 'Mentoring junior team members and contributing to their professional development', + 'Successfully completed advanced training and acquired new certifications', + 'Exceeded performance expectations and received positive client feedback', + 'Demonstrated reliability, dedication, and commitment to organizational values', + 'Led successful implementation of new systems and processes' + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get designations for this company + $designations = Designation::where('created_by', $company->id)->get(); + + if ($designations->isEmpty()) { + $this->command->warn('No designations found for company: ' . $company->name . '. Please run DesignationSeeder first.'); + continue; + } + + // Create 5-10 promotions for this company + $promotionCount = rand(5, 7); + + for ($i = 0; $i < $promotionCount; $i++) { + $employee = $employees->take(7)->random(); + $newDesignation = $designations->random(); + + // Get employee's current designation from employee table + $employeeRecord = Employee::where('user_id', $employee->id)->first(); + $currentDesignation = null; + + if ($employeeRecord && $employeeRecord->designation_id) { + $currentDesignationRecord = Designation::find($employeeRecord->designation_id); + $currentDesignation = $currentDesignationRecord ? $currentDesignationRecord->name : 'Previous Position'; + } else { + $currentDesignation = 'Previous Position'; + } + + $promotionDate = $faker->dateTimeBetween('-1 year', 'now'); + $effectiveDate = $faker->dateTimeBetween($promotionDate, '+1 month'); + + try { + Promotion::create([ + 'employee_id' => $employee->id, + 'previous_designation' => $currentDesignation, + 'designation_id' => $newDesignation->id, + 'promotion_date' => $promotionDate->format('Y-m-d'), + 'effective_date' => $effectiveDate->format('Y-m-d'), + 'salary_adjustment' => $faker->optional(0.8)->randomFloat(2, 5000, 50000), + 'reason' => $faker->randomElement($promotionReasons), + 'document' => randomImage(), + 'status' => $faker->randomElement(['pending', 'approved', 'rejected']), + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create promotion for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Promotion seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/ReferralSeeder.php b/database/seeders/ReferralSeeder.php new file mode 100644 index 000000000..e9526351b --- /dev/null +++ b/database/seeders/ReferralSeeder.php @@ -0,0 +1,61 @@ +get(); + $plans = Plan::take(3)->get(); + + if ($users->isEmpty() || $plans->isEmpty()) { + $this->command->warn('No users or plans found. Please seed users and plans first.'); + return; + } + + $referrals = [ + [ + 'user_id' => $users->first()->id, + 'company_id' => $users->skip(1)->first()->id, + 'commission_percentage' => 10.00, + 'amount' => 19.99, + 'plan_id' => $plans->first()->id + ], + [ + 'user_id' => $users->skip(1)->first()->id, + 'company_id' => $users->skip(2)->first()->id, + 'commission_percentage' => 15.00, + 'amount' => 49.99, + 'plan_id' => $plans->skip(1)->first()->id + ], + [ + 'user_id' => $users->skip(2)->first()->id, + 'company_id' => $users->skip(3)->first()->id, + 'commission_percentage' => 12.50, + 'amount' => 99.99, + 'plan_id' => $plans->last()->id + ], + [ + 'user_id' => $users->skip(3)->first()->id, + 'company_id' => $users->last()->id, + 'commission_percentage' => 8.00, + 'amount' => 19.99, + 'plan_id' => $plans->first()->id + ] + ]; + + foreach ($referrals as $referralData) { + Referral::create($referralData); + } + + $this->command->info('Referrals seeded successfully!'); + } + } +} diff --git a/database/seeders/ReferralSettingSeeder.php b/database/seeders/ReferralSettingSeeder.php new file mode 100644 index 000000000..3d3d4d89f --- /dev/null +++ b/database/seeders/ReferralSettingSeeder.php @@ -0,0 +1,21 @@ + true, + 'commission_percentage' => 10.00, + 'threshold_amount' => 50.00, + 'guidelines' => 'Welcome to our referral program! Earn commission when users sign up using your referral link and purchase a plan. Commission is calculated based on the plan price and will be available for payout once you reach the minimum threshold.', + ]); + } + } +} diff --git a/database/seeders/ResignationSeeder.php b/database/seeders/ResignationSeeder.php new file mode 100644 index 000000000..331e7673b --- /dev/null +++ b/database/seeders/ResignationSeeder.php @@ -0,0 +1,126 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Resignation reasons + $resignationReasons = [ + 'Better Career Opportunity', + 'Higher Salary Package', + 'Personal Reasons', + 'Family Relocation', + 'Health Issues', + 'Further Studies', + 'Career Change', + 'Work-Life Balance', + 'Company Culture Mismatch', + 'Lack of Growth Opportunities', + 'Starting Own Business', + 'Remote Work Preference' + ]; + + // Resignation descriptions based on reasons + $resignationDescriptions = [ + 'Better Career Opportunity' => 'I have received an offer that aligns better with my career goals and provides opportunities for professional growth in my field of expertise.', + 'Higher Salary Package' => 'I have been offered a position with significantly better compensation package that will help me meet my financial commitments and career aspirations.', + 'Personal Reasons' => 'Due to personal circumstances that require my immediate attention, I need to step away from my current role to focus on family matters.', + 'Family Relocation' => 'My family is relocating to another city/country, and I need to resign from my current position to accompany them and settle in the new location.', + 'Health Issues' => 'Due to health concerns that require extended treatment and recovery time, I am unable to continue in my current role and need to focus on my well-being.', + 'Further Studies' => 'I have been accepted into a full-time academic program that will enhance my qualifications and require my complete dedication to studies.', + 'Career Change' => 'I have decided to pursue a different career path that aligns better with my interests and long-term professional objectives.', + 'Work-Life Balance' => 'I am seeking a role that offers better work-life balance to spend more quality time with my family and pursue personal interests.', + 'Company Culture Mismatch' => 'After careful consideration, I feel that my values and working style do not align well with the current organizational culture.', + 'Lack of Growth Opportunities' => 'I feel that I have reached a plateau in my current role and there are limited opportunities for advancement and skill development.', + 'Starting Own Business' => 'I have decided to pursue entrepreneurship and start my own business venture, which requires my full-time commitment and attention.', + 'Remote Work Preference' => 'I am seeking opportunities that offer remote work flexibility, which is important for my current life situation and productivity.' + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get managers/HR for approval + $approvers = User::whereIn('type', ['manager', 'hr']) + ->where('created_by', $company->id) + ->get(); + + // Create 3-6 resignations for this company + $resignationCount = rand(3, 6); + + for ($i = 0; $i < $resignationCount; $i++) { + $employee = $employees->take(7)->random(); + $reason = $faker->randomElement($resignationReasons); + $description = $resignationDescriptions[$reason]; + + $resignationDate = $faker->dateTimeBetween('-6 months', 'now'); + $noticePeriod = $faker->randomElement(['30 days', '60 days', '90 days', '2 weeks', '1 month']); + + // Calculate last working day based on notice period + $noticeDays = match ($noticePeriod) { + '2 weeks' => 14, + '30 days', '1 month' => 30, + '60 days' => 60, + '90 days' => 90, + default => 30 + }; + + $lastWorkingDay = (clone $resignationDate)->modify("+{$noticeDays} days"); + + $status = $faker->randomElement(['pending', 'approved', 'rejected', 'completed']); + $approver = $approvers->isNotEmpty() ? $approvers->random() : null; + + try { + Resignation::create([ + 'employee_id' => $employee->id, + 'resignation_date' => $resignationDate->format('Y-m-d'), + 'last_working_day' => $lastWorkingDay->format('Y-m-d'), + 'notice_period' => $noticePeriod, + 'reason' => $reason, + 'description' => $description, + 'status' => $status, + 'documents' => randomImage(), + 'approved_by' => $status === 'approved' || $status === 'completed' ? $approver?->id : null, + 'approved_at' => $status === 'approved' || $status === 'completed' ? $faker->dateTimeBetween($resignationDate, 'now') : null, + 'exit_feedback' => $status === 'completed' ? $faker->sentence(15) : null, + 'exit_interview_conducted' => $status === 'completed' ? $faker->boolean(80) : false, + 'exit_interview_date' => $status === 'completed' && $faker->boolean(80) ? $lastWorkingDay->format('Y-m-d') : null, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create resignation for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Resignation seeder completed successfully!'); + } +} diff --git a/database/seeders/ReviewCycleSeeder.php b/database/seeders/ReviewCycleSeeder.php new file mode 100644 index 000000000..b14f3e582 --- /dev/null +++ b/database/seeders/ReviewCycleSeeder.php @@ -0,0 +1,100 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed review cycles for consistent data + $reviewCycles = [ + [ + 'name' => 'Monthly Performance Review', + 'frequency' => 'Monthly', + 'description' => 'Monthly performance evaluation to track progress, provide feedback, and address immediate performance concerns', + 'status' => 'active' + ], + [ + 'name' => 'Quarterly Business Review', + 'frequency' => 'Quarterly', + 'description' => 'Comprehensive quarterly assessment of employee performance, goal achievement, and development planning', + 'status' => 'active' + ], + [ + 'name' => 'Mid-Year Performance Review', + 'frequency' => 'Semi-Annual', + 'description' => 'Semi-annual performance evaluation focusing on goal progress, skill development, and career planning', + 'status' => 'active' + ], + [ + 'name' => 'Annual Performance Appraisal', + 'frequency' => 'Annual', + 'description' => 'Comprehensive annual performance review including goal assessment, competency evaluation, and career development planning', + 'status' => 'active' + ], + [ + 'name' => 'Probationary Review', + 'frequency' => 'Quarterly', + 'description' => 'Performance evaluation for employees during probationary period to assess job fit and performance standards', + 'status' => 'active' + ], + [ + 'name' => 'Project Completion Review', + 'frequency' => 'Monthly', + 'description' => 'Performance assessment conducted upon completion of major projects to evaluate contribution and outcomes', + 'status' => 'active' + ], + [ + 'name' => 'Leadership Assessment Cycle', + 'frequency' => 'Semi-Annual', + 'description' => 'Specialized review cycle for leadership positions focusing on management effectiveness and strategic contribution', + 'status' => 'active' + ], + [ + 'name' => 'Sales Performance Review', + 'frequency' => 'Quarterly', + 'description' => 'Performance evaluation specifically designed for sales team members focusing on targets and customer relationships', + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($reviewCycles as $cycleData) { + // Check if review cycle already exists for this company + if (ReviewCycle::where('name', $cycleData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + ReviewCycle::create([ + 'name' => $cycleData['name'], + 'frequency' => $cycleData['frequency'], + 'description' => $cycleData['description'], + 'status' => $cycleData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create review cycle: ' . $cycleData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('ReviewCycle seeder completed successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php new file mode 100644 index 000000000..faef80fc9 --- /dev/null +++ b/database/seeders/RoleSeeder.php @@ -0,0 +1,765 @@ + 'superadmin', 'guard_name' => 'web'], + ['label' => 'Super Admin', 'description' => 'Super Admin has full access to all features'] + ); + $superAdminRole->syncPermissions(Permission::all()); + } + + // Create Company role + $companyRole = Role::firstOrCreate( + ['name' => 'company', 'guard_name' => 'web'], + ['label' => 'Company', 'description' => 'Company has access to manage business'] + ); + + // Define company permissions + $companyPermissions = $this->getCompanyPermissions(); + $companyRole->syncPermissions(Permission::whereIn('name', $companyPermissions)->get()); + } + + private function getCompanyPermissions(): array + { + $basePermissions = [ + // Core system + 'manage-dashboard', + 'view-dashboard', + 'manage-users', + 'manage-any-users', + 'create-users', + 'edit-users', + 'delete-users', + 'view-users', + 'reset-password-users', + 'toggle-status-users', + 'manage-roles', + 'manage-any-roles', + 'create-roles', + 'edit-roles', + 'delete-roles', + 'view-roles', + 'view-permissions', + 'manage-email-settings', + 'manage-brand-settings', + 'manage-webhook-settings', + 'manage-settings', + 'manage-media', + 'manage-own-media', + 'create-media', + 'edit-media', + 'delete-media', + 'view-media', + 'download-media', + 'manage-media-directories', + 'manage-any-media-directories', + 'manage-own-media-directories', + 'create-media-directories', + 'edit-media-directories', + 'delete-media-directories', + 'manage-calendar', + 'view-calendar', + 'manage-language', + 'edit-language', + 'view-language', + 'view-landing-page', + + // Organization structure + 'manage-branches', + 'manage-any-branches', + 'manage-own-branches', + 'view-branches', + 'create-branches', + 'edit-branches', + 'delete-branches', + 'toggle-status-branches', + 'manage-departments', + 'manage-any-departments', + 'manage-own-departments', + 'view-departments', + 'create-departments', + 'edit-departments', + 'delete-departments', + 'toggle-status-departments', + 'manage-designations', + 'manage-any-designations', + 'manage-own-designations', + 'view-designations', + 'create-designations', + 'edit-designations', + 'delete-designations', + 'toggle-status-designations', + 'manage-document-types', + 'manage-any-document-types', + 'manage-own-document-types', + 'view-document-types', + 'create-document-types', + 'edit-document-types', + 'delete-document-types', + 'manage-employees', + 'manage-any-employees', + 'manage-own-employees', + 'view-employees', + 'create-employees', + 'edit-employees', + 'delete-employees', + + // Employee lifecycle + 'manage-award-types', + 'manage-any-award-types', + 'manage-own-award-types', + 'view-award-types', + 'create-award-types', + 'edit-award-types', + 'delete-award-types', + 'manage-awards', + 'manage-any-awards', + 'manage-own-awards', + 'view-awards', + 'create-awards', + 'edit-awards', + 'delete-awards', + 'manage-promotions', + 'manage-any-promotions', + 'manage-own-promotions', + 'view-promotions', + 'create-promotions', + 'edit-promotions', + 'delete-promotions', + 'approve-promotions', + 'reject-promotions', + 'manage-resignations', + 'manage-any-resignations', + 'manage-own-resignations', + 'view-resignations', + 'create-resignations', + 'edit-resignations', + 'delete-resignations', + 'approve-resignations', + 'reject-resignations', + 'manage-terminations', + 'manage-any-terminations', + 'manage-own-terminations', + 'view-terminations', + 'create-terminations', + 'edit-terminations', + 'delete-terminations', + 'approve-terminations', + 'reject-terminations', + 'manage-warnings', + 'manage-any-warnings', + 'manage-own-warnings', + 'view-warnings', + 'create-warnings', + 'edit-warnings', + 'delete-warnings', + 'approve-warnings', + 'acknowledge-warnings', + 'manage-trips', + 'manage-any-trips', + 'manage-own-trips', + 'view-trips', + 'create-trips', + 'edit-trips', + 'delete-trips', + 'approve-trips', + 'manage-trip-expenses', + 'approve-trip-expenses', + 'manage-complaints', + 'manage-any-complaints', + 'manage-own-complaints', + 'view-complaints', + 'create-complaints', + 'edit-complaints', + 'delete-complaints', + 'assign-complaints', + 'resolve-complaints', + 'manage-employee-transfers', + 'manage-any-employee-transfers', + 'manage-own-employee-transfers', + 'view-employee-transfers', + 'create-employee-transfers', + 'edit-employee-transfers', + 'delete-employee-transfers', + 'approve-employee-transfers', + 'reject-employee-transfers', + 'manage-holidays', + 'manage-any-holidays', + 'manage-own-holidays', + 'view-holidays', + 'create-holidays', + 'edit-holidays', + 'delete-holidays', + 'manage-announcements', + 'manage-any-announcements', + 'manage-own-announcements', + 'view-announcements', + 'create-announcements', + 'edit-announcements', + 'delete-announcements', + + // Assets + 'manage-asset-types', + 'manage-any-asset-types', + 'manage-own-asset-types', + 'view-asset-types', + 'create-asset-types', + 'edit-asset-types', + 'delete-asset-types', + 'manage-assets', + 'manage-any-assets', + 'manage-own-assets', + 'view-assets', + 'create-assets', + 'edit-assets', + 'delete-assets', + 'assign-assets', + 'manage-asset-maintenance', + 'export-assets', + 'import-assets', + + // Training + 'manage-training-types', + 'manage-any-training-types', + 'manage-own-training-types', + 'view-training-types', + 'create-training-types', + 'edit-training-types', + 'delete-training-types', + 'manage-training-programs', + 'manage-any-training-programs', + 'manage-own-training-programs', + 'view-training-programs', + 'create-training-programs', + 'edit-training-programs', + 'delete-training-programs', + 'manage-training-sessions', + 'manage-any-training-sessions', + 'manage-own-training-sessions', + 'view-training-sessions', + 'create-training-sessions', + 'edit-training-sessions', + 'delete-training-sessions', + 'manage-attendance', + 'manage-employee-trainings', + 'manage-any-employee-trainings', + 'manage-own-employee-trainings', + 'view-employee-trainings', + 'create-employee-trainings', + 'edit-employee-trainings', + 'delete-employee-trainings', + 'assign-trainings', + 'manage-assessments', + 'record-assessment-results', + + // Performance + 'manage-performance-indicator-categories', + 'manage-any-performance-indicator-categories', + 'manage-own-performance-indicator-categories', + 'view-performance-indicator-categories', + 'create-performance-indicator-categories', + 'edit-performance-indicator-categories', + 'delete-performance-indicator-categories', + 'manage-performance-indicators', + 'manage-any-performance-indicators', + 'manage-own-performance-indicators', + 'view-performance-indicators', + 'create-performance-indicators', + 'edit-performance-indicators', + 'delete-performance-indicators', + 'manage-goal-types', + 'manage-any-goal-types', + 'manage-own-goal-types', + 'view-goal-types', + 'create-goal-types', + 'edit-goal-types', + 'delete-goal-types', + 'manage-employee-goals', + 'manage-any-employee-goals', + 'manage-own-employee-goals', + 'view-employee-goals', + 'create-employee-goals', + 'edit-employee-goals', + 'delete-employee-goals', + 'manage-review-cycles', + 'manage-any-review-cycles', + 'manage-own-review-cycles', + 'view-review-cycles', + 'create-review-cycles', + 'edit-review-cycles', + 'delete-review-cycles', + 'manage-review-templates', + 'view-review-templates', + 'create-review-templates', + 'edit-review-templates', + 'delete-review-templates', + 'manage-employee-reviews', + 'manage-any-employee-reviews', + 'manage-own-employee-reviews', + 'view-employee-reviews', + 'create-employee-reviews', + 'edit-employee-reviews', + 'delete-employee-reviews', + + // Recruitment + 'manage-job-categories', + 'manage-any-job-categories', + 'manage-own-job-categories', + 'view-job-categories', + 'create-job-categories', + 'edit-job-categories', + 'delete-job-categories', + 'manage-job-requisitions', + 'manage-any-job-requisitions', + 'manage-own-job-requisitions', + 'view-job-requisitions', + 'create-job-requisitions', + 'edit-job-requisitions', + 'delete-job-requisitions', + 'approve-job-requisitions', + 'manage-job-types', + 'manage-any-job-types', + 'manage-own-job-types', + 'view-job-types', + 'create-job-types', + 'edit-job-types', + 'delete-job-types', + 'manage-job-locations', + 'manage-any-job-locations', + 'manage-own-job-locations', + 'view-job-locations', + 'create-job-locations', + 'edit-job-locations', + 'delete-job-locations', + 'manage-job-postings', + 'manage-any-job-postings', + 'manage-own-job-postings', + 'view-job-postings', + 'create-job-postings', + 'edit-job-postings', + 'delete-job-postings', + 'publish-job-postings', + 'manage-candidate-sources', + 'manage-any-candidate-sources', + 'manage-own-candidate-sources', + 'view-candidate-sources', + 'create-candidate-sources', + 'edit-candidate-sources', + 'delete-candidate-sources', + 'manage-candidates', + 'manage-any-candidates', + 'manage-own-candidates', + 'view-candidates', + 'convert-to-employee', + // 'create-candidates', + 'edit-candidates', + 'delete-candidates', + 'manage-interview-types', + 'manage-any-interview-types', + 'manage-own-interview-types', + 'view-interview-types', + 'create-interview-types', + 'edit-interview-types', + 'delete-interview-types', + 'manage-interview-rounds', + 'manage-any-interview-rounds', + 'manage-own-interview-rounds', + 'view-interview-rounds', + 'create-interview-rounds', + 'edit-interview-rounds', + 'delete-interview-rounds', + 'manage-interviews', + 'manage-any-interviews', + 'manage-own-interviews', + 'view-interviews', + 'create-interviews', + 'edit-interviews', + 'delete-interviews', + 'manage-interview-feedback', + 'manage-any-interview-feedback', + 'manage-own-interview-feedback', + 'view-interview-feedback', + 'create-interview-feedback', + 'edit-interview-feedback', + 'delete-interview-feedback', + 'manage-custom-questions', + 'manage-any-custom-questions', + 'manage-own-custom-questions', + 'view-custom-questions', + 'create-custom-questions', + 'edit-custom-questions', + 'delete-custom-questions', + 'manage-candidate-assessments', + 'manage-any-candidate-assessments', + 'manage-own-candidate-assessments', + 'view-candidate-assessments', + 'create-candidate-assessments', + 'edit-candidate-assessments', + 'delete-candidate-assessments', + 'manage-offer-templates', + 'manage-any-offer-templates', + 'manage-own-offer-templates', + 'view-offer-templates', + 'create-offer-templates', + 'edit-offer-templates', + 'delete-offer-templates', + 'manage-offers', + 'manage-any-offers', + 'manage-own-offers', + 'view-offers', + 'create-offers', + 'edit-offers', + 'delete-offers', + 'approve-offers', + 'manage-onboarding-checklists', + 'manage-any-onboarding-checklists', + 'manage-own-onboarding-checklists', + 'view-onboarding-checklists', + 'create-onboarding-checklists', + 'edit-onboarding-checklists', + 'delete-onboarding-checklists', + 'manage-checklist-items', + 'manage-any-checklist-items', + 'manage-own-checklist-items', + 'view-checklist-items', + 'create-checklist-items', + 'edit-checklist-items', + 'delete-checklist-items', + 'manage-candidate-onboarding', + 'manage-any-candidate-onboarding', + 'manage-own-candidate-onboarding', + 'manage-candidate-onboarding-status', + 'view-candidate-onboarding', + 'create-candidate-onboarding', + 'edit-candidate-onboarding', + 'delete-candidate-onboarding', + + // Meetings + 'manage-meeting-types', + 'manage-any-meeting-types', + 'manage-own-meeting-types', + 'view-meeting-types', + 'create-meeting-types', + 'edit-meeting-types', + 'delete-meeting-types', + 'manage-meeting-rooms', + 'manage-any-meeting-rooms', + 'manage-own-meeting-rooms', + 'view-meeting-rooms', + 'create-meeting-rooms', + 'edit-meeting-rooms', + 'delete-meeting-rooms', + 'manage-meetings', + 'manage-any-meetings', + 'manage-own-meetings', + 'view-meetings', + 'create-meetings', + 'edit-meetings', + 'delete-meetings', + 'manage-meeting-status', + 'manage-meeting-attendees', + 'manage-any-meeting-attendees', + 'manage-own-meeting-attendees', + 'view-meeting-attendees', + 'create-meeting-attendees', + 'edit-meeting-attendees', + 'delete-meeting-attendees', + 'manage-meeting-rsvp-status', + 'manage-meeting-attendance', + 'manage-meeting-minutes', + 'manage-any-meeting-minutes', + 'manage-own-meeting-minutes', + 'view-meeting-minutes', + 'create-meeting-minutes', + 'edit-meeting-minutes', + 'delete-meeting-minutes', + 'manage-action-items', + 'manage-any-action-items', + 'manage-own-action-items', + 'view-action-items', + 'create-action-items', + 'edit-action-items', + 'delete-action-items', + + // Contracts & Documents + 'manage-contract-types', + 'manage-any-contract-types', + 'manage-own-contract-types', + 'view-contract-types', + 'create-contract-types', + 'edit-contract-types', + 'delete-contract-types', + 'manage-employee-contracts', + 'manage-any-employee-contracts', + 'manage-own-employee-contracts', + 'view-employee-contracts', + 'create-employee-contracts', + 'edit-employee-contracts', + 'delete-employee-contracts', + 'approve-employee-contracts', + 'reject-employee-contracts', + 'manage-contract-renewals', + 'manage-any-contract-renewals', + 'manage-own-contract-renewals', + 'view-contract-renewals', + 'create-contract-renewals', + 'edit-contract-renewals', + 'delete-contract-renewals', + 'approve-contract-renewals', + 'reject-contract-renewals', + 'manage-contract-templates', + 'manage-any-contract-templates', + 'manage-own-contract-templates', + 'view-contract-templates', + 'create-contract-templates', + 'edit-contract-templates', + 'delete-contract-templates', + 'manage-document-categories', + 'manage-any-document-categories', + 'manage-own-document-categories', + 'view-document-categories', + 'create-document-categories', + 'edit-document-categories', + 'delete-document-categories', + 'manage-hr-documents', + 'manage-any-hr-documents', + 'manage-own-hr-documents', + 'view-hr-documents', + 'create-hr-documents', + 'edit-hr-documents', + 'delete-hr-documents', + 'manage-document-acknowledgments', + 'manage-any-document-acknowledgments', + 'manage-own-document-acknowledgments', + 'view-document-acknowledgments', + 'create-document-acknowledgments', + 'edit-document-acknowledgments', + 'delete-document-acknowledgments', + 'acknowledge-document-acknowledgments', + 'manage-document-templates', + 'manage-any-document-templates', + 'manage-own-document-templates', + 'view-document-templates', + 'create-document-templates', + 'edit-document-templates', + 'delete-document-templates', + + // Leave & Attendance + 'manage-leave-types', + 'manage-any-leave-types', + 'manage-own-leave-types', + 'view-leave-types', + 'create-leave-types', + 'edit-leave-types', + 'delete-leave-types', + 'manage-leave-policies', + 'manage-any-leave-policies', + 'manage-own-leave-policies', + 'view-leave-policies', + 'create-leave-policies', + 'edit-leave-policies', + 'delete-leave-policies', + 'manage-leave-applications', + 'manage-any-leave-applications', + 'manage-own-leave-applications', + 'view-leave-applications', + 'create-leave-applications', + 'edit-leave-applications', + 'delete-leave-applications', + 'approve-leave-applications', + 'reject-leave-applications', + 'manage-leave-balances', + 'manage-any-leave-balances', + 'manage-own-leave-balances', + 'view-leave-balances', + 'create-leave-balances', + 'edit-leave-balances', + 'delete-leave-balances', + 'adjust-leave-balances', + 'manage-shifts', + 'manage-any-shifts', + 'manage-own-shifts', + 'view-shifts', + 'create-shifts', + 'edit-shifts', + 'delete-shifts', + 'manage-attendance-policies', + 'manage-any-attendance-policies', + 'manage-own-attendance-policies', + 'view-attendance-policies', + 'create-attendance-policies', + 'edit-attendance-policies', + 'delete-attendance-policies', + 'manage-attendance-records', + 'manage-any-attendance-records', + 'manage-own-attendance-records', + 'view-attendance-records', + 'create-attendance-records', + 'edit-attendance-records', + 'delete-attendance-records', + 'import-attendance-record', + 'export-attendance-record', + 'clock-in-out', + 'manage-attendance-regularizations', + 'manage-any-attendance-regularizations', + 'manage-own-attendance-regularizations', + 'view-attendance-regularizations', + 'create-attendance-regularizations', + 'edit-attendance-regularizations', + 'delete-attendance-regularizations', + 'approve-attendance-regularizations', + 'reject-attendance-regularizations', + 'manage-time-entries', + 'manage-any-time-entries', + 'manage-own-time-entries', + 'view-time-entries', + 'create-time-entries', + 'edit-time-entries', + 'delete-time-entries', + 'import-time-entry', + 'export-time-entry', + 'approve-time-entries', + 'reject-time-entries', + + // Payroll + 'manage-salary-components', + 'manage-any-salary-components', + 'manage-own-salary-components', + 'view-salary-components', + 'create-salary-components', + 'edit-salary-components', + 'delete-salary-components', + 'manage-employee-salaries', + 'manage-any-employee-salaries', + 'manage-own-employee-salaries', + 'view-employee-salaries', + 'create-employee-salaries', + 'edit-employee-salaries', + 'delete-employee-salaries', + 'manage-payroll-runs', + 'manage-any-payroll-runs', + 'manage-own-payroll-runs', + 'view-payroll-runs', + 'create-payroll-runs', + 'edit-payroll-runs', + 'delete-payroll-runs', + 'process-payroll-runs', + 'import-payroll-runs', + 'export-payroll-runs', + 'manage-payslips', + 'manage-any-payslips', + 'manage-own-payslips', + 'view-payslips', + 'create-payslips', + 'download-payslips', + 'send-payslips', + + // Manage Working day + 'manage-working-days-settings', + 'update-working-days-settings', + + // Biomatric Attedance + 'manage-biomatric-attedance-settings', + 'manage-biometric-attendance', + 'sync-biometric-attendance', + 'view-biometric-attendance', + + // Ip Restriction + 'manage-ip-restriction-settings', + 'create-ip-restriction', + 'edit-ip-restriction', + 'delete-ip-restriction', + + // career page + 'manage-career-page', + + // Login History management + 'manage-login-history', + 'show-login-history', + 'delete-login-history', + + // Noc Template Permissions + 'manage-noc', + 'update-noc', + + // Joining Letter Template Permissions + 'manage-joining-letter', + 'update-joining-letter', + + // Experience Certificate Template Permissions + 'manage-experience-certificate', + 'update-experience-certificate', + + // Download Certificate permission + 'download-joining-letter', + 'download-experience-certificate', + 'download-noc-certificate', + + // Payroll Entries Permission + 'delete-payroll-entries', + + // Employee Import/Export Permissions + 'import-employee', + 'export-employee', + 'export-leave-applications', + + ]; + + // Add SaaS-specific permissions + if (isSaas()) { + $saasPermissions = [ + 'manage-plans', + 'manage-plan-requests', + 'manage-plan-orders', + 'view-plan-requests', + 'view-plan-orders', + 'view-plans', + 'request-plans', + 'trial-plans', + 'subscribe-plans', + 'manage-referral', + 'manage-users-referral', + 'manage-payout-referral', + 'manage-analytics', + ]; + $basePermissions = array_merge($basePermissions, $saasPermissions); + } else { + $nonSaasExtraPermissions = [ + 'manage-landing-page', + 'view-landing-page', + 'edit-landing-page', + 'manage-currencies', + 'manage-any-currencies', + 'manage-own-currencies', + 'view-currencies', + 'create-currencies', + 'edit-currencies', + 'delete-currencies', + 'manage-newsletters', + 'delete-newsletters', + 'manage-contacts', + 'view-contacts', + 'update-contact-status', + 'delete-contacts', + 'send-reply-contacts', + ]; + $basePermissions = array_merge($basePermissions, $nonSaasExtraPermissions); + } + + return $basePermissions; + } +} diff --git a/database/seeders/SalaryComponentSeeder.php b/database/seeders/SalaryComponentSeeder.php new file mode 100644 index 000000000..c987dc749 --- /dev/null +++ b/database/seeders/SalaryComponentSeeder.php @@ -0,0 +1,169 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed salary components for consistent data + $salaryComponents = [ + // Earnings + [ + 'name' => 'House Rent Allowance (HRA)', + 'description' => 'House rent allowance for accommodation expenses', + 'type' => 'earning', + 'calculation_type' => 'percentage', + 'default_amount' => 0.00, + 'percentage_of_basic' => 40.00, + 'is_taxable' => true, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Transport Allowance', + 'description' => 'Transportation allowance for commuting expenses', + 'type' => 'earning', + 'calculation_type' => 'fixed', + 'default_amount' => 2000.00, + 'percentage_of_basic' => null, + 'is_taxable' => false, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Medical Allowance', + 'description' => 'Medical allowance for healthcare expenses', + 'type' => 'earning', + 'calculation_type' => 'fixed', + 'default_amount' => 1500.00, + 'percentage_of_basic' => null, + 'is_taxable' => false, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Dearness Allowance (DA)', + 'description' => 'Dearness allowance to offset inflation impact', + 'type' => 'earning', + 'calculation_type' => 'percentage', + 'default_amount' => 0.00, + 'percentage_of_basic' => 15.00, + 'is_taxable' => true, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Special Allowance', + 'description' => 'Special allowance for additional responsibilities', + 'type' => 'earning', + 'calculation_type' => 'fixed', + 'default_amount' => 3000.00, + 'percentage_of_basic' => null, + 'is_taxable' => true, + 'is_mandatory' => false, + 'status' => 'active' + ], + // Deductions + [ + 'name' => 'Provident Fund (PF)', + 'description' => 'Employee provident fund contribution', + 'type' => 'deduction', + 'calculation_type' => 'percentage', + 'default_amount' => 0.00, + 'percentage_of_basic' => 12.00, + 'is_taxable' => false, + 'is_mandatory' => true, + 'status' => 'active' + ], + [ + 'name' => 'Employee State Insurance (ESI)', + 'description' => 'Employee state insurance contribution', + 'type' => 'deduction', + 'calculation_type' => 'percentage', + 'default_amount' => 0.00, + 'percentage_of_basic' => 0.75, + 'is_taxable' => false, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Professional Tax', + 'description' => 'Professional tax deduction as per state regulations', + 'type' => 'deduction', + 'calculation_type' => 'fixed', + 'default_amount' => 200.00, + 'percentage_of_basic' => null, + 'is_taxable' => false, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Income Tax (TDS)', + 'description' => 'Tax deducted at source on salary income', + 'type' => 'deduction', + 'calculation_type' => 'percentage', + 'default_amount' => 0.00, + 'percentage_of_basic' => 10.00, + 'is_taxable' => false, + 'is_mandatory' => false, + 'status' => 'active' + ], + [ + 'name' => 'Loan Deduction', + 'description' => 'Employee loan repayment deduction', + 'type' => 'deduction', + 'calculation_type' => 'fixed', + 'default_amount' => 1000.00, + 'percentage_of_basic' => null, + 'is_taxable' => false, + 'is_mandatory' => false, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($salaryComponents as $componentData) { + // Check if salary component already exists for this company + if (SalaryComponent::where('name', $componentData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + SalaryComponent::create([ + 'name' => $componentData['name'], + 'description' => $componentData['description'], + 'type' => $componentData['type'], + 'calculation_type' => $componentData['calculation_type'], + 'default_amount' => $componentData['default_amount'], + 'percentage_of_basic' => $componentData['percentage_of_basic'], + 'is_taxable' => $componentData['is_taxable'], + 'is_mandatory' => $componentData['is_mandatory'], + 'status' => $componentData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create salary component: ' . $componentData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + $this->command->info('SalaryComponent seeder completed successfully!'); + } +} diff --git a/database/seeders/ShiftSeeder.php b/database/seeders/ShiftSeeder.php new file mode 100644 index 000000000..a52236555 --- /dev/null +++ b/database/seeders/ShiftSeeder.php @@ -0,0 +1,114 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed shifts for consistent data + $shifts = [ + [ + 'name' => 'Morning Shift', + 'description' => 'Standard morning shift for regular office hours', + 'start_time' => '09:00:00', + 'end_time' => '18:00:00', + 'break_duration' => 60, + 'break_start_time' => '13:00:00', + 'break_end_time' => '14:00:00', + 'grace_period' => 15, + 'is_night_shift' => false, + 'status' => 'active' + ], + [ + 'name' => 'Evening Shift', + 'description' => 'Evening shift for extended business hours', + 'start_time' => '14:00:00', + 'end_time' => '23:00:00', + 'break_duration' => 60, + 'break_start_time' => '18:00:00', + 'break_end_time' => '19:00:00', + 'grace_period' => 15, + 'is_night_shift' => false, + 'status' => 'active' + ], + [ + 'name' => 'Night Shift', + 'description' => 'Night shift for 24/7 operations and support', + 'start_time' => '22:00:00', + 'end_time' => '07:00:00', + 'break_duration' => 60, + 'break_start_time' => '02:00:00', + 'break_end_time' => '03:00:00', + 'grace_period' => 15, + 'is_night_shift' => true, + 'status' => 'active' + ] + ]; + + foreach ($companies as $company) { + foreach ($shifts as $shiftData) { + // Check if shift already exists for this company + if (Shift::where('name', $shiftData['name'])->where('created_by', $company->id)->exists()) { + continue; + } + + try { + Shift::create([ + 'name' => $shiftData['name'], + 'description' => $shiftData['description'], + 'start_time' => $shiftData['start_time'], + 'end_time' => $shiftData['end_time'], + 'break_duration' => $shiftData['break_duration'], + 'break_start_time' => $shiftData['break_start_time'], + 'break_end_time' => $shiftData['break_end_time'], + 'grace_period' => $shiftData['grace_period'], + 'is_night_shift' => $shiftData['is_night_shift'], + 'status' => $shiftData['status'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create shift: ' . $shiftData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + + // Assign shifts to employees after creating shifts + foreach ($companies as $company) { + $companyShifts = Shift::where('created_by', $company->id)->get(); + $employeeUsers = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($companyShifts->isNotEmpty() && $employeeUsers->isNotEmpty()) { + // Assign Morning Shift to all employees by default + $morningShift = $companyShifts->where('name', 'Morning Shift')->first(); + if ($morningShift) { + foreach ($employeeUsers as $employeeUser) { + // Get employee record from employee table using user_id + $employee = \App\Models\Employee::where('user_id', $employeeUser->id)->first(); + if ($employee && !$employee->shift_id) { + $employee->update(['shift_id' => $morningShift->id]); + } + } + } + } + } + + $this->command->info('Shift seeder completed successfully!'); + } +} diff --git a/database/seeders/StaffRoleSeeder.php b/database/seeders/StaffRoleSeeder.php new file mode 100644 index 000000000..e78661116 --- /dev/null +++ b/database/seeders/StaffRoleSeeder.php @@ -0,0 +1,117 @@ +get(); + + if ($companyUsers->isEmpty()) { + $this->command->warn('No company users found. Please run CompanySeeder first.'); + return; + } + + // Define role templates with permissions + $roleTemplates = [ + [ + 'name' => 'manager', + 'label' => 'Manager', + 'description' => 'Manager has access to manage buissness', + 'permissions' => [ + 'manage-dashboard', 'view-dashboard', + 'manage-users', 'view-users', 'create-users', 'edit-users', 'reset-password-users', 'toggle-status-users', + 'manage-roles', 'view-roles', + 'manage-media', 'manage-own-media', 'view-media', 'create-media', 'edit-media', + 'manage-calendar', 'view-calendar', 'manage-appointments', 'manage-own-appointments', + ] + ], + [ + 'name' => 'contentcreator', + 'label' => 'Content Creator', + 'description' => 'Content Creator has access to manage buissness', + 'permissions' => [ + 'view-dashboard', + 'manage-own-media', 'view-media', 'create-media', 'edit-media', 'download-media', + ] + ], + [ + 'name' => 'supportagent', + 'label' => 'Support Agent', + 'description' => 'Support Agent has access to manage buissness', + 'permissions' => [ + 'view-dashboard', + ] + ] + ]; + + // Create roles and staff users for each company + foreach ($companyUsers as $company) { + // Create roles for each company + foreach ($roleTemplates as $roleTemplate) { + $role = Role::firstOrCreate([ + 'name' => $roleTemplate['name'], + 'label' => $roleTemplate['label'], + 'description' => $roleTemplate['description'], + 'guard_name' => 'web', + 'created_by' => $company->id + ]); + + // Get permissions for this role + $permissions = $roleTemplate['permissions']; + + // Get permission objects + $permissionObjects = Permission::whereIn('name', $permissions)->get(); + + // Assign permissions to role + $role->syncPermissions($permissionObjects); + + // Create 1-2 staff users for each role + $staffCount = rand(1, 2); + + for ($i = 0; $i < $staffCount; $i++) { + $firstName = $faker->firstName; + $lastName = $faker->lastName; + $name = $firstName . ' ' . $lastName; + $email = strtolower($firstName . '.' . $lastName . '.' . $company->id . '@example.com'); + + // Skip if user already exists + if (User::where('email', $email)->exists()) { + continue; + } + + // Create staff user + $staff = User::create([ + 'name' => $name, + 'email' => $email, + 'email_verified_at' => now(), + 'password' => Hash::make('password'), + 'type' => $roleTemplate['name'], + 'lang' => $faker->randomElement(['en', 'es', 'fr', 'de']), + 'created_by' => $company->id, + 'created_at' => $faker->dateTimeBetween('-6 months', 'now'), + ]); + + // Assign role to staff + $staff->assignRole($role); + } + } + } + + $this->command->info('Created staff roles and users successfully!'); + } +} \ No newline at end of file diff --git a/database/seeders/TerminationSeeder.php b/database/seeders/TerminationSeeder.php new file mode 100644 index 000000000..f5d98da20 --- /dev/null +++ b/database/seeders/TerminationSeeder.php @@ -0,0 +1,143 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Termination types and their reasons + $terminationData = [ + 'involuntary' => [ + 'reasons' => ['Performance Issues', 'Misconduct', 'Policy Violation', 'Attendance Issues', 'Insubordination'], + 'descriptions' => [ + 'Performance Issues' => 'Employee consistently failed to meet performance standards despite multiple warnings and performance improvement plans.', + 'Misconduct' => 'Employee engaged in serious misconduct that violated company policies and professional standards.', + 'Policy Violation' => 'Employee repeatedly violated company policies and procedures after receiving formal warnings and counseling.', + 'Attendance Issues' => 'Employee had excessive absenteeism and tardiness that negatively impacted work productivity and team performance.', + 'Insubordination' => 'Employee demonstrated insubordinate behavior and refused to follow direct instructions from supervisors.' + ] + ], + 'voluntary' => [ + 'reasons' => ['Mutual Agreement', 'End of Contract', 'Job Abandonment', 'Voluntary Separation'], + 'descriptions' => [ + 'Mutual Agreement' => 'Both employee and company mutually agreed to terminate the employment relationship due to changing business needs.', + 'End of Contract' => 'Employee\'s fixed-term contract has reached its natural expiration date and will not be renewed.', + 'Job Abandonment' => 'Employee abandoned their position by failing to report to work for consecutive days without proper notification.', + 'Voluntary Separation' => 'Employee requested voluntary separation as part of company restructuring or downsizing initiative.' + ] + ], + 'layoff' => [ + 'reasons' => ['Economic Downturn', 'Restructuring', 'Budget Cuts', 'Department Closure'], + 'descriptions' => [ + 'Economic Downturn' => 'Position eliminated due to economic challenges and reduced business operations requiring workforce reduction.', + 'Restructuring' => 'Role eliminated as part of organizational restructuring to improve efficiency and align with business strategy.', + 'Budget Cuts' => 'Position terminated due to budget constraints and cost reduction measures implemented by the organization.', + 'Department Closure' => 'Employee\'s department was closed due to strategic business decisions and operational changes.' + ] + ], + 'retirement' => [ + 'reasons' => ['Normal Retirement', 'Early Retirement', 'Medical Retirement'], + 'descriptions' => [ + 'Normal Retirement' => 'Employee reached the standard retirement age and chose to retire with full benefits and pension eligibility.', + 'Early Retirement' => 'Employee opted for early retirement package offered by the company with appropriate benefits and compensation.', + 'Medical Retirement' => 'Employee retired due to medical conditions that prevent them from continuing their work responsibilities.' + ] + ] + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get managers/HR for approval + $approvers = User::whereIn('type', ['manager', 'hr']) + ->where('created_by', $company->id) + ->get(); + + // Create 2-4 terminations for this company + $terminationCount = rand(2, 4); + + for ($i = 0; $i < $terminationCount; $i++) { + $employee = $employees->take(5)->random(); + + // Check if termination already exists for this employee + if (Termination::where('employee_id', $employee->id)->exists()) { + continue; + } + + $terminationType = $faker->randomElement(array_keys($terminationData)); + $typeData = $terminationData[$terminationType]; + $reason = $faker->randomElement($typeData['reasons']); + $description = $typeData['descriptions'][$reason]; + + $noticeDate = $faker->dateTimeBetween('-3 months', 'now'); + $noticePeriod = $faker->randomElement(['Immediate', '2 weeks', '30 days', '60 days']); + + // Calculate termination date based on notice period + $noticeDays = match ($noticePeriod) { + 'Immediate' => 0, + '2 weeks' => 14, + '30 days' => 30, + '60 days' => 60, + default => 0 + }; + + $terminationDate = (clone $noticeDate)->modify("+{$noticeDays} days"); + + $status = $faker->randomElement(['planned', 'in progress', 'completed']); + $approver = $approvers->isNotEmpty() ? $approvers->random() : null; + + try { + Termination::create([ + 'employee_id' => $employee->id, + 'termination_type' => $terminationType, + 'termination_date' => $terminationDate->format('Y-m-d'), + 'notice_date' => $noticeDate->format('Y-m-d'), + 'notice_period' => $noticePeriod, + 'reason' => $reason, + 'description' => $description, + 'status' => $status, + 'documents' => randomImage(), + 'approved_by' => $status === 'in progress' || $status === 'completed' ? $approver?->id : null, + 'approved_at' => $status === 'in progress' || $status === 'completed' ? $faker->dateTimeBetween($noticeDate, 'now') : null, + 'exit_interview_conducted' => $status === 'completed' ? $faker->boolean(70) : false, + 'exit_interview_date' => $status === 'completed' && $faker->boolean(70) ? $terminationDate->format('Y-m-d') : null, + 'exit_feedback' => $status === 'completed' ? $faker->sentence(12) : null, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create termination for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Termination seeder completed successfully!'); + } +} diff --git a/database/seeders/TimeEntrySeeder.php b/database/seeders/TimeEntrySeeder.php new file mode 100644 index 000000000..43ee15ef5 --- /dev/null +++ b/database/seeders/TimeEntrySeeder.php @@ -0,0 +1,132 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found.'); + return; + } + + $currentYear = date('Y'); + + foreach ($companies as $company) { + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + continue; + } + + // Process May to August 2025 + for ($month = 5; $month <= 8; $month++) { + $this->processMonth($company, $employees, $currentYear, $month); + } + } + + $this->command->info('TimeEntry seeder completed successfully!'); + } + + private function processMonth($company, $employees, $year, $month) + { + $startDate = Carbon::create($year, $month, 1); + $endDate = $startDate->copy()->endOfMonth(); + + foreach ($employees as $employee) { + // Create 8-10 time entries per employee per month + $entriesCount = 8 + ($employee->id % 3); // 8, 9, or 10 entries + + for ($i = 0; $i < $entriesCount; $i++) { + $entryDate = $startDate->copy()->addDays($i * 3 + ($employee->id % 3)); + + // Skip if date exceeds month end + if ($entryDate->gt($endDate)) { + break; + } + + // Skip weekends + if ($entryDate->dayOfWeek == 6 || $entryDate->dayOfWeek == 0) { + continue; + } + + $this->createTimeEntry($company, $employee, $entryDate, $i); + } + } + } + + private function createTimeEntry($company, $employee, $date, $index) + { + $dateString = $date->format('Y-m-d'); + + // Check if time entry already exists + $existingEntry = TimeEntry::where('employee_id', $employee->id) + ->where('date', $dateString) + ->first(); + + if ($existingEntry) { + return; + } + + // Get time entry pattern based on index + $pattern = $this->getTimeEntryPattern($index); + + TimeEntry::create([ + 'employee_id' => $employee->id, + 'date' => $dateString, + 'hours' => $pattern['hours'], + 'description' => $pattern['description'], + 'project' => $pattern['project'], + 'status' => 'pending', + 'manager_comments' => null, + 'approved_by' => null, + 'approved_at' => null, + 'created_by' => $employee->id, + ]); + } + + private function getTimeEntryPattern($index) + { + $projects = [ + 'Website Development', + 'Mobile App', + 'Database Optimization', + 'API Integration', + 'Bug Fixes', + 'Code Review', + 'Documentation', + 'Testing' + ]; + + $descriptions = [ + 'Worked on frontend components and user interface improvements', + 'Implemented new features and functionality as per requirements', + 'Fixed critical bugs and performance issues in the system', + 'Conducted code review and provided feedback to team members', + 'Updated project documentation and technical specifications', + 'Performed testing and quality assurance activities', + 'Attended team meetings and project planning sessions', + 'Researched new technologies and best practices' + ]; + + $hours = [6.5, 7.0, 7.5, 8.0, 8.5]; + + $patternIndex = $index % count($projects); + + return [ + 'hours' => $hours[$index % count($hours)], + 'description' => $descriptions[$patternIndex], + 'project' => $projects[$patternIndex] + ]; + } +} diff --git a/database/seeders/TrainingProgramSeeder.php b/database/seeders/TrainingProgramSeeder.php new file mode 100644 index 000000000..09da36e39 --- /dev/null +++ b/database/seeders/TrainingProgramSeeder.php @@ -0,0 +1,96 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed training programs by training type for consistent data + $programsByType = [ + 'Orientation and Onboarding' => [ + ['name' => 'New Employee Orientation Program', 'description' => 'Comprehensive 3-day orientation covering company culture, policies, and basic job requirements', 'duration' => 24, 'cost' => 5000, 'capacity' => 20, 'status' => 'active', 'prerequisites' => 'None', 'is_mandatory' => true, 'is_self_enrollment' => false], + ['name' => 'Department Integration Workshop', 'description' => 'Specialized onboarding focused on department-specific processes and team integration', 'duration' => 8, 'cost' => 2000, 'capacity' => 15, 'status' => 'active', 'prerequisites' => 'Completion of basic orientation', 'is_mandatory' => true, 'is_self_enrollment' => false] + ], + 'Technical Skills Training' => [ + ['name' => 'Advanced Software Development', 'description' => 'Intensive training on latest programming languages, frameworks, and development methodologies', 'duration' => 40, 'cost' => 15000, 'capacity' => 12, 'status' => 'active', 'prerequisites' => 'Basic programming knowledge', 'is_mandatory' => false, 'is_self_enrollment' => true], + ['name' => 'Database Management Certification', 'description' => 'Comprehensive database design, optimization, and administration training program', 'duration' => 32, 'cost' => 12000, 'capacity' => 10, 'status' => 'draft', 'prerequisites' => 'SQL fundamentals', 'is_mandatory' => false, 'is_self_enrollment' => true] + ], + 'Leadership Development' => [ + ['name' => 'Executive Leadership Program', 'description' => 'Strategic leadership development for senior management and high-potential employees', 'duration' => 48, 'cost' => 25000, 'capacity' => 8, 'status' => 'active', 'prerequisites' => 'Management experience', 'is_mandatory' => false, 'is_self_enrollment' => false], + ['name' => 'Team Leadership Fundamentals', 'description' => 'Essential leadership skills for new managers and team leads', 'duration' => 16, 'cost' => 8000, 'capacity' => 15, 'status' => 'completed', 'prerequisites' => 'Supervisory role', 'is_mandatory' => true, 'is_self_enrollment' => false] + ], + 'Communication Skills' => [ + ['name' => 'Effective Business Communication', 'description' => 'Professional communication skills including presentations, meetings, and written correspondence', 'duration' => 12, 'cost' => 4000, 'capacity' => 25, 'status' => 'active', 'prerequisites' => 'None', 'is_mandatory' => false, 'is_self_enrollment' => true], + ['name' => 'Cross-Cultural Communication', 'description' => 'Communication strategies for diverse and multicultural work environments', 'duration' => 8, 'cost' => 3000, 'capacity' => 20, 'status' => 'active', 'prerequisites' => 'Basic communication skills', 'is_mandatory' => false, 'is_self_enrollment' => true] + ], + 'Safety and Compliance' => [ + ['name' => 'Workplace Safety Certification', 'description' => 'Mandatory safety training covering OSHA regulations, emergency procedures, and risk management', 'duration' => 6, 'cost' => 1500, 'capacity' => 50, 'status' => 'active', 'prerequisites' => 'None', 'is_mandatory' => true, 'is_self_enrollment' => false], + ['name' => 'Data Privacy and Security Training', 'description' => 'Compliance training on data protection, privacy regulations, and cybersecurity best practices', 'duration' => 4, 'cost' => 1000, 'capacity' => 100, 'status' => 'active', 'prerequisites' => 'None', 'is_mandatory' => true, 'is_self_enrollment' => false] + ], + 'Customer Service Excellence' => [ + ['name' => 'Customer Experience Mastery', 'description' => 'Advanced customer service techniques, complaint handling, and relationship building strategies', 'duration' => 16, 'cost' => 6000, 'capacity' => 20, 'status' => 'active', 'prerequisites' => 'Customer service experience', 'is_mandatory' => false, 'is_self_enrollment' => true], + ['name' => 'Digital Customer Support', 'description' => 'Modern customer support tools, chatbots, CRM systems, and omnichannel service delivery', 'duration' => 12, 'cost' => 4500, 'capacity' => 15, 'status' => 'draft', 'prerequisites' => 'Basic computer skills', 'is_mandatory' => false, 'is_self_enrollment' => true] + ] + ]; + + foreach ($companies as $company) { + // Get training types for this company + $trainingTypes = TrainingType::where('created_by', $company->id)->get(); + + if ($trainingTypes->isEmpty()) { + $this->command->warn('No training types found for company: ' . $company->name . '. Please run TrainingTypeSeeder first.'); + continue; + } + + foreach ($trainingTypes as $trainingType) { + $typePrograms = $programsByType[$trainingType->name] ?? []; + + foreach ($typePrograms as $programData) { + // Check if program already exists for this training type + if (TrainingProgram::where('name', $programData['name'])->where('training_type_id', $trainingType->id)->exists()) { + continue; + } + + try { + TrainingProgram::create([ + 'name' => $programData['name'], + 'training_type_id' => $trainingType->id, + 'description' => $programData['description'], + 'duration' => $programData['duration'], + 'cost' => $programData['cost'], + 'capacity' => $programData['capacity'], + 'status' => $programData['status'], + 'materials' => randomImage(), + 'prerequisites' => $programData['prerequisites'], + 'is_mandatory' => $programData['is_mandatory'], + 'is_self_enrollment' => $programData['is_self_enrollment'], + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create training program: ' . $programData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('TrainingProgram seeder completed successfully!'); + } +} diff --git a/database/seeders/TrainingSessionSeeder.php b/database/seeders/TrainingSessionSeeder.php new file mode 100644 index 000000000..938e72387 --- /dev/null +++ b/database/seeders/TrainingSessionSeeder.php @@ -0,0 +1,165 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + $currentYear = date('Y'); + + // Fixed training session data for consistent results + $sessionData = [ + ['name' => 'Morning Session - Batch A', 'location' => 'Training Room 1', 'location_type' => 'physical', 'status' => 'completed', 'start_hour' => 9, 'duration' => 4], + ['name' => 'Afternoon Session - Batch B', 'location' => 'Training Room 2', 'location_type' => 'physical', 'status' => 'completed', 'start_hour' => 14, 'duration' => 4], + ['name' => 'Virtual Workshop Session', 'location' => 'Online Platform', 'location_type' => 'virtual', 'status' => 'in_progress', 'start_hour' => 10, 'duration' => 3], + ['name' => 'Weekend Intensive Session', 'location' => 'Conference Hall', 'location_type' => 'physical', 'status' => 'scheduled', 'start_hour' => 9, 'duration' => 6], + ['name' => 'Evening Online Session', 'location' => 'Zoom Meeting', 'location_type' => 'virtual', 'status' => 'completed', 'start_hour' => 18, 'duration' => 2] + ]; + + $meetingLinks = [ + 'https://zoom.us/j/123456789', + 'https://teams.microsoft.com/meeting/join', + 'https://meet.google.com/abc-defg-hij', + 'https://webex.com/meet/training', + 'https://gotomeeting.com/join/session' + ]; + + foreach ($companies as $company) { + // Get training programs for this company + $trainingPrograms = TrainingProgram::where('created_by', $company->id)->get(); + + if ($trainingPrograms->isEmpty()) { + $this->command->warn('No training programs found for company: ' . $company->name . '. Please run TrainingProgramSeeder first.'); + continue; + } + + // Get employees for trainers and attendees + $employees = User::where('type', 'employee')->where('created_by', $company->id)->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Create sessions for first 3 training programs + $selectedPrograms = $trainingPrograms->take(5); + + foreach ($selectedPrograms as $progIndex => $program) { + // Create 2 sessions per program + for ($sessionIndex = 0; $sessionIndex < 2; $sessionIndex++) { + $dataIndex = ($progIndex * 2) + $sessionIndex; + $session = $sessionData[$dataIndex % 5]; + + $startDate = $currentYear . '-' . str_pad($progIndex + 3, 2, '0', STR_PAD_LEFT) . '-' . str_pad(($sessionIndex + 1) * 10, 2, '0', STR_PAD_LEFT); + $startDateTime = $startDate . ' ' . str_pad($session['start_hour'], 2, '0', STR_PAD_LEFT) . ':00:00'; + $endDateTime = date('Y-m-d H:i:s', strtotime($startDateTime . ' +' . $session['duration'] . ' hours')); + + try { + $trainingSession = TrainingSession::create([ + 'training_program_id' => $program->id, + 'name' => $session['name'], + 'start_date' => $startDateTime, + 'end_date' => $endDateTime, + 'location' => $session['location'], + 'location_type' => $session['location_type'], + 'meeting_link' => $session['location_type'] === 'virtual' ? $meetingLinks[$dataIndex % 5] : null, + 'status' => $session['status'], + 'notes' => 'Training session for ' . $program->name, + 'is_recurring' => false, + 'recurrence_pattern' => null, + 'recurrence_count' => null, + 'created_by' => $company->id, + ]); + + // Create trainers for the session + $this->createSessionTrainers($trainingSession, $employees); + + // Create attendance records for completed and in_progress sessions + if (in_array($session['status'], ['completed', 'in_progress'])) { + $this->createSessionAttendance($trainingSession, $employees, $session['status']); + } + } catch (\Exception $e) { + $this->command->error('Failed to create training session for program: ' . $program->name . ' in company: ' . $company->name); + continue; + } + } + } + } + + $this->command->info('TrainingSession seeder completed successfully!'); + } + + /** + * Create trainers for training session + */ + private function createSessionTrainers($trainingSession, $employees) + { + // Assign 2 random trainers from 5 random employees + $trainers = $employees->random(3); + + foreach ($trainers as $trainer) { + try { + DB::table('training_session_trainer')->insert([ + 'training_session_id' => $trainingSession->id, + 'employee_id' => $trainer->id, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + continue; + } + } + } + + /** + * Create attendance records for training session + */ + private function createSessionAttendance($trainingSession, $employees, $status) + { + // Create attendance for 5 random employees + $attendees = $employees->random(5); + + // Attendance patterns based on status + $attendancePatterns = [ + 'completed' => [true, true, true, false, true], // 4 out of 5 attended + 'in_progress' => [true, true, false, false, false] // 2 out of 5 attended so far + ]; + + $pattern = $attendancePatterns[$status]; + + foreach ($attendees as $index => $attendee) { + $isPresent = $pattern[$index]; + + try { + DB::table('training_session_attendance')->insert([ + 'training_session_id' => $trainingSession->id, + 'employee_id' => $attendee->id, + 'is_present' => $isPresent, + 'notes' => $isPresent ? 'Attended session' : 'Absent from session', + 'created_at' => now(), + 'updated_at' => now(), + ]); + } catch (\Exception $e) { + continue; + } + } + } +} diff --git a/database/seeders/TrainingTypeSeeder.php b/database/seeders/TrainingTypeSeeder.php new file mode 100644 index 000000000..602431525 --- /dev/null +++ b/database/seeders/TrainingTypeSeeder.php @@ -0,0 +1,132 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Fixed training types for consistent data + $trainingTypes = [ + [ + 'name' => 'Orientation and Onboarding', + 'description' => 'Comprehensive introduction program for new employees covering company culture, policies, procedures, and job-specific training' + ], + [ + 'name' => 'Technical Skills Training', + 'description' => 'Specialized training focused on developing technical competencies, software proficiency, and job-specific technical skills' + ], + [ + 'name' => 'Leadership Development', + 'description' => 'Training program designed to develop leadership capabilities, management skills, and strategic thinking abilities' + ], + [ + 'name' => 'Communication Skills', + 'description' => 'Training to enhance verbal, written, and interpersonal communication skills for effective workplace interaction' + ], + [ + 'name' => 'Safety and Compliance', + 'description' => 'Mandatory training covering workplace safety protocols, regulatory compliance, and risk management procedures' + ], + [ + 'name' => 'Customer Service Excellence', + 'description' => 'Training focused on improving customer interaction skills, service quality, and customer satisfaction techniques' + ], + [ + 'name' => 'Sales and Marketing', + 'description' => 'Training program covering sales techniques, marketing strategies, customer relationship management, and revenue generation' + ], + [ + 'name' => 'Project Management', + 'description' => 'Training on project planning, execution, monitoring, and delivery methodologies including agile and traditional approaches' + ], + [ + 'name' => 'Quality Management', + 'description' => 'Training focused on quality standards, process improvement, quality control measures, and continuous improvement methodologies' + ], + [ + 'name' => 'Digital Literacy', + 'description' => 'Training to enhance digital skills, software applications, online tools, and technology adoption in the workplace' + ], + [ + 'name' => 'Soft Skills Development', + 'description' => 'Training covering teamwork, problem-solving, time management, adaptability, and other essential workplace soft skills' + ], + [ + 'name' => 'Professional Certification', + 'description' => 'Training programs designed to prepare employees for industry-recognized professional certifications and credentials' + ] + ]; + + foreach ($companies as $company) { + // Get branches for this company + $branches = Branch::where('created_by', $company->id)->get(); + + if ($branches->isEmpty()) { + $this->command->warn('No branches found for company: ' . $company->name . '. Please run BranchSeeder first.'); + continue; + } + + // Get departments for this company + $departments = Department::where('created_by', $company->id)->get(); + + if ($departments->isEmpty()) { + $this->command->warn('No departments found for company: ' . $company->name . '. Please run DepartmentSeeder first.'); + continue; + } + + foreach ($trainingTypes as $index => $trainingTypeData) { + // Assign training type to specific branch (cycling through branches) + $branch = $branches[$index % $branches->count()]; + + // Check if training type already exists for this branch + if (TrainingType::where('name', $trainingTypeData['name'])->where('branch_id', $branch->id)->exists()) { + continue; + } + + try { + $trainingType = TrainingType::create([ + 'name' => $trainingTypeData['name'], + 'description' => $trainingTypeData['description'], + 'branch_id' => $branch->id, + 'created_by' => $company->id, + ]); + + // Attach training type to departments within the same branch + $branchDepartments = $departments->where('branch_id', $branch->id); + + if ($branchDepartments->isNotEmpty()) { + if ($index < 6) { + // Company-wide training types: attach to all departments in this branch + $trainingType->departments()->attach($branchDepartments->pluck('id')); + } else { + // Department-specific training types: attach to first department in this branch + $trainingType->departments()->attach($branchDepartments->first()->id); + } + } + } catch (\Exception $e) { + $this->command->error('Failed to create training type: ' . $trainingTypeData['name'] . ' for company: ' . $company->name); + continue; + } + } + } + $this->command->info('TrainingType seeder completed successfully!'); + } +} diff --git a/database/seeders/TripSeeder.php b/database/seeders/TripSeeder.php new file mode 100644 index 000000000..1bccbd508 --- /dev/null +++ b/database/seeders/TripSeeder.php @@ -0,0 +1,187 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Trip purposes and destinations + $tripPurposes = [ + 'Client Meeting', + 'Conference Attendance', + 'Training Program', + 'Site Visit', + 'Business Development', + 'Project Implementation', + 'Vendor Meeting', + 'Market Research', + 'Team Building', + 'Product Launch' + ]; + + $destinations = [ + 'New York, USA', + 'London, UK', + 'Tokyo, Japan', + 'Singapore', + 'Dubai, UAE', + 'Mumbai, India', + 'Delhi, India', + 'Bangalore, India', + 'Chennai, India', + 'Pune, India', + 'Sydney, Australia', + 'Toronto, Canada' + ]; + + // Trip descriptions based on purpose + $tripDescriptions = [ + 'Client Meeting' => 'Meeting with key clients to discuss project requirements, deliverables, and future business opportunities.', + 'Conference Attendance' => 'Attending industry conference to gain insights on latest trends, technologies, and networking opportunities.', + 'Training Program' => 'Participating in professional development training to enhance skills and knowledge in specific domain areas.', + 'Site Visit' => 'Visiting client site or project location to assess requirements, progress, and coordinate implementation activities.', + 'Business Development' => 'Exploring new business opportunities, meeting potential clients, and expanding market presence in target regions.', + 'Project Implementation' => 'On-site project implementation, system deployment, and providing technical support to client teams.', + 'Vendor Meeting' => 'Meeting with vendors and suppliers to discuss partnerships, contracts, and service level agreements.', + 'Market Research' => 'Conducting market research, competitor analysis, and gathering insights for strategic business decisions.', + 'Team Building' => 'Participating in team building activities and workshops to improve collaboration and team dynamics.', + 'Product Launch' => 'Attending product launch events, demonstrations, and marketing activities in target markets.' + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get managers/HR for approval + $approvers = User::whereIn('type', ['manager', 'hr']) + ->where('created_by', $company->id) + ->get(); + + // Create 8-15 trips for this company + $tripCount = rand(8, 15); + + for ($i = 0; $i < $tripCount; $i++) { + $employee = $employees->take(5)->random(); + $purpose = $faker->randomElement($tripPurposes); + $destination = $faker->randomElement($destinations); + $description = $tripDescriptions[$purpose]; + + $startDate = $faker->dateTimeBetween('-6 months', '+3 months'); + $endDate = (clone $startDate)->modify('+' . rand(1, 7) . ' days'); + + $status = $faker->randomElement(['planned', 'ongoing', 'completed', 'cancelled']); + $approver = $approvers->isNotEmpty() ? $approvers->random() : null; + + $advanceAmount = $faker->optional(0.7)->randomFloat(2, 5000, 50000); + $totalExpenses = $status === 'completed' ? $faker->randomFloat(2, 3000, 60000) : null; + + try { + $trip = Trip::create([ + 'employee_id' => $employee->id, + 'purpose' => $purpose, + 'destination' => $destination, + 'start_date' => $startDate->format('Y-m-d'), + 'end_date' => $endDate->format('Y-m-d'), + 'description' => $description, + 'expected_outcomes' => $faker->sentence(12), + 'status' => $status, + 'documents' => randomImage(), + 'advance_amount' => $advanceAmount, + 'advance_status' => $advanceAmount ? $faker->randomElement(['requested', 'approved', 'paid', 'reconciled']) : null, + 'total_expenses' => $totalExpenses, + 'reimbursement_status' => $totalExpenses ? $faker->randomElement(['pending', 'approved', 'paid']) : null, + 'approved_by' => $status !== 'planned' ? $approver?->id : null, + 'approved_at' => $status !== 'planned' ? $faker->dateTimeBetween('-6 months', 'now') : null, + 'trip_report' => $status === 'completed' ? $faker->paragraph(3) : null, + 'created_by' => $company->id, + ]); + + // Create trip expenses for completed trips + if ($status === 'completed') { + $this->createTripExpenses($trip, $faker); + } + } catch (\Exception $e) { + $this->command->error($e->getMessage()); + continue; + } + } + } + + $this->command->info('Trip seeder completed successfully!'); + } + + /** + * Create trip expenses for a trip + */ + private function createTripExpenses($trip, $faker) + { + $expenseTypes = [ + 'Transportation' => ['Flight tickets', 'Train tickets', 'Taxi fare', 'Bus fare', 'Car rental'], + 'Accommodation' => ['Hotel charges', 'Guest house', 'Service apartment', 'Lodging'], + 'Meals' => ['Breakfast', 'Lunch', 'Dinner', 'Snacks', 'Business meal'], + 'Communication' => ['Phone calls', 'Internet charges', 'Mobile roaming'], + 'Miscellaneous' => ['Visa fees', 'Travel insurance', 'Airport parking', 'Tips', 'Laundry'] + ]; + + // Create 3-5 expenses per trip (max = available expense types) + $expenseCount = rand(3, min(5, count($expenseTypes))); + $selectedExpenseTypes = $faker->randomElements(array_keys($expenseTypes), $expenseCount); + + foreach ($selectedExpenseTypes as $expenseType) { + $expenseDescription = $faker->randomElement($expenseTypes[$expenseType]); + + $startDate = new \DateTime($trip->start_date); + $endDate = new \DateTime($trip->end_date); + // Ensure start date is before end date + if ($startDate >= $endDate) { + $expenseDate = $startDate; + } else { + $expenseDate = $faker->dateTimeBetween($startDate, $endDate); + } + + try { + TripExpense::create([ + 'trip_id' => $trip->id, + 'expense_type' => $expenseType, + 'expense_date' => $expenseDate->format('Y-m-d'), + 'amount' => $faker->randomFloat(2, 100, 5000), + 'currency' => $faker->randomElement(['USD', 'EUR', 'INR', 'GBP']), + 'description' => $expenseDescription, + 'receipt' => randomImage(), + 'is_reimbursable' => $faker->boolean(90), + 'status' => $faker->randomElement(['pending', 'approved', 'rejected']), + 'created_by' => $trip->employee_id, + ]); + } catch (\Exception $e) { + continue; + } + } + } +} diff --git a/database/seeders/WarningSeeder.php b/database/seeders/WarningSeeder.php new file mode 100644 index 000000000..031d34ea8 --- /dev/null +++ b/database/seeders/WarningSeeder.php @@ -0,0 +1,143 @@ +get(); + + if ($companies->isEmpty()) { + $this->command->warn('No company users found. Please run DefaultCompanySeeder first.'); + return; + } + + // Warning types and their subjects/descriptions + $warningData = [ + 'attendance' => [ + 'subjects' => ['Excessive Absenteeism', 'Frequent Tardiness', 'Unauthorized Leave', 'Pattern of Late Arrivals'], + 'descriptions' => [ + 'Excessive Absenteeism' => 'Employee has exceeded the acceptable number of absences without proper documentation or approval, affecting team productivity and work coverage.', + 'Frequent Tardiness' => 'Employee consistently arrives late to work, disrupting team meetings and affecting overall work schedule and productivity.', + 'Unauthorized Leave' => 'Employee took leave without following proper approval procedures and failed to notify supervisor in advance.', + 'Pattern of Late Arrivals' => 'Employee shows a consistent pattern of arriving late to work, which impacts team coordination and project timelines.' + ] + ], + 'performance' => [ + 'subjects' => ['Below Standard Performance', 'Missed Deadlines', 'Quality Issues', 'Productivity Concerns'], + 'descriptions' => [ + 'Below Standard Performance' => 'Employee\'s work performance has consistently fallen below established standards and expectations for their role and experience level.', + 'Missed Deadlines' => 'Employee has repeatedly failed to meet project deadlines, causing delays in deliverables and affecting client satisfaction.', + 'Quality Issues' => 'Employee\'s work output contains frequent errors and does not meet the quality standards expected for their position.', + 'Productivity Concerns' => 'Employee\'s productivity levels are significantly below team average and departmental expectations.' + ] + ], + 'conduct' => [ + 'subjects' => ['Inappropriate Behavior', 'Policy Violation', 'Unprofessional Conduct', 'Workplace Disruption'], + 'descriptions' => [ + 'Inappropriate Behavior' => 'Employee engaged in behavior that is inappropriate for the workplace environment and violates professional conduct standards.', + 'Policy Violation' => 'Employee violated specific company policies and procedures that are clearly outlined in the employee handbook.', + 'Unprofessional Conduct' => 'Employee demonstrated unprofessional behavior towards colleagues, clients, or supervisors that affects workplace harmony.', + 'Workplace Disruption' => 'Employee\'s actions have disrupted the normal work environment and negatively impacted team morale and productivity.' + ] + ], + 'safety' => [ + 'subjects' => ['Safety Protocol Violation', 'Unsafe Work Practices', 'Equipment Misuse', 'Failure to Use PPE'], + 'descriptions' => [ + 'Safety Protocol Violation' => 'Employee failed to follow established safety protocols, potentially endangering themselves and other employees.', + 'Unsafe Work Practices' => 'Employee engaged in unsafe work practices that could result in injury or damage to company property.', + 'Equipment Misuse' => 'Employee improperly used company equipment, potentially causing damage or creating safety hazards.', + 'Failure to Use PPE' => 'Employee failed to use required personal protective equipment as mandated by safety regulations.' + ] + ] + ]; + + foreach ($companies as $company) { + // Get employees for this company + $employees = User::where('type', 'employee') + ->where('created_by', $company->id) + ->get(); + + if ($employees->isEmpty()) { + $this->command->warn('No employees found for company: ' . $company->name . '. Please run EmployeeSeeder first.'); + continue; + } + + // Get managers/HR who can issue warnings + $warningIssuers = User::whereIn('type', ['manager', 'hr']) + ->where('created_by', $company->id) + ->get(); + + if ($warningIssuers->isEmpty()) { + $this->command->warn('No managers/HR found for company: ' . $company->name . '. Please run DefaultCompanyUserSeeder first.'); + continue; + } + + // Create 5-12 warnings for this company + $warningCount = rand(5, 12); + + for ($i = 0; $i < $warningCount; $i++) { + $employee = $employees->take(5)->random(); + $warningIssuer = $warningIssuers->random(); + $warningType = $faker->randomElement(array_keys($warningData)); + $typeData = $warningData[$warningType]; + $subject = $faker->randomElement($typeData['subjects']); + $description = $typeData['descriptions'][$subject]; + + $warningDate = $faker->dateTimeBetween('-1 year', 'now'); + $severity = $faker->randomElement(['verbal', 'written', 'final']); + $status = $faker->randomElement(['draft', 'issued', 'acknowledged', 'expired']); + + // Calculate expiry date (warnings typically expire after 6-12 months) + $expiryDate = (clone $warningDate)->modify('+' . rand(6, 12) . ' months'); + + // Improvement plan details for performance/conduct warnings + $hasImprovementPlan = in_array($warningType, ['performance', 'conduct']) && $faker->boolean(60); + $improvementStartDate = $hasImprovementPlan ? $warningDate : null; + $improvementEndDate = $hasImprovementPlan ? (clone $warningDate)->modify('+90 days') : null; + + try { + Warning::create([ + 'employee_id' => $employee->id, + 'warning_by' => $warningIssuer->id, + 'warning_type' => $warningType, + 'subject' => $subject, + 'severity' => $severity, + 'warning_date' => $warningDate->format('Y-m-d'), + 'description' => $description, + 'status' => $status, + 'documents' => randomImage(), + 'acknowledgment_date' => $status === 'acknowledged' ? $faker->dateTimeBetween($warningDate, 'now')->format('Y-m-d') : null, + 'employee_response' => $status === 'acknowledged' ? $faker->sentence(10) : null, + 'approved_by' => $status !== 'draft' ? $warningIssuer->id : null, + 'approved_at' => $status !== 'draft' ? $faker->dateTimeBetween($warningDate, 'now') : null, + 'expiry_date' => $expiryDate->format('Y-m-d'), + 'has_improvement_plan' => $hasImprovementPlan, + 'improvement_plan_goals' => $hasImprovementPlan ? $faker->sentence(15) : null, + 'improvement_plan_start_date' => $improvementStartDate?->format('Y-m-d'), + 'improvement_plan_end_date' => $improvementEndDate?->format('Y-m-d'), + 'improvement_plan_progress' => $hasImprovementPlan && $faker->boolean(40) ? $faker->sentence(12) : null, + 'created_by' => $company->id, + ]); + } catch (\Exception $e) { + $this->command->error('Failed to create warning for employee: ' . $employee->name . ' in company: ' . $company->name); + continue; + } + } + } + + $this->command->info('Warning seeder completed successfully!'); + } +} diff --git a/database/seeders/WebhookSeeder.php b/database/seeders/WebhookSeeder.php new file mode 100644 index 000000000..a0db19d25 --- /dev/null +++ b/database/seeders/WebhookSeeder.php @@ -0,0 +1,38 @@ +get(); + + if ($users->isEmpty()) { + $this->command->warn('No company users found. Please seed users first.'); + return; + } + + $webhookTemplates = [ + ['module' => 'New User', 'method' => 'POST', 'url' => 'https://example.com/webhooks/new-user'], + ['module' => 'New Appointment', 'method' => 'POST', 'url' => 'https://example.com/webhooks/new-appointment'], + ['module' => 'New User', 'method' => 'GET', 'url' => 'https://example.com/api/user-created'], + ['module' => 'New Appointment', 'method' => 'GET', 'url' => 'https://example.com/webhooks/appointment-status'] + ]; + + foreach ($users as $user) { + foreach ($webhookTemplates as $template) { + Webhook::firstOrCreate( + ['user_id' => $user->id, 'module' => $template['module']], + array_merge($template, ['user_id' => $user->id]) + ); + } + } + + $this->command->info('Webhooks seeded successfully!'); + } +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 000000000..a136d2248 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,44 @@ +import js from '@eslint/js'; +import prettier from 'eslint-config-prettier'; +import react from 'eslint-plugin-react'; +import reactHooks from 'eslint-plugin-react-hooks'; +import globals from 'globals'; +import typescript from 'typescript-eslint'; + +/** @type {import('eslint').Linter.Config[]} */ +export default [ + js.configs.recommended, + ...typescript.configs.recommended, + { + ...react.configs.flat.recommended, + ...react.configs.flat['jsx-runtime'], // Required for React 17+ + languageOptions: { + globals: { + ...globals.browser, + }, + }, + rules: { + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + 'react/no-unescaped-entities': 'off', + }, + settings: { + react: { + version: 'detect', + }, + }, + }, + { + plugins: { + 'react-hooks': reactHooks, + }, + rules: { + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'warn', + }, + }, + { + ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js'], + }, + prettier, // Turn off all rules that might conflict with Prettier +]; diff --git a/extract-translations.php b/extract-translations.php new file mode 100644 index 000000000..2345f1fa0 --- /dev/null +++ b/extract-translations.php @@ -0,0 +1,100 @@ + + */ + +$uri = urldecode( + parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) +); + +// This file allows us to emulate Apache's "mod_rewrite" functionality from the +// built-in PHP web server. This provides a convenient way to test a Laravel +// application without having installed a "real" web server software here. +if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { + return false; +} + +require_once __DIR__.'/public/index.php'; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..69d94a4a2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,10650 @@ +{ + "name": "main-file", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@fullcalendar/daygrid": "^6.1.17", + "@fullcalendar/interaction": "^6.1.17", + "@fullcalendar/react": "^6.1.17", + "@fullcalendar/timegrid": "^6.1.17", + "@headlessui/react": "^2.2.0", + "@hello-pangea/dnd": "^18.0.1", + "@inertiajs/react": "^2.0.0", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.3", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toggle": "^1.1.2", + "@radix-ui/react-toggle-group": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.8", + "@stripe/react-stripe-js": "^3.7.0", + "@stripe/stripe-js": "^7.4.0", + "@tailwindcss/oxide": "^4.1.10", + "@tailwindcss/vite": "^4.0.6", + "@tiptap/extension-color": "^2.23.0", + "@tiptap/extension-link": "^2.23.0", + "@tiptap/extension-text-align": "^2.23.0", + "@tiptap/extension-text-style": "^2.23.0", + "@tiptap/pm": "^2.23.0", + "@tiptap/react": "^2.23.0", + "@tiptap/starter-kit": "^2.23.0", + "@types/qrcode": "^1.5.5", + "@types/react": "^19.0.3", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "concurrently": "^9.0.1", + "date-fns": "^4.1.0", + "globals": "^15.14.0", + "i18next": "^25.2.1", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", + "laravel-vite-plugin": "^1.0", + "lucide-react": "^0.475.0", + "next-themes": "^0.4.6", + "qrcode": "^1.5.4", + "react": "^19.0.0", + "react-barcode": "^1.6.1", + "react-country-flag": "^3.1.0", + "react-dom": "^19.0.0", + "react-i18next": "^15.5.3", + "recharts": "^3.0.2", + "sonner": "^2.0.3", + "tailwind-merge": "^3.0.1", + "tailwind-scrollbar": "^4.0.2", + "tailwindcss": "^4.0.0", + "tailwindcss-animate": "^1.0.7", + "typescript": "^5.7.2", + "vite": "^6.0" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@types/node": "^22.13.5", + "eslint": "^9.17.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-react": "^7.37.3", + "eslint-plugin-react-hooks": "^5.1.0", + "prettier": "^3.4.2", + "prettier-plugin-organize-imports": "^4.1.0", + "prettier-plugin-tailwindcss": "^0.6.11", + "typescript-eslint": "^8.23.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.9.5", + "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", + "lightningcss-linux-x64-gnu": "^1.29.1" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.4.tgz", + "integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.5.tgz", + "integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.4", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.26.28", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.26.28.tgz", + "integrity": "sha512-yORQuuAtVpiRjpMhdc0wJj06b9JFjrYF4qp96j++v2NBpbi6SEGF7donUJ3TMieerQ6qVkAv1tgr7L4r5roTqw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.8", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.7.tgz", + "integrity": "sha512-0tLRojf/1Go2JgEVm+3Frg9A3IW8bJgKgdO0BN5RkF//ufuz2joZM63Npau2ff3J6lUVYgDSNzNkR+aH3IVfjg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.5" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@fullcalendar/core": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.20.tgz", + "integrity": "sha512-1cukXLlePFiJ8YKXn/4tMKsy0etxYLCkXk8nUCFi11nRONF2Ba2CD5b21/ovtOO2tL6afTJfwmc1ed3HG7eB1g==", + "license": "MIT", + "peer": true, + "dependencies": { + "preact": "~10.12.1" + } + }, + "node_modules/@fullcalendar/daygrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.20.tgz", + "integrity": "sha512-AO9vqhkLP77EesmJzuU+IGXgxNulsA8mgQHynclJ8U70vSwAVnbcLG9qftiTAFSlZjiY/NvhE7sflve6cJelyQ==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/interaction": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.20.tgz", + "integrity": "sha512-p6txmc5txL0bMiPaJxe2ip6o0T384TyoD2KGdsU6UjZ5yoBlaY+dg7kxfnYKpYMzEJLG58n+URrHr2PgNL2fyA==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@fullcalendar/react": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.20.tgz", + "integrity": "sha512-1w0pZtceaUdfAnxMSCGHCQalhi+mR1jOe76sXzyAXpcPz/Lf0zHSdcGK/U2XpZlnQgQtBZW+d+QBnnzVQKCxAA==", + "license": "MIT", + "peerDependencies": { + "@fullcalendar/core": "~6.1.20", + "react": "^16.7.0 || ^17 || ^18 || ^19", + "react-dom": "^16.7.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/@fullcalendar/timegrid": { + "version": "6.1.20", + "resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.20.tgz", + "integrity": "sha512-4H+/MWbz3ntA50lrPif+7TsvMeX3R1GSYjiLULz0+zEJ7/Yfd9pupZmAwUs/PBpA6aAcFmeRr0laWfcz1a9V1A==", + "license": "MIT", + "dependencies": { + "@fullcalendar/daygrid": "~6.1.20" + }, + "peerDependencies": { + "@fullcalendar/core": "~6.1.20" + } + }, + "node_modules/@headlessui/react": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@headlessui/react/-/react-2.2.9.tgz", + "integrity": "sha512-Mb+Un58gwBn0/yWZfyrCh0TJyurtT+dETj7YHleylHk5od3dv2XqETPGWMyQ5/7sYN7oWdyM1u9MvC0OC8UmzQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react": "^0.26.16", + "@react-aria/focus": "^3.20.2", + "@react-aria/interactions": "^3.25.0", + "@tanstack/react-virtual": "^3.13.9", + "use-sync-external-store": "^1.5.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/@hello-pangea/dnd": { + "version": "18.0.1", + "resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz", + "integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.7", + "css-box-model": "^1.2.1", + "raf-schd": "^4.0.3", + "react-redux": "^9.2.0", + "redux": "^5.0.1" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inertiajs/core": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@inertiajs/core/-/core-2.3.13.tgz", + "integrity": "sha512-qMHRnb59k/HehXw/WfQt5kPV0k9RapfFcWJZINJnYMwfHDEJ21iNVZjsJHmDN7yWdZmG1Dxi9FP4xarWWgdosQ==", + "license": "MIT", + "dependencies": { + "@types/lodash-es": "^4.17.12", + "axios": "^1.13.2", + "laravel-precognition": "^1.0.1", + "lodash-es": "^4.17.23", + "qs": "^6.14.1" + } + }, + "node_modules/@inertiajs/react": { + "version": "2.3.13", + "resolved": "https://registry.npmjs.org/@inertiajs/react/-/react-2.3.13.tgz", + "integrity": "sha512-0a4SgBadHfGX+H/lRQbOmPO8+jTa87nR6qUW/9+YU6eEf738bsQ6enHxEgx9jrpT5fJyLK8RcMUTKpadRUSs1Q==", + "license": "MIT", + "dependencies": { + "@inertiajs/core": "2.3.13", + "@types/lodash-es": "^4.17.12", + "laravel-precognition": "^1.0.1", + "lodash-es": "^4.17.23" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.9.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz", + "integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz", + "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", + "integrity": "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.12.tgz", + "integrity": "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz", + "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", + "integrity": "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz", + "integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz", + "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.16", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz", + "integrity": "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.8.tgz", + "integrity": "sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz", + "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz", + "integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-navigation-menu/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz", + "integrity": "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz", + "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz", + "integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz", + "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-progress": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.8.tgz", + "integrity": "sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.3", + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", + "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.10.tgz", + "integrity": "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz", + "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-focus-guards": "1.1.3", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz", + "integrity": "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.4" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz", + "integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz", + "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.13.tgz", + "integrity": "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz", + "integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz", + "integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-toggle": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.8.tgz", + "integrity": "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.11", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.8", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-aria/focus": { + "version": "3.21.4", + "resolved": "https://registry.npmjs.org/@react-aria/focus/-/focus-3.21.4.tgz", + "integrity": "sha512-6gz+j9ip0/vFRTKJMl3R30MHopn4i19HqqLfSQfElxJD+r9hBnYG1Q6Wd/kl/WRR1+CALn2F+rn06jUnf5sT8Q==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/interactions": "^3.27.0", + "@react-aria/utils": "^3.33.0", + "@react-types/shared": "^3.33.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/interactions": { + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@react-aria/interactions/-/interactions-3.27.0.tgz", + "integrity": "sha512-D27pOy+0jIfHK60BB26AgqjjRFOYdvVSkwC31b2LicIzRCSPOSP06V4gMHuGmkhNTF4+YWDi1HHYjxIvMeiSlA==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-aria/utils": "^3.33.0", + "@react-stately/flags": "^3.1.2", + "@react-types/shared": "^3.33.0", + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/ssr": { + "version": "3.9.10", + "resolved": "https://registry.npmjs.org/@react-aria/ssr/-/ssr-3.9.10.tgz", + "integrity": "sha512-hvTm77Pf+pMBhuBm760Li0BVIO38jv1IBws1xFm1NoL26PU+fe+FMW5+VZWyANR6nYL65joaJKZqOdTQMkO9IQ==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-aria/utils": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/@react-aria/utils/-/utils-3.33.0.tgz", + "integrity": "sha512-yvz7CMH8d2VjwbSa5nGXqjU031tYhD8ddax95VzJsHSPyqHDEGfxul8RkhGV6oO7bVqZxVs6xY66NIgae+FHjw==", + "license": "Apache-2.0", + "dependencies": { + "@react-aria/ssr": "^3.9.10", + "@react-stately/flags": "^3.1.2", + "@react-stately/utils": "^3.11.0", + "@react-types/shared": "^3.33.0", + "@swc/helpers": "^0.5.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1", + "react-dom": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-stately/flags": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@react-stately/flags/-/flags-3.1.2.tgz", + "integrity": "sha512-2HjFcZx1MyQXoPqcBGALwWWmgFVUk2TuKVIQxCbRq7fPyWXIl6VHcakCLurdtYC2Iks7zizvz0Idv48MQ38DWg==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + } + }, + "node_modules/@react-stately/utils": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/@react-stately/utils/-/utils-3.11.0.tgz", + "integrity": "sha512-8LZpYowJ9eZmmYLpudbo/eclIRnbhWIJZ994ncmlKlouNzKohtM8qTC6B1w1pwUbiwGdUoyzLuQbeaIor5Dvcw==", + "license": "Apache-2.0", + "dependencies": { + "@swc/helpers": "^0.5.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@react-types/shared": { + "version": "3.33.0", + "resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.33.0.tgz", + "integrity": "sha512-xuUpP6MyuPmJtzNOqF5pzFUIHH2YogyOQfUQHag54PRmWB7AbjuGWBUv0l1UDmz6+AbzAYGmDVAzcRDOu2PFpw==", + "license": "Apache-2.0", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@remirror/core-constants": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@remirror/core-constants/-/core-constants-3.0.0.tgz", + "integrity": "sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==", + "license": "MIT" + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", + "integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", + "integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", + "integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", + "integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", + "integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", + "integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", + "integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", + "integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", + "integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", + "integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", + "integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", + "integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", + "integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", + "integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", + "integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", + "integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", + "integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.9.5.tgz", + "integrity": "sha512-Dq1bqBdLaZ1Gb/l2e5/+o3B18+8TI9ANlA1SkejZqDgdU/jK/ThYaMPMJpVMMXy2uRHvGKbkz9vheVGdq3cJfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", + "integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", + "integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", + "integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", + "integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", + "integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", + "integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", + "integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@stripe/react-stripe-js": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/@stripe/react-stripe-js/-/react-stripe-js-3.10.0.tgz", + "integrity": "sha512-UPqHZwMwDzGSax0ZI7XlxR3tZSpgIiZdk3CiwjbTK978phwR/fFXeAXQcN/h8wTAjR4ZIAzdlI9DbOqJhuJdeg==", + "license": "MIT", + "dependencies": { + "prop-types": "^15.7.2" + }, + "peerDependencies": { + "@stripe/stripe-js": ">=1.44.1 <8.0.0", + "react": ">=16.8.0 <20.0.0", + "react-dom": ">=16.8.0 <20.0.0" + } + }, + "node_modules/@stripe/stripe-js": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-7.9.0.tgz", + "integrity": "sha512-ggs5k+/0FUJcIgNY08aZTqpBTtbExkJMYMLSMwyucrhtWexVOEY1KJmhBsxf+E/Q15f5rbwBpj+t0t2AW2oCsQ==", + "license": "MIT", + "engines": { + "node": ">=12.16" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.18.tgz", + "integrity": "sha512-TXTnIcNJQEKwThMMqBXsZ4VGAza6bvN4pa41Rkqoio6QBKMvo+5lexeTMScGCIxtzgQJzElcvIltani+adC5PQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", + "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.4", + "enhanced-resolve": "^5.18.3", + "jiti": "^2.6.1", + "lightningcss": "1.30.2", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", + "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "license": "MIT", + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-arm64": "4.1.18", + "@tailwindcss/oxide-darwin-x64": "4.1.18", + "@tailwindcss/oxide-freebsd-x64": "4.1.18", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", + "@tailwindcss/oxide-linux-x64-musl": "4.1.18", + "@tailwindcss/oxide-wasm32-wasi": "4.1.18", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", + "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", + "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", + "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", + "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", + "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", + "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", + "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", + "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", + "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", + "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@emnapi/wasi-threads": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.0", + "@tybys/wasm-util": "^0.10.1", + "tslib": "^2.4.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", + "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", + "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", + "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.18", + "@tailwindcss/oxide": "4.1.18", + "tailwindcss": "4.1.18" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.18", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.18.tgz", + "integrity": "sha512-dZkhyfahpvlaV0rIKnvQiVoWPyURppl6w4m9IwMDpuIjcJ1sD9YGWrt0wISvgU7ewACXx2Ct46WPgI6qAD4v6A==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.18", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.18.tgz", + "integrity": "sha512-Mx86Hqu1k39icq2Zusq+Ey2J6dDWTjDvEv43PJtRCoEYTLyfaPnxIQ6iy7YAOK0NV/qOEmZQ/uCufrppZxTgcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tiptap/core": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/core/-/core-2.27.2.tgz", + "integrity": "sha512-ABL1N6eoxzDzC1bYvkMbvyexHacszsKdVPYqhl5GwHLOvpZcv9VE9QaKwDILTyz5voCA0lGcAAXZp+qnXOk5lQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-blockquote": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-2.27.2.tgz", + "integrity": "sha512-oIGZgiAeA4tG3YxbTDfrmENL4/CIwGuP3THtHsNhwRqwsl9SfMk58Ucopi2GXTQSdYXpRJ0ahE6nPqB5D6j/Zw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bold": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-2.27.2.tgz", + "integrity": "sha512-bR7J5IwjCGQ0s3CIxyMvOCnMFMzIvsc5OVZKscTN5UkXzFsaY6muUAIqtKxayBUucjtUskm5qZowJITCeCb1/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bubble-menu": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-2.27.2.tgz", + "integrity": "sha512-VkwlCOcr0abTBGzjPXklJ92FCowG7InU8+Od9FyApdLNmn0utRYGRhw0Zno6VgE9EYr1JY4BRnuSa5f9wlR72w==", + "license": "MIT", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-bullet-list": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-2.27.2.tgz", + "integrity": "sha512-gmFuKi97u5f8uFc/GQs+zmezjiulZmFiDYTh3trVoLRoc2SAHOjGEB7qxdx7dsqmMN7gwiAWAEVurLKIi1lnnw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-code": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-2.27.2.tgz", + "integrity": "sha512-7X9AgwqiIGXoZX7uvdHQsGsjILnN/JaEVtqfXZnPECzKGaWHeK/Ao4sYvIIIffsyZJA8k5DC7ny2/0sAgr2TuA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-code-block": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-2.27.2.tgz", + "integrity": "sha512-KgvdQHS4jXr79aU3wZOGBIZYYl9vCB7uDEuRFV4so2rYrfmiYMw3T8bTnlNEEGe4RUeAms1i4fdwwvQp9nR1Dw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-color": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-color/-/extension-color-2.27.2.tgz", + "integrity": "sha512-sOKCP8/2V3sRM3FdWgMe1lFE5ewsWNCRafiVoujS1+TTHGCj4jw6W+LiumBUk7cRI8kXW/rqGWVC4RVdknYUCA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/extension-text-style": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-document": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-2.27.2.tgz", + "integrity": "sha512-CFhAYsPnyYnosDC4639sCJnBUnYH4Cat9qH5NZWHVvdgtDwu8GZgZn2eSzaKSYXWH1vJ9DSlCK+7UyC3SNXIBA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-dropcursor": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-2.27.2.tgz", + "integrity": "sha512-oEu/OrktNoQXq1x29NnH/GOIzQZm8ieTQl3FK27nxfBPA89cNoH4mFEUmBL5/OFIENIjiYG3qWpg6voIqzswNw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-floating-menu": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-2.27.2.tgz", + "integrity": "sha512-GUN6gPIGXS7ngRJOwdSmtBRBDt9Kt9CM/9pSwKebhLJ+honFoNA+Y6IpVyDvvDMdVNgBchiJLs6qA5H97gAePQ==", + "license": "MIT", + "dependencies": { + "tippy.js": "^6.3.7" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-gapcursor": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-2.27.2.tgz", + "integrity": "sha512-/c9VF1HBxj+AP54XGVgCmD9bEGYc5w5OofYCFQgM7l7PB1J00A4vOke0oPkHJnqnOOyPlFaxO/7N6l3XwFcnKA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-hard-break": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.27.2.tgz", + "integrity": "sha512-kSRVGKlCYK6AGR0h8xRkk0WOFGXHIIndod3GKgWU49APuIGDiXd8sziXsSlniUsWmqgDmDXcNnSzPcV7AQ8YNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-heading": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-2.27.2.tgz", + "integrity": "sha512-iM3yeRWuuQR/IRQ1djwNooJGfn9Jts9zF43qZIUf+U2NY8IlvdNsk2wTOdBgh6E0CamrStPxYGuln3ZS4fuglw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-history": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.27.2.tgz", + "integrity": "sha512-+hSyqERoFNTWPiZx4/FCyZ/0eFqB9fuMdTB4AC/q9iwu3RNWAQtlsJg5230bf/qmyO6bZxRUc0k8p4hrV6ybAw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-horizontal-rule": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.27.2.tgz", + "integrity": "sha512-WGWUSgX+jCsbtf9Y9OCUUgRZYuwjVoieW5n6mAUohJ9/6gc6sGIOrUpBShf+HHo6WD+gtQjRd+PssmX3NPWMpg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-italic": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-2.27.2.tgz", + "integrity": "sha512-1OFsw2SZqfaqx5Fa5v90iNlPRcqyt+lVSjBwTDzuPxTPFY4Q0mL89mKgkq2gVHYNCiaRkXvFLDxaSvBWbmthgg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-link": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-2.27.2.tgz", + "integrity": "sha512-bnP61qkr0Kj9Cgnop1hxn2zbOCBzNtmawxr92bVTOE31fJv6FhtCnQiD6tuPQVGMYhcmAj7eihtvuEMFfqEPcQ==", + "license": "MIT", + "dependencies": { + "linkifyjs": "^4.3.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-list-item": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-2.27.2.tgz", + "integrity": "sha512-eJNee7IEGXMnmygM5SdMGDC8m/lMWmwNGf9fPCK6xk0NxuQRgmZHL6uApKcdH6gyNcRPHCqvTTkhEP7pbny/fg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-ordered-list": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-2.27.2.tgz", + "integrity": "sha512-M7A4tLGJcLPYdLC4CI2Gwl8LOrENQW59u3cMVa+KkwG1hzSJyPsbDpa1DI6oXPC2WtYiTf22zrbq3gVvH+KA2w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-paragraph": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-2.27.2.tgz", + "integrity": "sha512-elYVn2wHJJ+zB9LESENWOAfI4TNT0jqEN34sMA/hCtA4im1ZG2DdLHwkHIshj/c4H0dzQhmsS/YmNC5Vbqab/A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-strike": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-2.27.2.tgz", + "integrity": "sha512-HHIjhafLhS2lHgfAsCwC1okqMsQzR4/mkGDm4M583Yftyjri1TNA7lzhzXWRFWiiMfJxKtdjHjUAQaHuteRTZw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-2.27.2.tgz", + "integrity": "sha512-Xk7nYcigljAY0GO9hAQpZ65ZCxqOqaAlTPDFcKerXmlkQZP/8ndx95OgUb1Xf63kmPOh3xypurGS2is3v0MXSA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-align": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-2.27.2.tgz", + "integrity": "sha512-0Pyks6Hu+Q/+9+5/osoSv0SP6jIerdWMYbi13aaZLsJoj3lBj5WNaE11JtAwSFN5sx0IbqhDSlp1zkvRnzgZ8g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/extension-text-style": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-style/-/extension-text-style-2.27.2.tgz", + "integrity": "sha512-Omk+uxjJLyEY69KStpCw5fA9asvV+MGcAX2HOxyISDFoLaL49TMrNjhGAuz09P1L1b0KGXo4ml7Q3v/Lfy4WPA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0" + } + }, + "node_modules/@tiptap/pm": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-2.27.2.tgz", + "integrity": "sha512-kaEg7BfiJPDQMKbjVIzEPO3wlcA+pZb2tlcK9gPrdDnEFaec2QTF1sXz2ak2IIb2curvnIrQ4yrfHgLlVA72wA==", + "license": "MIT", + "dependencies": { + "prosemirror-changeset": "^2.3.0", + "prosemirror-collab": "^1.3.1", + "prosemirror-commands": "^1.6.2", + "prosemirror-dropcursor": "^1.8.1", + "prosemirror-gapcursor": "^1.3.2", + "prosemirror-history": "^1.4.1", + "prosemirror-inputrules": "^1.4.0", + "prosemirror-keymap": "^1.2.2", + "prosemirror-markdown": "^1.13.1", + "prosemirror-menu": "^1.2.4", + "prosemirror-model": "^1.23.0", + "prosemirror-schema-basic": "^1.2.3", + "prosemirror-schema-list": "^1.4.1", + "prosemirror-state": "^1.4.3", + "prosemirror-tables": "^1.6.4", + "prosemirror-trailing-node": "^3.0.0", + "prosemirror-transform": "^1.10.2", + "prosemirror-view": "^1.37.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@tiptap/react": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/react/-/react-2.27.2.tgz", + "integrity": "sha512-0EAs8Cpkfbvben1PZ34JN2Nd79Dhioynm2jML27DBbf1VWPk+FFWFGTMLUT0bu+Np5iVxio8fqV9t0mc4D6thA==", + "license": "MIT", + "dependencies": { + "@tiptap/extension-bubble-menu": "^2.27.2", + "@tiptap/extension-floating-menu": "^2.27.2", + "@types/use-sync-external-store": "^0.0.6", + "fast-deep-equal": "^3", + "use-sync-external-store": "^1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.7.0", + "@tiptap/pm": "^2.7.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@tiptap/starter-kit": { + "version": "2.27.2", + "resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-2.27.2.tgz", + "integrity": "sha512-bb0gJvPoDuyRUQ/iuN52j1//EtWWttw+RXAv1uJxfR0uKf8X7uAqzaOOgwjknoCIDC97+1YHwpGdnRjpDkOBxw==", + "license": "MIT", + "dependencies": { + "@tiptap/core": "^2.27.2", + "@tiptap/extension-blockquote": "^2.27.2", + "@tiptap/extension-bold": "^2.27.2", + "@tiptap/extension-bullet-list": "^2.27.2", + "@tiptap/extension-code": "^2.27.2", + "@tiptap/extension-code-block": "^2.27.2", + "@tiptap/extension-document": "^2.27.2", + "@tiptap/extension-dropcursor": "^2.27.2", + "@tiptap/extension-gapcursor": "^2.27.2", + "@tiptap/extension-hard-break": "^2.27.2", + "@tiptap/extension-heading": "^2.27.2", + "@tiptap/extension-history": "^2.27.2", + "@tiptap/extension-horizontal-rule": "^2.27.2", + "@tiptap/extension-italic": "^2.27.2", + "@tiptap/extension-list-item": "^2.27.2", + "@tiptap/extension-ordered-list": "^2.27.2", + "@tiptap/extension-paragraph": "^2.27.2", + "@tiptap/extension-strike": "^2.27.2", + "@tiptap/extension-text": "^2.27.2", + "@tiptap/extension-text-style": "^2.27.2", + "@tiptap/pm": "^2.27.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "license": "MIT", + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", + "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/prismjs": { + "version": "1.26.5", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", + "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", + "license": "MIT" + }, + "node_modules/@types/qrcode": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/qrcode/-/qrcode-1.5.6.tgz", + "integrity": "sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/react": { + "version": "19.2.13", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz", + "integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==", + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", + "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/type-utils": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.55.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", + "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", + "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.55.0", + "@typescript-eslint/types": "^8.55.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", + "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", + "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", + "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", + "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", + "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.55.0", + "@typescript-eslint/tsconfig-utils": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/visitor-keys": "8.55.0", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", + "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.55.0", + "@typescript-eslint/types": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", + "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.55.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001769", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001769.tgz", + "integrity": "sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/concurrently": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz", + "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==", + "license": "MIT", + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.8.3", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" + }, + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.12" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "license": "MIT", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.2", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz", + "integrity": "sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/i18next": { + "version": "25.8.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.8.4.tgz", + "integrity": "sha512-a9A0MnUjKvzjEN/26ZY1okpra9kA8MEwzYEz1BNm+IyxUKPRH6ihf0p7vj8YvULwZHKHl3zkJ6KOt4hewxBecQ==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/i18next-browser-languagedetector": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.0.tgz", + "integrity": "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.2" + } + }, + "node_modules/i18next-http-backend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/i18next-http-backend/-/i18next-http-backend-3.0.2.tgz", + "integrity": "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g==", + "license": "MIT", + "dependencies": { + "cross-fetch": "4.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbarcode": { + "version": "3.12.3", + "resolved": "https://registry.npmjs.org/jsbarcode/-/jsbarcode-3.12.3.tgz", + "integrity": "sha512-CuHU9hC6dPsHF5oVFMo8NW76uQVjH4L22CsP4hW+dNnGywJHC/B0ThA1CTDVLnxKLrrpYdicBLnd2xsgTfRnvg==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/laravel-precognition": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-1.0.2.tgz", + "integrity": "sha512-0H08JDdMWONrL/N314fvsO3FATJwGGlFKGkMF3nNmizVFJaWs17816iM+sX7Rp8d5hUjYCx6WLfsehSKfaTxjg==", + "license": "MIT", + "dependencies": { + "axios": "^1.4.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/laravel-vite-plugin": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.3.0.tgz", + "integrity": "sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "vite-plugin-full-reload": "^1.1.0" + }, + "bin": { + "clean-orphaned-assets": "bin/clean.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", + "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.30.2", + "lightningcss-darwin-arm64": "1.30.2", + "lightningcss-darwin-x64": "1.30.2", + "lightningcss-freebsd-x64": "1.30.2", + "lightningcss-linux-arm-gnueabihf": "1.30.2", + "lightningcss-linux-arm64-gnu": "1.30.2", + "lightningcss-linux-arm64-musl": "1.30.2", + "lightningcss-linux-x64-gnu": "1.30.2", + "lightningcss-linux-x64-musl": "1.30.2", + "lightningcss-win32-arm64-msvc": "1.30.2", + "lightningcss-win32-x64-msvc": "1.30.2" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", + "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", + "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", + "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", + "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", + "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", + "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", + "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", + "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", + "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", + "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", + "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/linkifyjs": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.2.tgz", + "integrity": "sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==", + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.475.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.475.0.tgz", + "integrity": "sha512-NJzvVu1HwFVeZ+Gwq2q00KygM1aBhy/ZrhY9FsAgJtpB+E4R7uxRk9M2iKvHa6/vNxZydIB59htha4c2vvwvVg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-it": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.1.tgz", + "integrity": "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/orderedmap": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz", + "integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==", + "license": "MIT" + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.12.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz", + "integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==", + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-organize-imports": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.3.0.tgz", + "integrity": "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "prettier": ">=2.0", + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" + }, + "peerDependenciesMeta": { + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.6.14", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.14.tgz", + "integrity": "sha512-pi2e/+ZygeIqntN+vC573BcW5Cve8zUB0SSAGxqpB4f96boZF4M3phPVoOFCeypwkpRYdi7+jQ5YJJUwrkGUAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "@ianvs/prettier-plugin-sort-imports": "*", + "@prettier/plugin-hermes": "*", + "@prettier/plugin-oxc": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "@zackad/prettier-plugin-twig": "*", + "prettier": "^3.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-marko": "*", + "prettier-plugin-multiline-arrays": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-sort-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*" + }, + "peerDependenciesMeta": { + "@ianvs/prettier-plugin-sort-imports": { + "optional": true + }, + "@prettier/plugin-hermes": { + "optional": true + }, + "@prettier/plugin-oxc": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "@zackad/prettier-plugin-twig": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-marko": { + "optional": true + }, + "prettier-plugin-multiline-arrays": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + } + } + }, + "node_modules/prism-react-renderer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", + "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", + "license": "MIT", + "dependencies": { + "@types/prismjs": "^1.26.0", + "clsx": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.0.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/prosemirror-changeset": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.3.1.tgz", + "integrity": "sha512-j0kORIBm8ayJNl3zQvD1TTPHJX3g042et6y/KQhZhnPrruO8exkTgG8X+NRpj7kIyMMEx74Xb3DyMIBtO0IKkQ==", + "license": "MIT", + "dependencies": { + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-collab": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/prosemirror-collab/-/prosemirror-collab-1.3.1.tgz", + "integrity": "sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-commands": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz", + "integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.10.2" + } + }, + "node_modules/prosemirror-dropcursor": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz", + "integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0", + "prosemirror-view": "^1.1.0" + } + }, + "node_modules/prosemirror-gapcursor": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.0.tgz", + "integrity": "sha512-z00qvurSdCEWUIulij/isHaqu4uLS8r/Fi61IbjdIPJEonQgggbJsLnstW7Lgdk4zQ68/yr6B6bf7sJXowIgdQ==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.0.0", + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-view": "^1.0.0" + } + }, + "node_modules/prosemirror-history": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz", + "integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.2.2", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.31.0", + "rope-sequence": "^1.3.0" + } + }, + "node_modules/prosemirror-inputrules": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-inputrules/-/prosemirror-inputrules-1.5.1.tgz", + "integrity": "sha512-7wj4uMjKaXWAQ1CDgxNzNtR9AlsuwzHfdFH1ygEHA2KHF2DOEaXl1CJfNPAKCg9qNEh4rum975QLaCiQPyY6Fw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.0.0" + } + }, + "node_modules/prosemirror-keymap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz", + "integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==", + "license": "MIT", + "dependencies": { + "prosemirror-state": "^1.0.0", + "w3c-keyname": "^2.2.0" + } + }, + "node_modules/prosemirror-markdown": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/prosemirror-markdown/-/prosemirror-markdown-1.13.4.tgz", + "integrity": "sha512-D98dm4cQ3Hs6EmjK500TdAOew4Z03EV71ajEFiWra3Upr7diytJsjF4mPV2dW+eK5uNectiRj0xFxYI9NLXDbw==", + "license": "MIT", + "dependencies": { + "@types/markdown-it": "^14.0.0", + "markdown-it": "^14.0.0", + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-menu": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/prosemirror-menu/-/prosemirror-menu-1.2.5.tgz", + "integrity": "sha512-qwXzynnpBIeg1D7BAtjOusR+81xCp53j7iWu/IargiRZqRjGIlQuu1f3jFi+ehrHhWMLoyOQTSRx/IWZJqOYtQ==", + "license": "MIT", + "dependencies": { + "crelt": "^1.0.0", + "prosemirror-commands": "^1.0.0", + "prosemirror-history": "^1.0.0", + "prosemirror-state": "^1.0.0" + } + }, + "node_modules/prosemirror-model": { + "version": "1.25.4", + "resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz", + "integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==", + "license": "MIT", + "dependencies": { + "orderedmap": "^2.0.0" + } + }, + "node_modules/prosemirror-schema-basic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/prosemirror-schema-basic/-/prosemirror-schema-basic-1.2.4.tgz", + "integrity": "sha512-ELxP4TlX3yr2v5rM7Sb70SqStq5NvI15c0j9j/gjsrO5vaw+fnnpovCLEGIcpeGfifkuqJwl4fon6b+KdrODYQ==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.25.0" + } + }, + "node_modules/prosemirror-schema-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz", + "integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.7.3" + } + }, + "node_modules/prosemirror-state": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz", + "integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.0.0", + "prosemirror-transform": "^1.0.0", + "prosemirror-view": "^1.27.0" + } + }, + "node_modules/prosemirror-tables": { + "version": "1.8.5", + "resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz", + "integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==", + "license": "MIT", + "dependencies": { + "prosemirror-keymap": "^1.2.3", + "prosemirror-model": "^1.25.4", + "prosemirror-state": "^1.4.4", + "prosemirror-transform": "^1.10.5", + "prosemirror-view": "^1.41.4" + } + }, + "node_modules/prosemirror-trailing-node": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prosemirror-trailing-node/-/prosemirror-trailing-node-3.0.0.tgz", + "integrity": "sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==", + "license": "MIT", + "dependencies": { + "@remirror/core-constants": "3.0.0", + "escape-string-regexp": "^4.0.0" + }, + "peerDependencies": { + "prosemirror-model": "^1.22.1", + "prosemirror-state": "^1.4.2", + "prosemirror-view": "^1.33.8" + } + }, + "node_modules/prosemirror-transform": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.11.0.tgz", + "integrity": "sha512-4I7Ce4KpygXb9bkiPS3hTEk4dSHorfRw8uI0pE8IhxlK2GXsqv5tIA7JUSxtSu7u8APVOTtbUBxTmnHIxVkIJw==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.21.0" + } + }, + "node_modules/prosemirror-view": { + "version": "1.41.6", + "resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.6.tgz", + "integrity": "sha512-mxpcDG4hNQa/CPtzxjdlir5bJFDlm0/x5nGBbStB2BWX+XOQ9M8ekEG+ojqB5BcVu2Rc80/jssCMZzSstJuSYg==", + "license": "MIT", + "dependencies": { + "prosemirror-model": "^1.20.0", + "prosemirror-state": "^1.0.0", + "prosemirror-transform": "^1.1.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC" + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", + "license": "MIT" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-barcode": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/react-barcode/-/react-barcode-1.6.1.tgz", + "integrity": "sha512-pc4ftnO5syHa/UjCruEeRsomlhoxKSugIgTA8T4dH0fvc89UMHL+/1Sp25IAphqG44pJkE5hMXhv89iS09jQyw==", + "license": "ISC", + "dependencies": { + "jsbarcode": "^3.8.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": "16 - 19" + } + }, + "node_modules/react-country-flag": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/react-country-flag/-/react-country-flag-3.1.0.tgz", + "integrity": "sha512-JWQFw1efdv9sTC+TGQvTKXQg1NKbDU2mBiAiRWcKM9F1sK+/zjhP2yGmm8YDddWyZdXVkR8Md47rPMJmo4YO5g==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/react-i18next": { + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", + "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.4.0", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==", + "license": "MIT", + "peer": true + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.2.tgz", + "integrity": "sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/recharts": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.7.0.tgz", + "integrity": "sha512-l2VCsy3XXeraxIID9fx23eCb6iCBsxUQDnE8tWm6DFdszVAO7WVY/ChAD9wVit01y6B2PMupYiMmQwhgPHc9Ew==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC" + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz", + "integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.57.1", + "@rollup/rollup-android-arm64": "4.57.1", + "@rollup/rollup-darwin-arm64": "4.57.1", + "@rollup/rollup-darwin-x64": "4.57.1", + "@rollup/rollup-freebsd-arm64": "4.57.1", + "@rollup/rollup-freebsd-x64": "4.57.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", + "@rollup/rollup-linux-arm-musleabihf": "4.57.1", + "@rollup/rollup-linux-arm64-gnu": "4.57.1", + "@rollup/rollup-linux-arm64-musl": "4.57.1", + "@rollup/rollup-linux-loong64-gnu": "4.57.1", + "@rollup/rollup-linux-loong64-musl": "4.57.1", + "@rollup/rollup-linux-ppc64-gnu": "4.57.1", + "@rollup/rollup-linux-ppc64-musl": "4.57.1", + "@rollup/rollup-linux-riscv64-gnu": "4.57.1", + "@rollup/rollup-linux-riscv64-musl": "4.57.1", + "@rollup/rollup-linux-s390x-gnu": "4.57.1", + "@rollup/rollup-linux-x64-gnu": "4.57.1", + "@rollup/rollup-linux-x64-musl": "4.57.1", + "@rollup/rollup-openbsd-x64": "4.57.1", + "@rollup/rollup-openharmony-arm64": "4.57.1", + "@rollup/rollup-win32-arm64-msvc": "4.57.1", + "@rollup/rollup-win32-ia32-msvc": "4.57.1", + "@rollup/rollup-win32-x64-gnu": "4.57.1", + "@rollup/rollup-win32-x64-msvc": "4.57.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.57.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", + "integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rope-sequence": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz", + "integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==", + "license": "MIT" + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", + "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-scrollbar": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/tailwind-scrollbar/-/tailwind-scrollbar-4.0.2.tgz", + "integrity": "sha512-wAQiIxAPqk0MNTPptVe/xoyWi27y+NRGnTwvn4PQnbvB9kp8QUBiGl/wsfoVBHnQxTmhXJSNt9NHTmcz9EivFA==", + "license": "MIT", + "dependencies": { + "prism-react-renderer": "^2.4.1" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "tailwindcss": "4.x" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", + "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", + "license": "MIT" + }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tippy.js": { + "version": "6.3.7", + "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz", + "integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==", + "license": "MIT", + "dependencies": { + "@popperjs/core": "^2.9.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", + "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.55.0", + "@typescript-eslint/parser": "8.55.0", + "@typescript-eslint/typescript-estree": "8.55.0", + "@typescript-eslint/utils": "8.55.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz", + "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "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 + } + } + }, + "node_modules/vite-plugin-full-reload": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz", + "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==", + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "picomatch": "^2.3.1" + } + }, + "node_modules/vite-plugin-full-reload/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC" + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 000000000..da7d2340a --- /dev/null +++ b/package.json @@ -0,0 +1,98 @@ +{ + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "build:ssr": "vite build && vite build --ssr", + "dev": "vite", + "format": "prettier --write resources/", + "format:check": "prettier --check resources/", + "lint": "eslint . --fix", + "types": "tsc --noEmit" + }, + "devDependencies": { + "@eslint/js": "^9.19.0", + "@types/node": "^22.13.5", + "eslint": "^9.17.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-react": "^7.37.3", + "eslint-plugin-react-hooks": "^5.1.0", + "prettier": "^3.4.2", + "prettier-plugin-organize-imports": "^4.1.0", + "prettier-plugin-tailwindcss": "^0.6.11", + "typescript-eslint": "^8.23.0" + }, + "dependencies": { + "@fullcalendar/daygrid": "^6.1.17", + "@fullcalendar/interaction": "^6.1.17", + "@fullcalendar/react": "^6.1.17", + "@fullcalendar/timegrid": "^6.1.17", + "@headlessui/react": "^2.2.0", + "@hello-pangea/dnd": "^18.0.1", + "@inertiajs/react": "^2.0.0", + "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.4", + "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-dropdown-menu": "^2.1.6", + "@radix-ui/react-label": "^2.1.2", + "@radix-ui/react-navigation-menu": "^1.2.5", + "@radix-ui/react-popover": "^1.1.3", + "@radix-ui/react-progress": "^1.1.7", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-scroll-area": "^1.2.9", + "@radix-ui/react-select": "^2.1.6", + "@radix-ui/react-separator": "^1.1.2", + "@radix-ui/react-slot": "^1.1.2", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tabs": "^1.1.12", + "@radix-ui/react-toggle": "^1.1.2", + "@radix-ui/react-toggle-group": "^1.1.2", + "@radix-ui/react-tooltip": "^1.1.8", + "@stripe/react-stripe-js": "^3.7.0", + "@stripe/stripe-js": "^7.4.0", + "@tailwindcss/oxide": "^4.1.10", + "@tailwindcss/vite": "^4.0.6", + "@tiptap/extension-color": "^2.23.0", + "@tiptap/extension-link": "^2.23.0", + "@tiptap/extension-text-align": "^2.23.0", + "@tiptap/extension-text-style": "^2.23.0", + "@tiptap/pm": "^2.23.0", + "@tiptap/react": "^2.23.0", + "@tiptap/starter-kit": "^2.23.0", + "@types/qrcode": "^1.5.5", + "@types/react": "^19.0.3", + "@types/react-dom": "^19.0.2", + "@vitejs/plugin-react": "^4.3.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "concurrently": "^9.0.1", + "date-fns": "^4.1.0", + "globals": "^15.14.0", + "i18next": "^25.2.1", + "i18next-browser-languagedetector": "^8.2.0", + "i18next-http-backend": "^3.0.2", + "laravel-vite-plugin": "^1.0", + "lucide-react": "^0.475.0", + "next-themes": "^0.4.6", + "qrcode": "^1.5.4", + "react": "^19.0.0", + "react-barcode": "^1.6.1", + "react-country-flag": "^3.1.0", + "react-dom": "^19.0.0", + "react-i18next": "^15.5.3", + "recharts": "^3.0.2", + "sonner": "^2.0.3", + "tailwind-merge": "^3.0.1", + "tailwind-scrollbar": "^4.0.2", + "tailwindcss": "^4.0.0", + "tailwindcss-animate": "^1.0.7", + "typescript": "^5.7.2", + "vite": "^6.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.9.5", + "@tailwindcss/oxide-linux-x64-gnu": "^4.0.1", + "lightningcss-linux-x64-gnu": "^1.29.1" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 000000000..61c031c47 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,33 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + diff --git a/public/.DS_Store b/public/.DS_Store new file mode 100755 index 000000000..bf242b320 Binary files /dev/null and b/public/.DS_Store differ diff --git a/public/.htaccess b/public/.htaccess new file mode 100755 index 000000000..b574a597d --- /dev/null +++ b/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/public/images/avatar/avatar.png b/public/images/avatar/avatar.png new file mode 100755 index 000000000..43e7ab3d6 Binary files /dev/null and b/public/images/avatar/avatar.png differ diff --git a/public/images/default/image-not-found.jpg b/public/images/default/image-not-found.jpg new file mode 100755 index 000000000..e00f52074 Binary files /dev/null and b/public/images/default/image-not-found.jpg differ diff --git a/public/images/logos/favicon.png b/public/images/logos/favicon.png new file mode 100755 index 000000000..afb99bb33 Binary files /dev/null and b/public/images/logos/favicon.png differ diff --git a/public/images/logos/logo-dark.png b/public/images/logos/logo-dark.png new file mode 100755 index 000000000..d8a7f63a5 Binary files /dev/null and b/public/images/logos/logo-dark.png differ diff --git a/public/images/logos/logo-light.png b/public/images/logos/logo-light.png new file mode 100755 index 000000000..47fc71fa4 Binary files /dev/null and b/public/images/logos/logo-light.png differ diff --git a/public/index.php b/public/index.php new file mode 100755 index 000000000..ee8f07e99 --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/public/installer/css/sass/_variables.sass b/public/installer/css/sass/_variables.sass new file mode 100755 index 000000000..72c39a6a7 --- /dev/null +++ b/public/installer/css/sass/_variables.sass @@ -0,0 +1,46 @@ +//colors +$color_0: #ff0 +$color_1: #000 +$color_2: silver +$color_3: #666 +$color_4: #111 +$color_5: #1d73a2 +$color_6: #175c82 +$color_7: rgba(0, 0, 0, .19) +$color_8: rgba(0, 0, 0, .23) +$color_9: #357295 +$color_10: #fff +$color_11: #cacfd2 +$color_12: #34a0db +$color_13: rgba(0, 0, 0, .12) +$color_14: rgba(0, 0, 0, .24) +$color_15: #2490cb +$color_16: #eee +$color_17: #222 +$color_18: rgba(0, 0, 0, .16) +$color_19: #2ecc71 +$color_20: #e74c3c +$color_21: #f5f5f5 +$color_22: rgba(0, 0, 0, .2) + +//fonts +$font_0: Ionicons +$font_1: sans-serif +$font_2: monospace +$font_3: Roboto +$font_4: Helvetica Neue +$font_5: Helvetica +$font_6: Arial +$font_7: Courier New +$font_8: Courier +$font_9: Lucida Sans Typewriter +$font_10: Lucida Typewriter + +//urls +$url_0: url(https://fonts.googleapis.com/css?family=Roboto:400,300,500,700,900) +$url_1: url(../fonts/ionicons.eot?v=2.0.1) +$url_2: url(../fonts/ionicons.eot?v=2.0.1#iefix) +$url_3: url(../fonts/ionicons.ttf?v=2.0.1) +$url_4: url(../fonts/ionicons.woff?v=2.0.1) +$url_5: url(../fonts/ionicons.svg?v=2.0.1#Ionicons) +$url_6: url(../img/background.png) \ No newline at end of file diff --git a/public/installer/css/sass/style.sass b/public/installer/css/sass/style.sass new file mode 100755 index 000000000..b5d9ba574 --- /dev/null +++ b/public/installer/css/sass/style.sass @@ -0,0 +1,3213 @@ +// Variables +@import "variables"; + +//@extend-elements +//original selectors +//sub, sup +.extend_1 + font-size: 75% + line-height: 0 + position: relative + vertical-align: baseline + + +//original selectors +//button, input, optgroup, select, textarea +.extend_2 + color: inherit + font: inherit + margin: 0 + + +@import $url_0; +html + font-family: $font_1 + -ms-text-size-adjust: 100% + -webkit-text-size-adjust: 100% + font-family: $font_3, $font_4, $font_5, $font_6, $font_1 + font-weight: 300 + color: $color_3 + font-size: 12px + line-height: 1.75em + input[type=button] + -webkit-appearance: button + cursor: pointer + + input[disabled] + cursor: default + + +body + margin: 0 + text-rendering: optimizeLegibility + -webkit-font-smoothing: antialiased + -moz-osx-font-smoothing: grayscale + -moz-font-feature-settings: "liga" on + +article + display: block + +aside + display: block + +details + display: block + +figcaption + display: block + +figure + display: block + margin: 1em 40px + +footer + display: block + +header + display: block + +hgroup + display: block + +main + display: block + +menu + display: block + +nav + display: block + +section + display: block + +summary + display: block + +audio + display: inline-block + vertical-align: baseline + &:not([controls]) + display: none + height: 0 + + +canvas + display: inline-block + vertical-align: baseline + +progress + display: inline-block + vertical-align: baseline + +video + display: inline-block + vertical-align: baseline + +[hidden] + display: none + +template + display: none + +a + background-color: transparent + text-decoration: none + color: $color_5 + //If you use compass, instead of the line below you could use + transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: all .2s + margin: 0 + padding: 0 + &:active + outline: 0 + + &:hover + outline: 0 + color: $color_6 + + +abbr[title] + border-bottom: 1px dotted + +b + font-weight: 700 + margin: 0 + padding: 0 + +strong + font-weight: 700 + margin: 0 + padding: 0 + +dfn + font-style: italic + margin: 0 + padding: 0 + +h1 + font-size: 2em + margin: .67em 0 + font-size: 27.85438995234061px + margin-top: .942400822452556em + line-height: 1.130880986943067em + margin-bottom: .188480164490511em + margin: 0 + padding: 0 + font-family: $font_3, $font_4, $font_5, $font_6, $font_1 + font-weight: 500 + color: $color_4 + clear: both + +mark + background: $color_0 + color: $color_1 + +small + font-size: 80% + margin: 0 + padding: 0 + line-height: 0 + +sub + @extend %extend_1 + bottom: -.25em + margin: 0 + padding: 0 + line-height: 0 + +sup + @extend %extend_1 + top: -.5em + margin: 0 + padding: 0 + line-height: 0 + +img + border: 0 + margin: 0 + padding: 0 + +hr + //If you use compass, instead of the line below you could use + box-sizing($bs) + box-sizing: content-box + height: 0 + +pre + overflow: auto + padding: .875em + margin-bottom: 1.75em + font-family: $font_2 + font-size: 1em + margin: 0 + padding: 0 + margin-bottom: 1.75em + code + padding: 0 + + +code + font-family: $font_2 + font-size: 1em + margin: 0 + padding: 0 + font-family: $font_7, $font_8, $font_9, $font_10, $font_2 + padding: .0875em .2625em + line-height: 0 + +kbd + font-family: $font_2 + font-size: 1em + margin: 0 + padding: 0 + +samp + font-family: $font_2 + font-size: 1em + margin: 0 + padding: 0 + +button + @extend %extend_2 + overflow: visible + text-transform: none + -webkit-appearance: button + cursor: pointer + display: block + cursor: pointer + font-size: 12px + padding: .4375em 1.75em + margin-bottom: 1.18125em + +input + @extend %extend_2 + line-height: normal + +optgroup + @extend %extend_2 + font-weight: 700 + +select + @extend %extend_2 + text-transform: none + +textarea + @extend %extend_2 + overflow: auto + display: block + max-width: 100% + padding: .4375em + font-size: 12px + margin-bottom: 1.18125em + +input[type=reset] + -webkit-appearance: button + cursor: pointer + +input[type=submit] + -webkit-appearance: button + cursor: pointer + display: block + cursor: pointer + font-size: 12px + padding: .4375em 1.75em + margin-bottom: 1.18125em + +button[disabled] + cursor: default + +button::-moz-focus-inner + border: 0 + padding: 0 + +input::-moz-focus-inner + border: 0 + padding: 0 + +input[type=checkbox] + //If you use compass, instead of the line below you could use + box-sizing($bs) + box-sizing: border-box + padding: 0 + +input[type=radio] + //If you use compass, instead of the line below you could use + box-sizing($bs) + box-sizing: border-box + padding: 0 + +input[type=number]::-webkit-inner-spin-button + height: auto + +input[type=number]::-webkit-outer-spin-button + height: auto + +input[type=search] + -webkit-appearance: textfield + //If you use compass, instead of the line below you could use + box-sizing($bs) + box-sizing: content-box + +input[type=search]::-webkit-search-cancel-button + -webkit-appearance: none + +input[type=search]::-webkit-search-decoration + -webkit-appearance: none + +fieldset + border: 1px solid $color_2 + margin: 0 2px + padding: .35em .625em .75em + padding: .875em 1.75em 1.75em + border-width: 1px + border-style: solid + max-width: 100% + margin-bottom: 1.8375em + margin: 0 + padding: 0 + button + margin-bottom: 0 + + input[type=submit] + margin-bottom: 0 + + +legend + border: 0 + padding: 0 + color: $color_4 + font-weight: 700 + margin: 0 + padding: 0 + +table + width: 100% + border-spacing: 0 + border-collapse: collapse + margin-bottom: 2.1875em + margin: 0 + padding: 0 + margin-bottom: 1.75em + +td + padding: 0 + margin: 0 + padding: 0 + padding: .21875em .875em + +th + padding: 0 + margin: 0 + padding: 0 + text-align: left + color: $color_4 + padding: .21875em .875em + +@font-face + font-family: $font_0 + src: $url_1 + src: $url_2 format("embedded-opentype"), $url_3 format("truetype"), $url_4 format("woff"), $url_5 format("svg") + font-weight: 400 + font-style: normal + +@media(min-width:600px) + html + font-size: calc(12px +8 *((100vw - 600px) / 540)) + + h1 + font-size: calc(27.85438995234061px +18.56959 *((100vw - 600px) / 540)) + + h2 + font-size: calc(23.53700340860508px +15.69134 *((100vw - 600px) / 540)) + + h3 + font-size: calc(19.888804974891777px +13.2592 *((100vw - 600px) / 540)) + + h4 + font-size: calc(16.806071548796314px +11.20405 *((100vw - 600px) / 540)) + + h5 + font-size: calc(14.201156945318074px +9.46744 *((100vw - 600px) / 540)) + + h6 + font-size: calc(12px +8 *((100vw - 600px) / 540)) + + input[type=email] + font-size: calc(12px +8 *((100vw - 600px) / 540)) + + input[type=password] + font-size: calc(12px +8 *((100vw - 600px) / 540)) + + input[type=text] + font-size: calc(12px +8 *((100vw - 600px) / 540)) + + textarea + font-size: calc(12px +8 *((100vw - 600px) / 540)) + + button + font-size: calc(12px +8 *((100vw - 600px) / 540)) + + input[type=submit] + font-size: calc(12px +8 *((100vw - 600px) / 540)) + + +@media(min-width:1140px) + html + font-size: 20px + + h1 + font-size: 46.423983253901014px + margin-top: .942400822452556em + line-height: 1.130880986943067em + margin-bottom: .188480164490511em + + h2 + font-size: 39.228339014341806px + margin-top: 1.115265165420465em + line-height: 1.338318198504558em + margin-bottom: .240111086421698em + + h3 + font-size: 33.14800829148629px + margin-top: 1.319837970815179em + line-height: 1.583805564978215em + margin-bottom: .287857499569283em + + h4 + font-size: 28.01011924799386px + margin-top: 1.561935513828041em + line-height: 1.87432261659365em + margin-bottom: .345845057728222em + + h5 + font-size: 23.668594908863454px + margin-top: 1.84844094752817em + line-height: 2.218129137033805em + margin-bottom: .369688189505634em + + h6 + font-size: 20px + margin-top: 2.1875em + line-height: 2.625em + margin-bottom: .473958333333333em + + fieldset + margin-bottom: 2.078125em + + input[type=email] + font-size: 20px + margin-bottom: .5140625em + + input[type=password] + font-size: 20px + margin-bottom: .5140625em + + input[type=text] + font-size: 20px + margin-bottom: .5140625em + + textarea + font-size: 20px + margin-bottom: .5140625em + + button + font-size: 20px + margin-bottom: 1.3125em + + input[type=submit] + font-size: 20px + margin-bottom: 1.3125em + + table + margin-bottom: 2.05625em + + th + padding: .4375em .875em + + td + padding: .4375em .875em + + +abbr + margin: 0 + padding: 0 + border-bottom: 1px dotted currentColor + cursor: help + +acronym + margin: 0 + padding: 0 + border-bottom: 1px dotted currentColor + cursor: help + +address + margin: 0 + padding: 0 + margin-bottom: 1.75em + font-style: normal + +big + margin: 0 + padding: 0 + line-height: 0 + +blockquote + margin: 0 + padding: 0 + margin-bottom: 1.75em + font-style: italic + cite + display: block + font-style: normal + + +caption + margin: 0 + padding: 0 + +center + margin: 0 + padding: 0 + +cite + margin: 0 + padding: 0 + +dd + margin: 0 + padding: 0 + +del + margin: 0 + padding: 0 + +dl + margin: 0 + padding: 0 + margin-bottom: 1.75em + +dt + margin: 0 + padding: 0 + color: $color_4 + font-weight: 700 + +em + margin: 0 + padding: 0 + +form + margin: 0 + padding: 0 + +h2 + margin: 0 + padding: 0 + font-family: $font_3, $font_4, $font_5, $font_6, $font_1 + font-weight: 500 + color: $color_4 + clear: both + font-size: 23.53700340860508px + margin-top: 1.115265165420465em + line-height: 1.338318198504558em + margin-bottom: .251483121980101em + +h3 + margin: 0 + padding: 0 + font-family: $font_3, $font_4, $font_5, $font_6, $font_1 + font-weight: 500 + color: $color_4 + clear: both + font-size: 19.888804974891777px + margin-top: 1.319837970815179em + line-height: 1.583805564978215em + margin-bottom: .303784103173448em + +h4 + margin: 0 + padding: 0 + font-family: $font_3, $font_4, $font_5, $font_6, $font_1 + font-weight: 500 + color: $color_4 + clear: both + font-size: 16.806071548796314px + margin-top: 1.561935513828041em + line-height: 1.87432261659365em + margin-bottom: .368150361036632em + +h5 + margin: 0 + padding: 0 + font-family: $font_3, $font_4, $font_5, $font_6, $font_1 + font-weight: 500 + color: $color_4 + clear: both + font-size: 14.201156945318074px + margin-top: 1.84844094752817em + line-height: 2.218129137033805em + margin-bottom: .369688189505634em + +h6 + margin: 0 + padding: 0 + font-family: $font_3, $font_4, $font_5, $font_6, $font_1 + font-weight: 500 + color: $color_4 + clear: both + font-size: 12px + margin-top: 2.1875em + line-height: 2.625em + margin-bottom: .619791666666667em + +i + margin: 0 + padding: 0 + +ins + margin: 0 + padding: 0 + +label + margin: 0 + padding: 0 + display: block + padding-bottom: .21875em + margin-bottom: -.21875em + +li + margin: 0 + padding: 0 + +ol + margin: 0 + padding: 0 + margin-bottom: 1.75em + padding-left: 1.4em + +p + margin: 0 + padding: 0 + margin-bottom: 1.75em + +q + margin: 0 + padding: 0 + +s + margin: 0 + padding: 0 + +strike + margin: 0 + padding: 0 + +tbody + margin: 0 + padding: 0 + +tfoot + margin: 0 + padding: 0 + +thead + margin: 0 + padding: 0 + +tr + margin: 0 + padding: 0 + +tt + margin: 0 + padding: 0 + +u + margin: 0 + padding: 0 + +ul + margin: 0 + padding: 0 + margin-bottom: 1.75em + padding-left: 1.1em + +var + margin: 0 + padding: 0 + +input[type=email] + display: block + max-width: 100% + padding: .4375em + font-size: 12px + margin-bottom: 1.18125em + +input[type=password] + display: block + max-width: 100% + padding: .4375em + font-size: 12px + margin-bottom: 1.18125em + +input[type=text] + display: block + max-width: 100% + padding: .4375em + font-size: 12px + margin-bottom: 1.18125em + +.master + background-image: $url_6 + background-size: cover + background-position: top + min-height: 100vh + display: -webkit-flex + display: -ms-flexbox + display: flex + -webkit-justify-content: center + -ms-flex-pack: center + justify-content: center + -webkit-align-items: center + -ms-flex-align: center + align-items: center + +.box + width: 450px + //If you use compass, instead of the line below you could use + border-radius($radius, $vertical-radius) + border-radius: 0 0 3px 3px + overflow: hidden + //If you use compass, instead of the line below you could use + box-sizing($bs) + box-sizing: border-box + //If you use compass, instead of the line below you could use + box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 10px 10px $color_7, 0 6px 3px $color_8 + +.header + background-color: $color_9 + padding: 30px 30px 40px + //If you use compass, instead of the line below you could use + border-radius($radius, $vertical-radius) + border-radius: 3px 3px 0 0 + text-align: center + +.header__step + font-weight: 300 + text-transform: uppercase + font-size: 14px + letter-spacing: 1.1px + margin: 0 0 10px + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + //If you use compass, instead of the line below you could use + user-select($select) + user-select: none + color: $color_10 + +.header__title + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + //If you use compass, instead of the line below you could use + user-select($select) + user-select: none + color: $color_10 + font-weight: 400 + font-size: 20px + margin: 0 0 15px + +.step + padding-left: 0 + list-style: none + margin-bottom: 0 + display: -webkit-flex + display: -ms-flexbox + display: flex + -webkit-flex-direction: row-reverse + -ms-flex-direction: row-reverse + flex-direction: row-reverse + -webkit-justify-content: center + -ms-flex-pack: center + justify-content: center + -webkit-align-items: center + -ms-flex-align: center + align-items: center + margin-top: -20px + +.step__divider + background-color: $color_11 + -webkit-user-select: none + -moz-user-select: none + -ms-user-select: none + //If you use compass, instead of the line below you could use + user-select($select) + user-select: none + width: 60px + height: 3px + &:first-child + -webkit-flex: 1 0 auto + -ms-flex: 1 0 auto + flex: 1 0 auto + + &:last-child + -webkit-flex: 1 0 auto + -ms-flex: 1 0 auto + flex: 1 0 auto + + +.step__icon + background-color: $color_11 + font-style: normal + width: 40px + height: 40px + display: -webkit-flex + display: -ms-flexbox + display: flex + -webkit-justify-content: center + -ms-flex-pack: center + justify-content: center + -webkit-align-items: center + -ms-flex-align: center + align-items: center + //If you use compass, instead of the line below you could use + border-radius($radius, $vertical-radius) + border-radius: 50% + color: $color_10 + &.welcome:before + content: '\f144' + font-family: $font_0 + + &.requirements:before + content: '\f127' + font-family: $font_0 + + &.permissions:before + content: '\f296' + font-family: $font_0 + + &.database:before + content: '\f454' + font-family: $font_0 + + &.update:before + content: '\f2bf' + font-family: $font_0 + + +.main + margin-top: -20px + background-color: $color_10 + //If you use compass, instead of the line below you could use + border-radius($radius, $vertical-radius) + border-radius: 0 0 3px 3px + padding: 40px 40px 30px + +.buttons + text-align: center + +.buttons--right + text-align: right + +.button + display: inline-block + background-color: $color_12 + //If you use compass, instead of the line below you could use + border-radius($radius, $vertical-radius) + border-radius: 2px + padding: 7px 20px + color: $color_10 + //If you use compass, instead of the line below you could use + box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 1px 1.5px $color_13, 0 1px 1px $color_14 + text-decoration: none + outline: none + border: none + //If you use compass, instead of the line below you could use + transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: box-shadow .2s ease, background-color .2s ease + &:hover + color: $color_10 + //If you use compass, instead of the line below you could use + box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 10px 10px $color_7, 0 6px 3px $color_8 + background-color: $color_15 + + +.button--light + padding: 3px 16px + font-size: 16px + border-top: 1px solid $color_16 + color: $color_17 + background: $color_10 + &:hover + color: $color_17 + background: $color_10 + //If you use compass, instead of the line below you could use + box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 3px 3px $color_18, 0 3px 3px $color_8 + + +.list + padding-left: 0 + list-style: none + margin-bottom: 0 + margin: 20px 0 35px + border: 1px solid $color_13 + //If you use compass, instead of the line below you could use + border-radius($radius, $vertical-radius) + border-radius: 2px + +.list__item + position: relative + overflow: hidden + padding: 7px 20px + border-bottom: 1px solid $color_13 + &:first-letter + text-transform: uppercase + + &:last-child + border-bottom: none + + &:before + display: -webkit-flex + display: -ms-flexbox + display: flex + -webkit-justify-content: center + -ms-flex-pack: center + justify-content: center + -webkit-align-items: center + -ms-flex-align: center + align-items: center + padding: 7px 20px + position: absolute + top: 0 + right: 0 + bottom: 0 + + &.success:before + color: $color_19 + content: '\f120' + font-family: $font_0 + + &.error:before + color: $color_20 + content: '\f128' + font-family: $font_0 + + +.list__item--permissions + &:before + content: ''!important + + span + display: -webkit-flex + display: -ms-flexbox + display: flex + -webkit-justify-content: center + -ms-flex-pack: center + justify-content: center + -webkit-align-items: center + -ms-flex-align: center + align-items: center + padding: 7px 20px + position: absolute + top: 0 + right: 0 + bottom: 0 + background-color: $color_21 + font-weight: 700 + font-size: 16px + &:before + margin-right: 7px + font-weight: 400 + + + &.success span:before + color: $color_19 + content: '\f120' + font-family: $font_0 + + &.error span:before + color: $color_20 + content: '\f128' + font-family: $font_0 + + +.textarea + //If you use compass, instead of the line below you could use + box-sizing($bs) + box-sizing: border-box + width: 100% + font-size: 14px + line-height: 25px + height: 150px + outline: none + border: 1px solid $color_22 + ~ .button + margin-bottom: 35px + + +.alert + margin: 0 0 10px + font-weight: 700 + font-size: 16px + background-color: $color_21 + //If you use compass, instead of the line below you could use + border-radius($radius, $vertical-radius) + border-radius: 2px + padding: 0 10px + +svg:not(:root) + overflow: hidden + +.ion-alert:before + content: "\f101" + +.ion-alert-circled:before + content: "\f100" + +.ion-android-add:before + content: "\f2c7" + +.ion-android-add-circle:before + content: "\f359" + +.ion-android-alarm-clock:before + content: "\f35a" + +.ion-android-alert:before + content: "\f35b" + +.ion-android-apps:before + content: "\f35c" + +.ion-android-archive:before + content: "\f2c9" + +.ion-android-arrow-back:before + content: "\f2ca" + +.ion-android-arrow-down:before + content: "\f35d" + +.ion-android-arrow-dropdown:before + content: "\f35f" + +.ion-android-arrow-dropdown-circle:before + content: "\f35e" + +.ion-android-arrow-dropleft:before + content: "\f361" + +.ion-android-arrow-dropleft-circle:before + content: "\f360" + +.ion-android-arrow-dropright:before + content: "\f363" + +.ion-android-arrow-dropright-circle:before + content: "\f362" + +.ion-android-arrow-dropup:before + content: "\f365" + +.ion-android-arrow-dropup-circle:before + content: "\f364" + +.ion-android-arrow-forward:before + content: "\f30f" + +.ion-android-arrow-up:before + content: "\f366" + +.ion-android-attach:before + content: "\f367" + +.ion-android-bar:before + content: "\f368" + +.ion-android-bicycle:before + content: "\f369" + +.ion-android-boat:before + content: "\f36a" + +.ion-android-bookmark:before + content: "\f36b" + +.ion-android-bulb:before + content: "\f36c" + +.ion-android-bus:before + content: "\f36d" + +.ion-android-calendar:before + content: "\f2d1" + +.ion-android-call:before + content: "\f2d2" + +.ion-android-camera:before + content: "\f2d3" + +.ion-android-cancel:before + content: "\f36e" + +.ion-android-car:before + content: "\f36f" + +.ion-android-cart:before + content: "\f370" + +.ion-android-chat:before + content: "\f2d4" + +.ion-android-checkbox:before + content: "\f374" + +.ion-android-checkbox-blank:before + content: "\f371" + +.ion-android-checkbox-outline:before + content: "\f373" + +.ion-android-checkbox-outline-blank:before + content: "\f372" + +.ion-android-checkmark-circle:before + content: "\f375" + +.ion-android-clipboard:before + content: "\f376" + +.ion-android-close:before + content: "\f2d7" + +.ion-android-cloud:before + content: "\f37a" + +.ion-android-cloud-circle:before + content: "\f377" + +.ion-android-cloud-done:before + content: "\f378" + +.ion-android-cloud-outline:before + content: "\f379" + +.ion-android-color-palette:before + content: "\f37b" + +.ion-android-compass:before + content: "\f37c" + +.ion-android-contact:before + content: "\f2d8" + +.ion-android-contacts:before + content: "\f2d9" + +.ion-android-contract:before + content: "\f37d" + +.ion-android-create:before + content: "\f37e" + +.ion-android-delete:before + content: "\f37f" + +.ion-android-desktop:before + content: "\f380" + +.ion-android-document:before + content: "\f381" + +.ion-android-done:before + content: "\f383" + +.ion-android-done-all:before + content: "\f382" + +.ion-android-download:before + content: "\f2dd" + +.ion-android-drafts:before + content: "\f384" + +.ion-android-exit:before + content: "\f385" + +.ion-android-expand:before + content: "\f386" + +.ion-android-favorite:before + content: "\f388" + +.ion-android-favorite-outline:before + content: "\f387" + +.ion-android-film:before + content: "\f389" + +.ion-android-folder:before + content: "\f2e0" + +.ion-android-folder-open:before + content: "\f38a" + +.ion-android-funnel:before + content: "\f38b" + +.ion-android-globe:before + content: "\f38c" + +.ion-android-hand:before + content: "\f2e3" + +.ion-android-hangout:before + content: "\f38d" + +.ion-android-happy:before + content: "\f38e" + +.ion-android-home:before + content: "\f38f" + +.ion-android-image:before + content: "\f2e4" + +.ion-android-laptop:before + content: "\f390" + +.ion-android-list:before + content: "\f391" + +.ion-android-locate:before + content: "\f2e9" + +.ion-android-lock:before + content: "\f392" + +.ion-android-mail:before + content: "\f2eb" + +.ion-android-map:before + content: "\f393" + +.ion-android-menu:before + content: "\f394" + +.ion-android-microphone:before + content: "\f2ec" + +.ion-android-microphone-off:before + content: "\f395" + +.ion-android-more-horizontal:before + content: "\f396" + +.ion-android-more-vertical:before + content: "\f397" + +.ion-android-navigate:before + content: "\f398" + +.ion-android-notifications:before + content: "\f39b" + +.ion-android-notifications-none:before + content: "\f399" + +.ion-android-notifications-off:before + content: "\f39a" + +.ion-android-open:before + content: "\f39c" + +.ion-android-options:before + content: "\f39d" + +.ion-android-people:before + content: "\f39e" + +.ion-android-person:before + content: "\f3a0" + +.ion-android-person-add:before + content: "\f39f" + +.ion-android-phone-landscape:before + content: "\f3a1" + +.ion-android-phone-portrait:before + content: "\f3a2" + +.ion-android-pin:before + content: "\f3a3" + +.ion-android-plane:before + content: "\f3a4" + +.ion-android-playstore:before + content: "\f2f0" + +.ion-android-print:before + content: "\f3a5" + +.ion-android-radio-button-off:before + content: "\f3a6" + +.ion-android-radio-button-on:before + content: "\f3a7" + +.ion-android-refresh:before + content: "\f3a8" + +.ion-android-remove:before + content: "\f2f4" + +.ion-android-remove-circle:before + content: "\f3a9" + +.ion-android-restaurant:before + content: "\f3aa" + +.ion-android-sad:before + content: "\f3ab" + +.ion-android-search:before + content: "\f2f5" + +.ion-android-send:before + content: "\f2f6" + +.ion-android-settings:before + content: "\f2f7" + +.ion-android-share:before + content: "\f2f8" + +.ion-android-share-alt:before + content: "\f3ac" + +.ion-android-star:before + content: "\f2fc" + +.ion-android-star-half:before + content: "\f3ad" + +.ion-android-star-outline:before + content: "\f3ae" + +.ion-android-stopwatch:before + content: "\f2fd" + +.ion-android-subway:before + content: "\f3af" + +.ion-android-sunny:before + content: "\f3b0" + +.ion-android-sync:before + content: "\f3b1" + +.ion-android-textsms:before + content: "\f3b2" + +.ion-android-time:before + content: "\f3b3" + +.ion-android-train:before + content: "\f3b4" + +.ion-android-unlock:before + content: "\f3b5" + +.ion-android-upload:before + content: "\f3b6" + +.ion-android-volume-down:before + content: "\f3b7" + +.ion-android-volume-mute:before + content: "\f3b8" + +.ion-android-volume-off:before + content: "\f3b9" + +.ion-android-volume-up:before + content: "\f3ba" + +.ion-android-walk:before + content: "\f3bb" + +.ion-android-warning:before + content: "\f3bc" + +.ion-android-watch:before + content: "\f3bd" + +.ion-android-wifi:before + content: "\f305" + +.ion-aperture:before + content: "\f313" + +.ion-archive:before + content: "\f102" + +.ion-arrow-down-a:before + content: "\f103" + +.ion-arrow-down-b:before + content: "\f104" + +.ion-arrow-down-c:before + content: "\f105" + +.ion-arrow-expand:before + content: "\f25e" + +.ion-arrow-graph-down-left:before + content: "\f25f" + +.ion-arrow-graph-down-right:before + content: "\f260" + +.ion-arrow-graph-up-left:before + content: "\f261" + +.ion-arrow-graph-up-right:before + content: "\f262" + +.ion-arrow-left-a:before + content: "\f106" + +.ion-arrow-left-b:before + content: "\f107" + +.ion-arrow-left-c:before + content: "\f108" + +.ion-arrow-move:before + content: "\f263" + +.ion-arrow-resize:before + content: "\f264" + +.ion-arrow-return-left:before + content: "\f265" + +.ion-arrow-return-right:before + content: "\f266" + +.ion-arrow-right-a:before + content: "\f109" + +.ion-arrow-right-b:before + content: "\f10a" + +.ion-arrow-right-c:before + content: "\f10b" + +.ion-arrow-shrink:before + content: "\f267" + +.ion-arrow-swap:before + content: "\f268" + +.ion-arrow-up-a:before + content: "\f10c" + +.ion-arrow-up-b:before + content: "\f10d" + +.ion-arrow-up-c:before + content: "\f10e" + +.ion-asterisk:before + content: "\f314" + +.ion-at:before + content: "\f10f" + +.ion-backspace:before + content: "\f3bf" + +.ion-backspace-outline:before + content: "\f3be" + +.ion-bag:before + content: "\f110" + +.ion-battery-charging:before + content: "\f111" + +.ion-battery-empty:before + content: "\f112" + +.ion-battery-full:before + content: "\f113" + +.ion-battery-half:before + content: "\f114" + +.ion-battery-low:before + content: "\f115" + +.ion-beaker:before + content: "\f269" + +.ion-beer:before + content: "\f26a" + +.ion-bluetooth:before + content: "\f116" + +.ion-bonfire:before + content: "\f315" + +.ion-bookmark:before + content: "\f26b" + +.ion-bowtie:before + content: "\f3c0" + +.ion-briefcase:before + content: "\f26c" + +.ion-bug:before + content: "\f2be" + +.ion-calculator:before + content: "\f26d" + +.ion-calendar:before + content: "\f117" + +.ion-camera:before + content: "\f118" + +.ion-card:before + content: "\f119" + +.ion-cash:before + content: "\f316" + +.ion-chatbox:before + content: "\f11b" + +.ion-chatbox-working:before + content: "\f11a" + +.ion-chatboxes:before + content: "\f11c" + +.ion-chatbubble:before + content: "\f11e" + +.ion-chatbubble-working:before + content: "\f11d" + +.ion-chatbubbles:before + content: "\f11f" + +.ion-checkmark:before + content: "\f122" + +.ion-checkmark-circled:before + content: "\f120" + +.ion-checkmark-round:before + content: "\f121" + +.ion-chevron-down:before + content: "\f123" + +.ion-chevron-left:before + content: "\f124" + +.ion-chevron-right:before + content: "\f125" + +.ion-chevron-up:before + content: "\f126" + +.ion-clipboard:before + content: "\f127" + +.ion-clock:before + content: "\f26e" + +.ion-close:before + content: "\f12a" + +.ion-close-circled:before + content: "\f128" + +.ion-close-round:before + content: "\f129" + +.ion-closed-captioning:before + content: "\f317" + +.ion-cloud:before + content: "\f12b" + +.ion-code:before + content: "\f271" + +.ion-code-download:before + content: "\f26f" + +.ion-code-working:before + content: "\f270" + +.ion-coffee:before + content: "\f272" + +.ion-compass:before + content: "\f273" + +.ion-compose:before + content: "\f12c" + +.ion-connection-bars:before + content: "\f274" + +.ion-contrast:before + content: "\f275" + +.ion-crop:before + content: "\f3c1" + +.ion-cube:before + content: "\f318" + +.ion-disc:before + content: "\f12d" + +.ion-document:before + content: "\f12f" + +.ion-document-text:before + content: "\f12e" + +.ion-drag:before + content: "\f130" + +.ion-earth:before + content: "\f276" + +.ion-easel:before + content: "\f3c2" + +.ion-edit:before + content: "\f2bf" + +.ion-egg:before + content: "\f277" + +.ion-eject:before + content: "\f131" + +.ion-email:before + content: "\f132" + +.ion-email-unread:before + content: "\f3c3" + +.ion-erlenmeyer-flask:before + content: "\f3c5" + +.ion-erlenmeyer-flask-bubbles:before + content: "\f3c4" + +.ion-eye:before + content: "\f133" + +.ion-eye-disabled:before + content: "\f306" + +.ion-female:before + content: "\f278" + +.ion-filing:before + content: "\f134" + +.ion-film-marker:before + content: "\f135" + +.ion-fireball:before + content: "\f319" + +.ion-flag:before + content: "\f279" + +.ion-flame:before + content: "\f31a" + +.ion-flash:before + content: "\f137" + +.ion-flash-off:before + content: "\f136" + +.ion-folder:before + content: "\f139" + +.ion-fork:before + content: "\f27a" + +.ion-fork-repo:before + content: "\f2c0" + +.ion-forward:before + content: "\f13a" + +.ion-funnel:before + content: "\f31b" + +.ion-gear-a:before + content: "\f13d" + +.ion-gear-b:before + content: "\f13e" + +.ion-grid:before + content: "\f13f" + +.ion-hammer:before + content: "\f27b" + +.ion-happy:before + content: "\f31c" + +.ion-happy-outline:before + content: "\f3c6" + +.ion-headphone:before + content: "\f140" + +.ion-heart:before + content: "\f141" + +.ion-heart-broken:before + content: "\f31d" + +.ion-help:before + content: "\f143" + +.ion-help-buoy:before + content: "\f27c" + +.ion-help-circled:before + content: "\f142" + +.ion-home:before + content: "\f144" + +.ion-icecream:before + content: "\f27d" + +.ion-image:before + content: "\f147" + +.ion-images:before + content: "\f148" + +.ion-information:before + content: "\f14a" + +.ion-information-circled:before + content: "\f149" + +.ion-ionic:before + content: "\f14b" + +.ion-ios-alarm:before + content: "\f3c8" + +.ion-ios-alarm-outline:before + content: "\f3c7" + +.ion-ios-albums:before + content: "\f3ca" + +.ion-ios-albums-outline:before + content: "\f3c9" + +.ion-ios-americanfootball:before + content: "\f3cc" + +.ion-ios-americanfootball-outline:before + content: "\f3cb" + +.ion-ios-analytics:before + content: "\f3ce" + +.ion-ios-analytics-outline:before + content: "\f3cd" + +.ion-ios-arrow-back:before + content: "\f3cf" + +.ion-ios-arrow-down:before + content: "\f3d0" + +.ion-ios-arrow-forward:before + content: "\f3d1" + +.ion-ios-arrow-left:before + content: "\f3d2" + +.ion-ios-arrow-right:before + content: "\f3d3" + +.ion-ios-arrow-thin-down:before + content: "\f3d4" + +.ion-ios-arrow-thin-left:before + content: "\f3d5" + +.ion-ios-arrow-thin-right:before + content: "\f3d6" + +.ion-ios-arrow-thin-up:before + content: "\f3d7" + +.ion-ios-arrow-up:before + content: "\f3d8" + +.ion-ios-at:before + content: "\f3da" + +.ion-ios-at-outline:before + content: "\f3d9" + +.ion-ios-barcode:before + content: "\f3dc" + +.ion-ios-barcode-outline:before + content: "\f3db" + +.ion-ios-baseball:before + content: "\f3de" + +.ion-ios-baseball-outline:before + content: "\f3dd" + +.ion-ios-basketball:before + content: "\f3e0" + +.ion-ios-basketball-outline:before + content: "\f3df" + +.ion-ios-bell:before + content: "\f3e2" + +.ion-ios-bell-outline:before + content: "\f3e1" + +.ion-ios-body:before + content: "\f3e4" + +.ion-ios-body-outline:before + content: "\f3e3" + +.ion-ios-bolt:before + content: "\f3e6" + +.ion-ios-bolt-outline:before + content: "\f3e5" + +.ion-ios-book:before + content: "\f3e8" + +.ion-ios-book-outline:before + content: "\f3e7" + +.ion-ios-bookmarks:before + content: "\f3ea" + +.ion-ios-bookmarks-outline:before + content: "\f3e9" + +.ion-ios-box:before + content: "\f3ec" + +.ion-ios-box-outline:before + content: "\f3eb" + +.ion-ios-briefcase:before + content: "\f3ee" + +.ion-ios-briefcase-outline:before + content: "\f3ed" + +.ion-ios-browsers:before + content: "\f3f0" + +.ion-ios-browsers-outline:before + content: "\f3ef" + +.ion-ios-calculator:before + content: "\f3f2" + +.ion-ios-calculator-outline:before + content: "\f3f1" + +.ion-ios-calendar:before + content: "\f3f4" + +.ion-ios-calendar-outline:before + content: "\f3f3" + +.ion-ios-camera:before + content: "\f3f6" + +.ion-ios-camera-outline:before + content: "\f3f5" + +.ion-ios-cart:before + content: "\f3f8" + +.ion-ios-cart-outline:before + content: "\f3f7" + +.ion-ios-chatboxes:before + content: "\f3fa" + +.ion-ios-chatboxes-outline:before + content: "\f3f9" + +.ion-ios-chatbubble:before + content: "\f3fc" + +.ion-ios-chatbubble-outline:before + content: "\f3fb" + +.ion-ios-checkmark:before + content: "\f3ff" + +.ion-ios-checkmark-empty:before + content: "\f3fd" + +.ion-ios-checkmark-outline:before + content: "\f3fe" + +.ion-ios-circle-filled:before + content: "\f400" + +.ion-ios-circle-outline:before + content: "\f401" + +.ion-ios-clock:before + content: "\f403" + +.ion-ios-clock-outline:before + content: "\f402" + +.ion-ios-close:before + content: "\f406" + +.ion-ios-close-empty:before + content: "\f404" + +.ion-ios-close-outline:before + content: "\f405" + +.ion-ios-cloud:before + content: "\f40c" + +.ion-ios-cloud-download:before + content: "\f408" + +.ion-ios-cloud-download-outline:before + content: "\f407" + +.ion-ios-cloud-outline:before + content: "\f409" + +.ion-ios-cloud-upload:before + content: "\f40b" + +.ion-ios-cloud-upload-outline:before + content: "\f40a" + +.ion-ios-cloudy:before + content: "\f410" + +.ion-ios-cloudy-night:before + content: "\f40e" + +.ion-ios-cloudy-night-outline:before + content: "\f40d" + +.ion-ios-cloudy-outline:before + content: "\f40f" + +.ion-ios-cog:before + content: "\f412" + +.ion-ios-cog-outline:before + content: "\f411" + +.ion-ios-color-filter:before + content: "\f414" + +.ion-ios-color-filter-outline:before + content: "\f413" + +.ion-ios-color-wand:before + content: "\f416" + +.ion-ios-color-wand-outline:before + content: "\f415" + +.ion-ios-compose:before + content: "\f418" + +.ion-ios-compose-outline:before + content: "\f417" + +.ion-ios-contact:before + content: "\f41a" + +.ion-ios-contact-outline:before + content: "\f419" + +.ion-ios-copy:before + content: "\f41c" + +.ion-ios-copy-outline:before + content: "\f41b" + +.ion-ios-crop:before + content: "\f41e" + +.ion-ios-crop-strong:before + content: "\f41d" + +.ion-ios-download:before + content: "\f420" + +.ion-ios-download-outline:before + content: "\f41f" + +.ion-ios-drag:before + content: "\f421" + +.ion-ios-email:before + content: "\f423" + +.ion-ios-email-outline:before + content: "\f422" + +.ion-ios-eye:before + content: "\f425" + +.ion-ios-eye-outline:before + content: "\f424" + +.ion-ios-fastforward:before + content: "\f427" + +.ion-ios-fastforward-outline:before + content: "\f426" + +.ion-ios-filing:before + content: "\f429" + +.ion-ios-filing-outline:before + content: "\f428" + +.ion-ios-film:before + content: "\f42b" + +.ion-ios-film-outline:before + content: "\f42a" + +.ion-ios-flag:before + content: "\f42d" + +.ion-ios-flag-outline:before + content: "\f42c" + +.ion-ios-flame:before + content: "\f42f" + +.ion-ios-flame-outline:before + content: "\f42e" + +.ion-ios-flask:before + content: "\f431" + +.ion-ios-flask-outline:before + content: "\f430" + +.ion-ios-flower:before + content: "\f433" + +.ion-ios-flower-outline:before + content: "\f432" + +.ion-ios-folder:before + content: "\f435" + +.ion-ios-folder-outline:before + content: "\f434" + +.ion-ios-football:before + content: "\f437" + +.ion-ios-football-outline:before + content: "\f436" + +.ion-ios-game-controller-a:before + content: "\f439" + +.ion-ios-game-controller-a-outline:before + content: "\f438" + +.ion-ios-game-controller-b:before + content: "\f43b" + +.ion-ios-game-controller-b-outline:before + content: "\f43a" + +.ion-ios-gear:before + content: "\f43d" + +.ion-ios-gear-outline:before + content: "\f43c" + +.ion-ios-glasses:before + content: "\f43f" + +.ion-ios-glasses-outline:before + content: "\f43e" + +.ion-ios-grid-view:before + content: "\f441" + +.ion-ios-grid-view-outline:before + content: "\f440" + +.ion-ios-heart:before + content: "\f443" + +.ion-ios-heart-outline:before + content: "\f442" + +.ion-ios-help:before + content: "\f446" + +.ion-ios-help-empty:before + content: "\f444" + +.ion-ios-help-outline:before + content: "\f445" + +.ion-ios-home:before + content: "\f448" + +.ion-ios-home-outline:before + content: "\f447" + +.ion-ios-infinite:before + content: "\f44a" + +.ion-ios-infinite-outline:before + content: "\f449" + +.ion-ios-information:before + content: "\f44d" + +.ion-ios-information-empty:before + content: "\f44b" + +.ion-ios-information-outline:before + content: "\f44c" + +.ion-ios-ionic-outline:before + content: "\f44e" + +.ion-ios-keypad:before + content: "\f450" + +.ion-ios-keypad-outline:before + content: "\f44f" + +.ion-ios-lightbulb:before + content: "\f452" + +.ion-ios-lightbulb-outline:before + content: "\f451" + +.ion-ios-list:before + content: "\f454" + +.ion-ios-list-outline:before + content: "\f453" + +.ion-ios-location:before + content: "\f456" + +.ion-ios-location-outline:before + content: "\f455" + +.ion-ios-locked:before + content: "\f458" + +.ion-ios-locked-outline:before + content: "\f457" + +.ion-ios-loop:before + content: "\f45a" + +.ion-ios-loop-strong:before + content: "\f459" + +.ion-ios-medical:before + content: "\f45c" + +.ion-ios-medical-outline:before + content: "\f45b" + +.ion-ios-medkit:before + content: "\f45e" + +.ion-ios-medkit-outline:before + content: "\f45d" + +.ion-ios-mic:before + content: "\f461" + +.ion-ios-mic-off:before + content: "\f45f" + +.ion-ios-mic-outline:before + content: "\f460" + +.ion-ios-minus:before + content: "\f464" + +.ion-ios-minus-empty:before + content: "\f462" + +.ion-ios-minus-outline:before + content: "\f463" + +.ion-ios-monitor:before + content: "\f466" + +.ion-ios-monitor-outline:before + content: "\f465" + +.ion-ios-moon:before + content: "\f468" + +.ion-ios-moon-outline:before + content: "\f467" + +.ion-ios-more:before + content: "\f46a" + +.ion-ios-more-outline:before + content: "\f469" + +.ion-ios-musical-note:before + content: "\f46b" + +.ion-ios-musical-notes:before + content: "\f46c" + +.ion-ios-navigate:before + content: "\f46e" + +.ion-ios-navigate-outline:before + content: "\f46d" + +.ion-ios-nutrition:before + content: "\f470" + +.ion-ios-nutrition-outline:before + content: "\f46f" + +.ion-ios-paper:before + content: "\f472" + +.ion-ios-paper-outline:before + content: "\f471" + +.ion-ios-paperplane:before + content: "\f474" + +.ion-ios-paperplane-outline:before + content: "\f473" + +.ion-ios-partlysunny:before + content: "\f476" + +.ion-ios-partlysunny-outline:before + content: "\f475" + +.ion-ios-pause:before + content: "\f478" + +.ion-ios-pause-outline:before + content: "\f477" + +.ion-ios-paw:before + content: "\f47a" + +.ion-ios-paw-outline:before + content: "\f479" + +.ion-ios-people:before + content: "\f47c" + +.ion-ios-people-outline:before + content: "\f47b" + +.ion-ios-person:before + content: "\f47e" + +.ion-ios-person-outline:before + content: "\f47d" + +.ion-ios-personadd:before + content: "\f480" + +.ion-ios-personadd-outline:before + content: "\f47f" + +.ion-ios-photos:before + content: "\f482" + +.ion-ios-photos-outline:before + content: "\f481" + +.ion-ios-pie:before + content: "\f484" + +.ion-ios-pie-outline:before + content: "\f483" + +.ion-ios-pint:before + content: "\f486" + +.ion-ios-pint-outline:before + content: "\f485" + +.ion-ios-play:before + content: "\f488" + +.ion-ios-play-outline:before + content: "\f487" + +.ion-ios-plus:before + content: "\f48b" + +.ion-ios-plus-empty:before + content: "\f489" + +.ion-ios-plus-outline:before + content: "\f48a" + +.ion-ios-pricetag:before + content: "\f48d" + +.ion-ios-pricetag-outline:before + content: "\f48c" + +.ion-ios-pricetags:before + content: "\f48f" + +.ion-ios-pricetags-outline:before + content: "\f48e" + +.ion-ios-printer:before + content: "\f491" + +.ion-ios-printer-outline:before + content: "\f490" + +.ion-ios-pulse:before + content: "\f493" + +.ion-ios-pulse-strong:before + content: "\f492" + +.ion-ios-rainy:before + content: "\f495" + +.ion-ios-rainy-outline:before + content: "\f494" + +.ion-ios-recording:before + content: "\f497" + +.ion-ios-recording-outline:before + content: "\f496" + +.ion-ios-redo:before + content: "\f499" + +.ion-ios-redo-outline:before + content: "\f498" + +.ion-ios-refresh:before + content: "\f49c" + +.ion-ios-refresh-empty:before + content: "\f49a" + +.ion-ios-refresh-outline:before + content: "\f49b" + +.ion-ios-reload:before + content: "\f49d" + +.ion-ios-reverse-camera:before + content: "\f49f" + +.ion-ios-reverse-camera-outline:before + content: "\f49e" + +.ion-ios-rewind:before + content: "\f4a1" + +.ion-ios-rewind-outline:before + content: "\f4a0" + +.ion-ios-rose:before + content: "\f4a3" + +.ion-ios-rose-outline:before + content: "\f4a2" + +.ion-ios-search:before + content: "\f4a5" + +.ion-ios-search-strong:before + content: "\f4a4" + +.ion-ios-settings:before + content: "\f4a7" + +.ion-ios-settings-strong:before + content: "\f4a6" + +.ion-ios-shuffle:before + content: "\f4a9" + +.ion-ios-shuffle-strong:before + content: "\f4a8" + +.ion-ios-skipbackward:before + content: "\f4ab" + +.ion-ios-skipbackward-outline:before + content: "\f4aa" + +.ion-ios-skipforward:before + content: "\f4ad" + +.ion-ios-skipforward-outline:before + content: "\f4ac" + +.ion-ios-snowy:before + content: "\f4ae" + +.ion-ios-speedometer:before + content: "\f4b0" + +.ion-ios-speedometer-outline:before + content: "\f4af" + +.ion-ios-star:before + content: "\f4b3" + +.ion-ios-star-half:before + content: "\f4b1" + +.ion-ios-star-outline:before + content: "\f4b2" + +.ion-ios-stopwatch:before + content: "\f4b5" + +.ion-ios-stopwatch-outline:before + content: "\f4b4" + +.ion-ios-sunny:before + content: "\f4b7" + +.ion-ios-sunny-outline:before + content: "\f4b6" + +.ion-ios-telephone:before + content: "\f4b9" + +.ion-ios-telephone-outline:before + content: "\f4b8" + +.ion-ios-tennisball:before + content: "\f4bb" + +.ion-ios-tennisball-outline:before + content: "\f4ba" + +.ion-ios-thunderstorm:before + content: "\f4bd" + +.ion-ios-thunderstorm-outline:before + content: "\f4bc" + +.ion-ios-time:before + content: "\f4bf" + +.ion-ios-time-outline:before + content: "\f4be" + +.ion-ios-timer:before + content: "\f4c1" + +.ion-ios-timer-outline:before + content: "\f4c0" + +.ion-ios-toggle:before + content: "\f4c3" + +.ion-ios-toggle-outline:before + content: "\f4c2" + +.ion-ios-trash:before + content: "\f4c5" + +.ion-ios-trash-outline:before + content: "\f4c4" + +.ion-ios-undo:before + content: "\f4c7" + +.ion-ios-undo-outline:before + content: "\f4c6" + +.ion-ios-unlocked:before + content: "\f4c9" + +.ion-ios-unlocked-outline:before + content: "\f4c8" + +.ion-ios-upload:before + content: "\f4cb" + +.ion-ios-upload-outline:before + content: "\f4ca" + +.ion-ios-videocam:before + content: "\f4cd" + +.ion-ios-videocam-outline:before + content: "\f4cc" + +.ion-ios-volume-high:before + content: "\f4ce" + +.ion-ios-volume-low:before + content: "\f4cf" + +.ion-ios-wineglass:before + content: "\f4d1" + +.ion-ios-wineglass-outline:before + content: "\f4d0" + +.ion-ios-world:before + content: "\f4d3" + +.ion-ios-world-outline:before + content: "\f4d2" + +.ion-ipad:before + content: "\f1f9" + +.ion-iphone:before + content: "\f1fa" + +.ion-ipod:before + content: "\f1fb" + +.ion-jet:before + content: "\f295" + +.ion-key:before + content: "\f296" + +.ion-knife:before + content: "\f297" + +.ion-laptop:before + content: "\f1fc" + +.ion-leaf:before + content: "\f1fd" + +.ion-levels:before + content: "\f298" + +.ion-lightbulb:before + content: "\f299" + +.ion-link:before + content: "\f1fe" + +.ion-load-a:before + content: "\f29a" + +.ion-load-b:before + content: "\f29b" + +.ion-load-c:before + content: "\f29c" + +.ion-load-d:before + content: "\f29d" + +.ion-location:before + content: "\f1ff" + +.ion-lock-combination:before + content: "\f4d4" + +.ion-locked:before + content: "\f200" + +.ion-log-in:before + content: "\f29e" + +.ion-log-out:before + content: "\f29f" + +.ion-loop:before + content: "\f201" + +.ion-magnet:before + content: "\f2a0" + +.ion-male:before + content: "\f2a1" + +.ion-man:before + content: "\f202" + +.ion-map:before + content: "\f203" + +.ion-medkit:before + content: "\f2a2" + +.ion-merge:before + content: "\f33f" + +.ion-mic-a:before + content: "\f204" + +.ion-mic-b:before + content: "\f205" + +.ion-mic-c:before + content: "\f206" + +.ion-minus:before + content: "\f209" + +.ion-minus-circled:before + content: "\f207" + +.ion-minus-round:before + content: "\f208" + +.ion-model-s:before + content: "\f2c1" + +.ion-monitor:before + content: "\f20a" + +.ion-more:before + content: "\f20b" + +.ion-mouse:before + content: "\f340" + +.ion-music-note:before + content: "\f20c" + +.ion-navicon:before + content: "\f20e" + +.ion-navicon-round:before + content: "\f20d" + +.ion-navigate:before + content: "\f2a3" + +.ion-network:before + content: "\f341" + +.ion-no-smoking:before + content: "\f2c2" + +.ion-nuclear:before + content: "\f2a4" + +.ion-outlet:before + content: "\f342" + +.ion-paintbrush:before + content: "\f4d5" + +.ion-paintbucket:before + content: "\f4d6" + +.ion-paper-airplane:before + content: "\f2c3" + +.ion-paperclip:before + content: "\f20f" + +.ion-pause:before + content: "\f210" + +.ion-person:before + content: "\f213" + +.ion-person-add:before + content: "\f211" + +.ion-person-stalker:before + content: "\f212" + +.ion-pie-graph:before + content: "\f2a5" + +.ion-pin:before + content: "\f2a6" + +.ion-pinpoint:before + content: "\f2a7" + +.ion-pizza:before + content: "\f2a8" + +.ion-plane:before + content: "\f214" + +.ion-planet:before + content: "\f343" + +.ion-play:before + content: "\f215" + +.ion-playstation:before + content: "\f30a" + +.ion-plus:before + content: "\f218" + +.ion-plus-circled:before + content: "\f216" + +.ion-plus-round:before + content: "\f217" + +.ion-podium:before + content: "\f344" + +.ion-pound:before + content: "\f219" + +.ion-power:before + content: "\f2a9" + +.ion-pricetag:before + content: "\f2aa" + +.ion-pricetags:before + content: "\f2ab" + +.ion-printer:before + content: "\f21a" + +.ion-pull-request:before + content: "\f345" + +.ion-qr-scanner:before + content: "\f346" + +.ion-quote:before + content: "\f347" + +.ion-radio-waves:before + content: "\f2ac" + +.ion-record:before + content: "\f21b" + +.ion-refresh:before + content: "\f21c" + +.ion-reply:before + content: "\f21e" + +.ion-reply-all:before + content: "\f21d" + +.ion-ribbon-a:before + content: "\f348" + +.ion-ribbon-b:before + content: "\f349" + +.ion-sad:before + content: "\f34a" + +.ion-sad-outline:before + content: "\f4d7" + +.ion-scissors:before + content: "\f34b" + +.ion-search:before + content: "\f21f" + +.ion-settings:before + content: "\f2ad" + +.ion-share:before + content: "\f220" + +.ion-shuffle:before + content: "\f221" + +.ion-skip-backward:before + content: "\f222" + +.ion-skip-forward:before + content: "\f223" + +.ion-social-android:before + content: "\f225" + +.ion-social-android-outline:before + content: "\f224" + +.ion-social-angular:before + content: "\f4d9" + +.ion-social-angular-outline:before + content: "\f4d8" + +.ion-social-apple:before + content: "\f227" + +.ion-social-apple-outline:before + content: "\f226" + +.ion-social-bitcoin:before + content: "\f2af" + +.ion-social-bitcoin-outline:before + content: "\f2ae" + +.ion-social-buffer:before + content: "\f229" + +.ion-social-buffer-outline:before + content: "\f228" + +.ion-social-chrome:before + content: "\f4db" + +.ion-social-chrome-outline:before + content: "\f4da" + +.ion-social-codepen:before + content: "\f4dd" + +.ion-social-codepen-outline:before + content: "\f4dc" + +.ion-social-css3:before + content: "\f4df" + +.ion-social-css3-outline:before + content: "\f4de" + +.ion-social-designernews:before + content: "\f22b" + +.ion-social-designernews-outline:before + content: "\f22a" + +.ion-social-dribbble:before + content: "\f22d" + +.ion-social-dribbble-outline:before + content: "\f22c" + +.ion-social-dropbox:before + content: "\f22f" + +.ion-social-dropbox-outline:before + content: "\f22e" + +.ion-social-euro:before + content: "\f4e1" + +.ion-social-euro-outline:before + content: "\f4e0" + +.ion-social-facebook:before + content: "\f231" + +.ion-social-facebook-outline:before + content: "\f230" + +.ion-social-foursquare:before + content: "\f34d" + +.ion-social-foursquare-outline:before + content: "\f34c" + +.ion-social-freebsd-devil:before + content: "\f2c4" + +.ion-social-github:before + content: "\f233" + +.ion-social-github-outline:before + content: "\f232" + +.ion-social-google:before + content: "\f34f" + +.ion-social-google-outline:before + content: "\f34e" + +.ion-social-googleplus:before + content: "\f235" + +.ion-social-googleplus-outline:before + content: "\f234" + +.ion-social-hackernews:before + content: "\f237" + +.ion-social-hackernews-outline:before + content: "\f236" + +.ion-social-html5:before + content: "\f4e3" + +.ion-social-html5-outline:before + content: "\f4e2" + +.ion-social-instagram:before + content: "\f351" + +.ion-social-instagram-outline:before + content: "\f350" + +.ion-social-javascript:before + content: "\f4e5" + +.ion-social-javascript-outline:before + content: "\f4e4" + +.ion-social-linkedin:before + content: "\f239" + +.ion-social-linkedin-outline:before + content: "\f238" + +.ion-social-markdown:before + content: "\f4e6" + +.ion-social-nodejs:before + content: "\f4e7" + +.ion-social-octocat:before + content: "\f4e8" + +.ion-social-pinterest:before + content: "\f2b1" + +.ion-social-pinterest-outline:before + content: "\f2b0" + +.ion-social-python:before + content: "\f4e9" + +.ion-social-reddit:before + content: "\f23b" + +.ion-social-reddit-outline:before + content: "\f23a" + +.ion-social-rss:before + content: "\f23d" + +.ion-social-rss-outline:before + content: "\f23c" + +.ion-social-sass:before + content: "\f4ea" + +.ion-social-skype:before + content: "\f23f" + +.ion-social-skype-outline:before + content: "\f23e" + +.ion-social-snapchat:before + content: "\f4ec" + +.ion-social-snapchat-outline:before + content: "\f4eb" + +.ion-social-tumblr:before + content: "\f241" + +.ion-social-tumblr-outline:before + content: "\f240" + +.ion-social-tux:before + content: "\f2c5" + +.ion-social-twitch:before + content: "\f4ee" + +.ion-social-twitch-outline:before + content: "\f4ed" + +.ion-social-twitter:before + content: "\f243" + +.ion-social-twitter-outline:before + content: "\f242" + +.ion-social-usd:before + content: "\f353" + +.ion-social-usd-outline:before + content: "\f352" + +.ion-social-vimeo:before + content: "\f245" + +.ion-social-vimeo-outline:before + content: "\f244" + +.ion-social-whatsapp:before + content: "\f4f0" + +.ion-social-whatsapp-outline:before + content: "\f4ef" + +.ion-social-windows:before + content: "\f247" + +.ion-social-windows-outline:before + content: "\f246" + +.ion-social-wordpress:before + content: "\f249" + +.ion-social-wordpress-outline:before + content: "\f248" + +.ion-social-yahoo:before + content: "\f24b" + +.ion-social-yahoo-outline:before + content: "\f24a" + +.ion-social-yen:before + content: "\f4f2" + +.ion-social-yen-outline:before + content: "\f4f1" + +.ion-social-youtube:before + content: "\f24d" + +.ion-social-youtube-outline:before + content: "\f24c" + +.ion-soup-can:before + content: "\f4f4" + +.ion-soup-can-outline:before + content: "\f4f3" + +.ion-speakerphone:before + content: "\f2b2" + +.ion-speedometer:before + content: "\f2b3" + +.ion-spoon:before + content: "\f2b4" + +.ion-star:before + content: "\f24e" + +.ion-stats-bars:before + content: "\f2b5" + +.ion-steam:before + content: "\f30b" + +.ion-stop:before + content: "\f24f" + +.ion-thermometer:before + content: "\f2b6" + +.ion-thumbsdown:before + content: "\f250" + +.ion-thumbsup:before + content: "\f251" + +.ion-toggle:before + content: "\f355" + +.ion-toggle-filled:before + content: "\f354" + +.ion-transgender:before + content: "\f4f5" + +.ion-trash-a:before + content: "\f252" + +.ion-trash-b:before + content: "\f253" + +.ion-trophy:before + content: "\f356" + +.ion-tshirt:before + content: "\f4f7" + +.ion-tshirt-outline:before + content: "\f4f6" + +.ion-umbrella:before + content: "\f2b7" + +.ion-university:before + content: "\f357" + +.ion-unlocked:before + content: "\f254" + +.ion-upload:before + content: "\f255" + +.ion-usb:before + content: "\f2b8" + +.ion-videocamera:before + content: "\f256" + +.ion-volume-high:before + content: "\f257" + +.ion-volume-low:before + content: "\f258" + +.ion-volume-medium:before + content: "\f259" + +.ion-volume-mute:before + content: "\f25a" + +.ion-wand:before + content: "\f358" + +.ion-waterdrop:before + content: "\f25b" + +.ion-wifi:before + content: "\f25c" + +.ion-wineglass:before + content: "\f2b9" + +.ion-woman:before + content: "\f25d" + +.ion-wrench:before + content: "\f2ba" + +.ion-xbox:before + content: "\f30c" + +.step__item.active + .step__icon + background-color: $color_12 + + ~ + &.step__divider + background-color: $color_12 + + &.step__item .step__icon + background-color: $color_12 \ No newline at end of file diff --git a/public/installer/css/scss/_variables.scss b/public/installer/css/scss/_variables.scss new file mode 100755 index 000000000..fca47d1ce --- /dev/null +++ b/public/installer/css/scss/_variables.scss @@ -0,0 +1,58 @@ +//colors +$color_0: #ff0; +$color_1: #000; +$color_2: silver; +$color_3: #666; +$color_4: #111; +$color_5: #1d73a2; +$color_6: #175c82; +$color_7: rgba(0, 0, 0, .19); +$color_8: rgba(0, 0, 0, .23); +$color_9: #357295; +$color_10: #fff; +$color_11: #cacfd2; +$color_12: #34a0db; +$color_13: #3d657b; +$color_14: rgba(0, 0, 0, .12); +$color_15: rgba(0, 0, 0, .24); +$color_16: #2490cb; +$color_17: #eee; +$color_18: #222; +$color_19: rgba(0, 0, 0, .16); +$color_20: #2ecc71; +$color_21: #e74c3c; +$color_22: #f5f5f5; +$color_23: rgba(0, 0, 0, .2); +$color_24: #ff0000; +$color_25: #000000; +$color_26: red; +$color_27: #dddddd; +$color_28: #ddd; +$color_29: #333; +$color_30: #ffff00; +$color_31: #008000; +$color_32: darkgray; +$color_33: #008080; +$color_34: #144242; +$color_35: #a9a9a9; +$color_36: rgba(0, 0, 0, 0.1); +$color_37: #ccc; +$color_38: rgba(0, 0, 0, 0.3); +$color_39: #ffffff; + +//fonts +$font_0: sans-serif; +$font_1: monospace; +$font_2: Roboto; +$font_3: Helvetica Neue; +$font_4: Helvetica; +$font_5: Arial; +$font_6: Courier New; +$font_7: Courier; +$font_8: Lucida Sans Typewriter; +$font_9: Lucida Typewriter; +$font_10: Ionicons; + +//urls +$url_0: 'https://fonts.googleapis.com/css?family=Roboto:400,300,500,700,900'; +$url_1: '../img/background.png'; \ No newline at end of file diff --git a/public/installer/css/scss/font-awesome/_animated.scss b/public/installer/css/scss/font-awesome/_animated.scss new file mode 100755 index 000000000..8a020dbff --- /dev/null +++ b/public/installer/css/scss/font-awesome/_animated.scss @@ -0,0 +1,34 @@ +// Spinning Icons +// -------------------------- + +.#{$fa-css-prefix}-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; +} + +.#{$fa-css-prefix}-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); +} + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} + +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} diff --git a/public/installer/css/scss/font-awesome/_bordered-pulled.scss b/public/installer/css/scss/font-awesome/_bordered-pulled.scss new file mode 100755 index 000000000..d4b85a02f --- /dev/null +++ b/public/installer/css/scss/font-awesome/_bordered-pulled.scss @@ -0,0 +1,25 @@ +// Bordered & Pulled +// ------------------------- + +.#{$fa-css-prefix}-border { + padding: .2em .25em .15em; + border: solid .08em $fa-border-color; + border-radius: .1em; +} + +.#{$fa-css-prefix}-pull-left { float: left; } +.#{$fa-css-prefix}-pull-right { float: right; } + +.#{$fa-css-prefix} { + &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } + &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } +} + +/* Deprecated as of 4.4.0 */ +.pull-right { float: right; } +.pull-left { float: left; } + +.#{$fa-css-prefix} { + &.pull-left { margin-right: .3em; } + &.pull-right { margin-left: .3em; } +} diff --git a/public/installer/css/scss/font-awesome/_core.scss b/public/installer/css/scss/font-awesome/_core.scss new file mode 100755 index 000000000..7425ef85f --- /dev/null +++ b/public/installer/css/scss/font-awesome/_core.scss @@ -0,0 +1,12 @@ +// Base Class Definition +// ------------------------- + +.#{$fa-css-prefix} { + display: inline-block; + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font-size: inherit; // can't have font-size inherit on line above, so need to override + text-rendering: auto; // optimizelegibility throws things off #1094 + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + +} diff --git a/public/installer/css/scss/font-awesome/_fixed-width.scss b/public/installer/css/scss/font-awesome/_fixed-width.scss new file mode 100755 index 000000000..b221c9813 --- /dev/null +++ b/public/installer/css/scss/font-awesome/_fixed-width.scss @@ -0,0 +1,6 @@ +// Fixed Width Icons +// ------------------------- +.#{$fa-css-prefix}-fw { + width: (18em / 14); + text-align: center; +} diff --git a/public/installer/css/scss/font-awesome/_icons.scss b/public/installer/css/scss/font-awesome/_icons.scss new file mode 100755 index 000000000..e63e702c4 --- /dev/null +++ b/public/installer/css/scss/font-awesome/_icons.scss @@ -0,0 +1,789 @@ +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ + +.#{$fa-css-prefix}-glass:before { content: $fa-var-glass; } +.#{$fa-css-prefix}-music:before { content: $fa-var-music; } +.#{$fa-css-prefix}-search:before { content: $fa-var-search; } +.#{$fa-css-prefix}-envelope-o:before { content: $fa-var-envelope-o; } +.#{$fa-css-prefix}-heart:before { content: $fa-var-heart; } +.#{$fa-css-prefix}-star:before { content: $fa-var-star; } +.#{$fa-css-prefix}-star-o:before { content: $fa-var-star-o; } +.#{$fa-css-prefix}-user:before { content: $fa-var-user; } +.#{$fa-css-prefix}-film:before { content: $fa-var-film; } +.#{$fa-css-prefix}-th-large:before { content: $fa-var-th-large; } +.#{$fa-css-prefix}-th:before { content: $fa-var-th; } +.#{$fa-css-prefix}-th-list:before { content: $fa-var-th-list; } +.#{$fa-css-prefix}-check:before { content: $fa-var-check; } +.#{$fa-css-prefix}-remove:before, +.#{$fa-css-prefix}-close:before, +.#{$fa-css-prefix}-times:before { content: $fa-var-times; } +.#{$fa-css-prefix}-search-plus:before { content: $fa-var-search-plus; } +.#{$fa-css-prefix}-search-minus:before { content: $fa-var-search-minus; } +.#{$fa-css-prefix}-power-off:before { content: $fa-var-power-off; } +.#{$fa-css-prefix}-signal:before { content: $fa-var-signal; } +.#{$fa-css-prefix}-gear:before, +.#{$fa-css-prefix}-cog:before { content: $fa-var-cog; } +.#{$fa-css-prefix}-trash-o:before { content: $fa-var-trash-o; } +.#{$fa-css-prefix}-home:before { content: $fa-var-home; } +.#{$fa-css-prefix}-file-o:before { content: $fa-var-file-o; } +.#{$fa-css-prefix}-clock-o:before { content: $fa-var-clock-o; } +.#{$fa-css-prefix}-road:before { content: $fa-var-road; } +.#{$fa-css-prefix}-download:before { content: $fa-var-download; } +.#{$fa-css-prefix}-arrow-circle-o-down:before { content: $fa-var-arrow-circle-o-down; } +.#{$fa-css-prefix}-arrow-circle-o-up:before { content: $fa-var-arrow-circle-o-up; } +.#{$fa-css-prefix}-inbox:before { content: $fa-var-inbox; } +.#{$fa-css-prefix}-play-circle-o:before { content: $fa-var-play-circle-o; } +.#{$fa-css-prefix}-rotate-right:before, +.#{$fa-css-prefix}-repeat:before { content: $fa-var-repeat; } +.#{$fa-css-prefix}-refresh:before { content: $fa-var-refresh; } +.#{$fa-css-prefix}-list-alt:before { content: $fa-var-list-alt; } +.#{$fa-css-prefix}-lock:before { content: $fa-var-lock; } +.#{$fa-css-prefix}-flag:before { content: $fa-var-flag; } +.#{$fa-css-prefix}-headphones:before { content: $fa-var-headphones; } +.#{$fa-css-prefix}-volume-off:before { content: $fa-var-volume-off; } +.#{$fa-css-prefix}-volume-down:before { content: $fa-var-volume-down; } +.#{$fa-css-prefix}-volume-up:before { content: $fa-var-volume-up; } +.#{$fa-css-prefix}-qrcode:before { content: $fa-var-qrcode; } +.#{$fa-css-prefix}-barcode:before { content: $fa-var-barcode; } +.#{$fa-css-prefix}-tag:before { content: $fa-var-tag; } +.#{$fa-css-prefix}-tags:before { content: $fa-var-tags; } +.#{$fa-css-prefix}-book:before { content: $fa-var-book; } +.#{$fa-css-prefix}-bookmark:before { content: $fa-var-bookmark; } +.#{$fa-css-prefix}-print:before { content: $fa-var-print; } +.#{$fa-css-prefix}-camera:before { content: $fa-var-camera; } +.#{$fa-css-prefix}-font:before { content: $fa-var-font; } +.#{$fa-css-prefix}-bold:before { content: $fa-var-bold; } +.#{$fa-css-prefix}-italic:before { content: $fa-var-italic; } +.#{$fa-css-prefix}-text-height:before { content: $fa-var-text-height; } +.#{$fa-css-prefix}-text-width:before { content: $fa-var-text-width; } +.#{$fa-css-prefix}-align-left:before { content: $fa-var-align-left; } +.#{$fa-css-prefix}-align-center:before { content: $fa-var-align-center; } +.#{$fa-css-prefix}-align-right:before { content: $fa-var-align-right; } +.#{$fa-css-prefix}-align-justify:before { content: $fa-var-align-justify; } +.#{$fa-css-prefix}-list:before { content: $fa-var-list; } +.#{$fa-css-prefix}-dedent:before, +.#{$fa-css-prefix}-outdent:before { content: $fa-var-outdent; } +.#{$fa-css-prefix}-indent:before { content: $fa-var-indent; } +.#{$fa-css-prefix}-video-camera:before { content: $fa-var-video-camera; } +.#{$fa-css-prefix}-photo:before, +.#{$fa-css-prefix}-image:before, +.#{$fa-css-prefix}-picture-o:before { content: $fa-var-picture-o; } +.#{$fa-css-prefix}-pencil:before { content: $fa-var-pencil; } +.#{$fa-css-prefix}-map-marker:before { content: $fa-var-map-marker; } +.#{$fa-css-prefix}-adjust:before { content: $fa-var-adjust; } +.#{$fa-css-prefix}-tint:before { content: $fa-var-tint; } +.#{$fa-css-prefix}-edit:before, +.#{$fa-css-prefix}-pencil-square-o:before { content: $fa-var-pencil-square-o; } +.#{$fa-css-prefix}-share-square-o:before { content: $fa-var-share-square-o; } +.#{$fa-css-prefix}-check-square-o:before { content: $fa-var-check-square-o; } +.#{$fa-css-prefix}-arrows:before { content: $fa-var-arrows; } +.#{$fa-css-prefix}-step-backward:before { content: $fa-var-step-backward; } +.#{$fa-css-prefix}-fast-backward:before { content: $fa-var-fast-backward; } +.#{$fa-css-prefix}-backward:before { content: $fa-var-backward; } +.#{$fa-css-prefix}-play:before { content: $fa-var-play; } +.#{$fa-css-prefix}-pause:before { content: $fa-var-pause; } +.#{$fa-css-prefix}-stop:before { content: $fa-var-stop; } +.#{$fa-css-prefix}-forward:before { content: $fa-var-forward; } +.#{$fa-css-prefix}-fast-forward:before { content: $fa-var-fast-forward; } +.#{$fa-css-prefix}-step-forward:before { content: $fa-var-step-forward; } +.#{$fa-css-prefix}-eject:before { content: $fa-var-eject; } +.#{$fa-css-prefix}-chevron-left:before { content: $fa-var-chevron-left; } +.#{$fa-css-prefix}-chevron-right:before { content: $fa-var-chevron-right; } +.#{$fa-css-prefix}-plus-circle:before { content: $fa-var-plus-circle; } +.#{$fa-css-prefix}-minus-circle:before { content: $fa-var-minus-circle; } +.#{$fa-css-prefix}-times-circle:before { content: $fa-var-times-circle; } +.#{$fa-css-prefix}-check-circle:before { content: $fa-var-check-circle; } +.#{$fa-css-prefix}-question-circle:before { content: $fa-var-question-circle; } +.#{$fa-css-prefix}-info-circle:before { content: $fa-var-info-circle; } +.#{$fa-css-prefix}-crosshairs:before { content: $fa-var-crosshairs; } +.#{$fa-css-prefix}-times-circle-o:before { content: $fa-var-times-circle-o; } +.#{$fa-css-prefix}-check-circle-o:before { content: $fa-var-check-circle-o; } +.#{$fa-css-prefix}-ban:before { content: $fa-var-ban; } +.#{$fa-css-prefix}-arrow-left:before { content: $fa-var-arrow-left; } +.#{$fa-css-prefix}-arrow-right:before { content: $fa-var-arrow-right; } +.#{$fa-css-prefix}-arrow-up:before { content: $fa-var-arrow-up; } +.#{$fa-css-prefix}-arrow-down:before { content: $fa-var-arrow-down; } +.#{$fa-css-prefix}-mail-forward:before, +.#{$fa-css-prefix}-share:before { content: $fa-var-share; } +.#{$fa-css-prefix}-expand:before { content: $fa-var-expand; } +.#{$fa-css-prefix}-compress:before { content: $fa-var-compress; } +.#{$fa-css-prefix}-plus:before { content: $fa-var-plus; } +.#{$fa-css-prefix}-minus:before { content: $fa-var-minus; } +.#{$fa-css-prefix}-asterisk:before { content: $fa-var-asterisk; } +.#{$fa-css-prefix}-exclamation-circle:before { content: $fa-var-exclamation-circle; } +.#{$fa-css-prefix}-gift:before { content: $fa-var-gift; } +.#{$fa-css-prefix}-leaf:before { content: $fa-var-leaf; } +.#{$fa-css-prefix}-fire:before { content: $fa-var-fire; } +.#{$fa-css-prefix}-eye:before { content: $fa-var-eye; } +.#{$fa-css-prefix}-eye-slash:before { content: $fa-var-eye-slash; } +.#{$fa-css-prefix}-warning:before, +.#{$fa-css-prefix}-exclamation-triangle:before { content: $fa-var-exclamation-triangle; } +.#{$fa-css-prefix}-plane:before { content: $fa-var-plane; } +.#{$fa-css-prefix}-calendar:before { content: $fa-var-calendar; } +.#{$fa-css-prefix}-random:before { content: $fa-var-random; } +.#{$fa-css-prefix}-comment:before { content: $fa-var-comment; } +.#{$fa-css-prefix}-magnet:before { content: $fa-var-magnet; } +.#{$fa-css-prefix}-chevron-up:before { content: $fa-var-chevron-up; } +.#{$fa-css-prefix}-chevron-down:before { content: $fa-var-chevron-down; } +.#{$fa-css-prefix}-retweet:before { content: $fa-var-retweet; } +.#{$fa-css-prefix}-shopping-cart:before { content: $fa-var-shopping-cart; } +.#{$fa-css-prefix}-folder:before { content: $fa-var-folder; } +.#{$fa-css-prefix}-folder-open:before { content: $fa-var-folder-open; } +.#{$fa-css-prefix}-arrows-v:before { content: $fa-var-arrows-v; } +.#{$fa-css-prefix}-arrows-h:before { content: $fa-var-arrows-h; } +.#{$fa-css-prefix}-bar-chart-o:before, +.#{$fa-css-prefix}-bar-chart:before { content: $fa-var-bar-chart; } +.#{$fa-css-prefix}-twitter-square:before { content: $fa-var-twitter-square; } +.#{$fa-css-prefix}-facebook-square:before { content: $fa-var-facebook-square; } +.#{$fa-css-prefix}-camera-retro:before { content: $fa-var-camera-retro; } +.#{$fa-css-prefix}-key:before { content: $fa-var-key; } +.#{$fa-css-prefix}-gears:before, +.#{$fa-css-prefix}-cogs:before { content: $fa-var-cogs; } +.#{$fa-css-prefix}-comments:before { content: $fa-var-comments; } +.#{$fa-css-prefix}-thumbs-o-up:before { content: $fa-var-thumbs-o-up; } +.#{$fa-css-prefix}-thumbs-o-down:before { content: $fa-var-thumbs-o-down; } +.#{$fa-css-prefix}-star-half:before { content: $fa-var-star-half; } +.#{$fa-css-prefix}-heart-o:before { content: $fa-var-heart-o; } +.#{$fa-css-prefix}-sign-out:before { content: $fa-var-sign-out; } +.#{$fa-css-prefix}-linkedin-square:before { content: $fa-var-linkedin-square; } +.#{$fa-css-prefix}-thumb-tack:before { content: $fa-var-thumb-tack; } +.#{$fa-css-prefix}-external-link:before { content: $fa-var-external-link; } +.#{$fa-css-prefix}-sign-in:before { content: $fa-var-sign-in; } +.#{$fa-css-prefix}-trophy:before { content: $fa-var-trophy; } +.#{$fa-css-prefix}-github-square:before { content: $fa-var-github-square; } +.#{$fa-css-prefix}-upload:before { content: $fa-var-upload; } +.#{$fa-css-prefix}-lemon-o:before { content: $fa-var-lemon-o; } +.#{$fa-css-prefix}-phone:before { content: $fa-var-phone; } +.#{$fa-css-prefix}-square-o:before { content: $fa-var-square-o; } +.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; } +.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; } +.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; } +.#{$fa-css-prefix}-facebook-f:before, +.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; } +.#{$fa-css-prefix}-github:before { content: $fa-var-github; } +.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; } +.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; } +.#{$fa-css-prefix}-feed:before, +.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; } +.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; } +.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; } +.#{$fa-css-prefix}-bell:before { content: $fa-var-bell; } +.#{$fa-css-prefix}-certificate:before { content: $fa-var-certificate; } +.#{$fa-css-prefix}-hand-o-right:before { content: $fa-var-hand-o-right; } +.#{$fa-css-prefix}-hand-o-left:before { content: $fa-var-hand-o-left; } +.#{$fa-css-prefix}-hand-o-up:before { content: $fa-var-hand-o-up; } +.#{$fa-css-prefix}-hand-o-down:before { content: $fa-var-hand-o-down; } +.#{$fa-css-prefix}-arrow-circle-left:before { content: $fa-var-arrow-circle-left; } +.#{$fa-css-prefix}-arrow-circle-right:before { content: $fa-var-arrow-circle-right; } +.#{$fa-css-prefix}-arrow-circle-up:before { content: $fa-var-arrow-circle-up; } +.#{$fa-css-prefix}-arrow-circle-down:before { content: $fa-var-arrow-circle-down; } +.#{$fa-css-prefix}-globe:before { content: $fa-var-globe; } +.#{$fa-css-prefix}-wrench:before { content: $fa-var-wrench; } +.#{$fa-css-prefix}-tasks:before { content: $fa-var-tasks; } +.#{$fa-css-prefix}-filter:before { content: $fa-var-filter; } +.#{$fa-css-prefix}-briefcase:before { content: $fa-var-briefcase; } +.#{$fa-css-prefix}-arrows-alt:before { content: $fa-var-arrows-alt; } +.#{$fa-css-prefix}-group:before, +.#{$fa-css-prefix}-users:before { content: $fa-var-users; } +.#{$fa-css-prefix}-chain:before, +.#{$fa-css-prefix}-link:before { content: $fa-var-link; } +.#{$fa-css-prefix}-cloud:before { content: $fa-var-cloud; } +.#{$fa-css-prefix}-flask:before { content: $fa-var-flask; } +.#{$fa-css-prefix}-cut:before, +.#{$fa-css-prefix}-scissors:before { content: $fa-var-scissors; } +.#{$fa-css-prefix}-copy:before, +.#{$fa-css-prefix}-files-o:before { content: $fa-var-files-o; } +.#{$fa-css-prefix}-paperclip:before { content: $fa-var-paperclip; } +.#{$fa-css-prefix}-save:before, +.#{$fa-css-prefix}-floppy-o:before { content: $fa-var-floppy-o; } +.#{$fa-css-prefix}-square:before { content: $fa-var-square; } +.#{$fa-css-prefix}-navicon:before, +.#{$fa-css-prefix}-reorder:before, +.#{$fa-css-prefix}-bars:before { content: $fa-var-bars; } +.#{$fa-css-prefix}-list-ul:before { content: $fa-var-list-ul; } +.#{$fa-css-prefix}-list-ol:before { content: $fa-var-list-ol; } +.#{$fa-css-prefix}-strikethrough:before { content: $fa-var-strikethrough; } +.#{$fa-css-prefix}-underline:before { content: $fa-var-underline; } +.#{$fa-css-prefix}-table:before { content: $fa-var-table; } +.#{$fa-css-prefix}-magic:before { content: $fa-var-magic; } +.#{$fa-css-prefix}-truck:before { content: $fa-var-truck; } +.#{$fa-css-prefix}-pinterest:before { content: $fa-var-pinterest; } +.#{$fa-css-prefix}-pinterest-square:before { content: $fa-var-pinterest-square; } +.#{$fa-css-prefix}-google-plus-square:before { content: $fa-var-google-plus-square; } +.#{$fa-css-prefix}-google-plus:before { content: $fa-var-google-plus; } +.#{$fa-css-prefix}-money:before { content: $fa-var-money; } +.#{$fa-css-prefix}-caret-down:before { content: $fa-var-caret-down; } +.#{$fa-css-prefix}-caret-up:before { content: $fa-var-caret-up; } +.#{$fa-css-prefix}-caret-left:before { content: $fa-var-caret-left; } +.#{$fa-css-prefix}-caret-right:before { content: $fa-var-caret-right; } +.#{$fa-css-prefix}-columns:before { content: $fa-var-columns; } +.#{$fa-css-prefix}-unsorted:before, +.#{$fa-css-prefix}-sort:before { content: $fa-var-sort; } +.#{$fa-css-prefix}-sort-down:before, +.#{$fa-css-prefix}-sort-desc:before { content: $fa-var-sort-desc; } +.#{$fa-css-prefix}-sort-up:before, +.#{$fa-css-prefix}-sort-asc:before { content: $fa-var-sort-asc; } +.#{$fa-css-prefix}-envelope:before { content: $fa-var-envelope; } +.#{$fa-css-prefix}-linkedin:before { content: $fa-var-linkedin; } +.#{$fa-css-prefix}-rotate-left:before, +.#{$fa-css-prefix}-undo:before { content: $fa-var-undo; } +.#{$fa-css-prefix}-legal:before, +.#{$fa-css-prefix}-gavel:before { content: $fa-var-gavel; } +.#{$fa-css-prefix}-dashboard:before, +.#{$fa-css-prefix}-tachometer:before { content: $fa-var-tachometer; } +.#{$fa-css-prefix}-comment-o:before { content: $fa-var-comment-o; } +.#{$fa-css-prefix}-comments-o:before { content: $fa-var-comments-o; } +.#{$fa-css-prefix}-flash:before, +.#{$fa-css-prefix}-bolt:before { content: $fa-var-bolt; } +.#{$fa-css-prefix}-sitemap:before { content: $fa-var-sitemap; } +.#{$fa-css-prefix}-umbrella:before { content: $fa-var-umbrella; } +.#{$fa-css-prefix}-paste:before, +.#{$fa-css-prefix}-clipboard:before { content: $fa-var-clipboard; } +.#{$fa-css-prefix}-lightbulb-o:before { content: $fa-var-lightbulb-o; } +.#{$fa-css-prefix}-exchange:before { content: $fa-var-exchange; } +.#{$fa-css-prefix}-cloud-download:before { content: $fa-var-cloud-download; } +.#{$fa-css-prefix}-cloud-upload:before { content: $fa-var-cloud-upload; } +.#{$fa-css-prefix}-user-md:before { content: $fa-var-user-md; } +.#{$fa-css-prefix}-stethoscope:before { content: $fa-var-stethoscope; } +.#{$fa-css-prefix}-suitcase:before { content: $fa-var-suitcase; } +.#{$fa-css-prefix}-bell-o:before { content: $fa-var-bell-o; } +.#{$fa-css-prefix}-coffee:before { content: $fa-var-coffee; } +.#{$fa-css-prefix}-cutlery:before { content: $fa-var-cutlery; } +.#{$fa-css-prefix}-file-text-o:before { content: $fa-var-file-text-o; } +.#{$fa-css-prefix}-building-o:before { content: $fa-var-building-o; } +.#{$fa-css-prefix}-hospital-o:before { content: $fa-var-hospital-o; } +.#{$fa-css-prefix}-ambulance:before { content: $fa-var-ambulance; } +.#{$fa-css-prefix}-medkit:before { content: $fa-var-medkit; } +.#{$fa-css-prefix}-fighter-jet:before { content: $fa-var-fighter-jet; } +.#{$fa-css-prefix}-beer:before { content: $fa-var-beer; } +.#{$fa-css-prefix}-h-square:before { content: $fa-var-h-square; } +.#{$fa-css-prefix}-plus-square:before { content: $fa-var-plus-square; } +.#{$fa-css-prefix}-angle-double-left:before { content: $fa-var-angle-double-left; } +.#{$fa-css-prefix}-angle-double-right:before { content: $fa-var-angle-double-right; } +.#{$fa-css-prefix}-angle-double-up:before { content: $fa-var-angle-double-up; } +.#{$fa-css-prefix}-angle-double-down:before { content: $fa-var-angle-double-down; } +.#{$fa-css-prefix}-angle-left:before { content: $fa-var-angle-left; } +.#{$fa-css-prefix}-angle-right:before { content: $fa-var-angle-right; } +.#{$fa-css-prefix}-angle-up:before { content: $fa-var-angle-up; } +.#{$fa-css-prefix}-angle-down:before { content: $fa-var-angle-down; } +.#{$fa-css-prefix}-desktop:before { content: $fa-var-desktop; } +.#{$fa-css-prefix}-laptop:before { content: $fa-var-laptop; } +.#{$fa-css-prefix}-tablet:before { content: $fa-var-tablet; } +.#{$fa-css-prefix}-mobile-phone:before, +.#{$fa-css-prefix}-mobile:before { content: $fa-var-mobile; } +.#{$fa-css-prefix}-circle-o:before { content: $fa-var-circle-o; } +.#{$fa-css-prefix}-quote-left:before { content: $fa-var-quote-left; } +.#{$fa-css-prefix}-quote-right:before { content: $fa-var-quote-right; } +.#{$fa-css-prefix}-spinner:before { content: $fa-var-spinner; } +.#{$fa-css-prefix}-circle:before { content: $fa-var-circle; } +.#{$fa-css-prefix}-mail-reply:before, +.#{$fa-css-prefix}-reply:before { content: $fa-var-reply; } +.#{$fa-css-prefix}-github-alt:before { content: $fa-var-github-alt; } +.#{$fa-css-prefix}-folder-o:before { content: $fa-var-folder-o; } +.#{$fa-css-prefix}-folder-open-o:before { content: $fa-var-folder-open-o; } +.#{$fa-css-prefix}-smile-o:before { content: $fa-var-smile-o; } +.#{$fa-css-prefix}-frown-o:before { content: $fa-var-frown-o; } +.#{$fa-css-prefix}-meh-o:before { content: $fa-var-meh-o; } +.#{$fa-css-prefix}-gamepad:before { content: $fa-var-gamepad; } +.#{$fa-css-prefix}-keyboard-o:before { content: $fa-var-keyboard-o; } +.#{$fa-css-prefix}-flag-o:before { content: $fa-var-flag-o; } +.#{$fa-css-prefix}-flag-checkered:before { content: $fa-var-flag-checkered; } +.#{$fa-css-prefix}-terminal:before { content: $fa-var-terminal; } +.#{$fa-css-prefix}-code:before { content: $fa-var-code; } +.#{$fa-css-prefix}-mail-reply-all:before, +.#{$fa-css-prefix}-reply-all:before { content: $fa-var-reply-all; } +.#{$fa-css-prefix}-star-half-empty:before, +.#{$fa-css-prefix}-star-half-full:before, +.#{$fa-css-prefix}-star-half-o:before { content: $fa-var-star-half-o; } +.#{$fa-css-prefix}-location-arrow:before { content: $fa-var-location-arrow; } +.#{$fa-css-prefix}-crop:before { content: $fa-var-crop; } +.#{$fa-css-prefix}-code-fork:before { content: $fa-var-code-fork; } +.#{$fa-css-prefix}-unlink:before, +.#{$fa-css-prefix}-chain-broken:before { content: $fa-var-chain-broken; } +.#{$fa-css-prefix}-question:before { content: $fa-var-question; } +.#{$fa-css-prefix}-info:before { content: $fa-var-info; } +.#{$fa-css-prefix}-exclamation:before { content: $fa-var-exclamation; } +.#{$fa-css-prefix}-superscript:before { content: $fa-var-superscript; } +.#{$fa-css-prefix}-subscript:before { content: $fa-var-subscript; } +.#{$fa-css-prefix}-eraser:before { content: $fa-var-eraser; } +.#{$fa-css-prefix}-puzzle-piece:before { content: $fa-var-puzzle-piece; } +.#{$fa-css-prefix}-microphone:before { content: $fa-var-microphone; } +.#{$fa-css-prefix}-microphone-slash:before { content: $fa-var-microphone-slash; } +.#{$fa-css-prefix}-shield:before { content: $fa-var-shield; } +.#{$fa-css-prefix}-calendar-o:before { content: $fa-var-calendar-o; } +.#{$fa-css-prefix}-fire-extinguisher:before { content: $fa-var-fire-extinguisher; } +.#{$fa-css-prefix}-rocket:before { content: $fa-var-rocket; } +.#{$fa-css-prefix}-maxcdn:before { content: $fa-var-maxcdn; } +.#{$fa-css-prefix}-chevron-circle-left:before { content: $fa-var-chevron-circle-left; } +.#{$fa-css-prefix}-chevron-circle-right:before { content: $fa-var-chevron-circle-right; } +.#{$fa-css-prefix}-chevron-circle-up:before { content: $fa-var-chevron-circle-up; } +.#{$fa-css-prefix}-chevron-circle-down:before { content: $fa-var-chevron-circle-down; } +.#{$fa-css-prefix}-html5:before { content: $fa-var-html5; } +.#{$fa-css-prefix}-css3:before { content: $fa-var-css3; } +.#{$fa-css-prefix}-anchor:before { content: $fa-var-anchor; } +.#{$fa-css-prefix}-unlock-alt:before { content: $fa-var-unlock-alt; } +.#{$fa-css-prefix}-bullseye:before { content: $fa-var-bullseye; } +.#{$fa-css-prefix}-ellipsis-h:before { content: $fa-var-ellipsis-h; } +.#{$fa-css-prefix}-ellipsis-v:before { content: $fa-var-ellipsis-v; } +.#{$fa-css-prefix}-rss-square:before { content: $fa-var-rss-square; } +.#{$fa-css-prefix}-play-circle:before { content: $fa-var-play-circle; } +.#{$fa-css-prefix}-ticket:before { content: $fa-var-ticket; } +.#{$fa-css-prefix}-minus-square:before { content: $fa-var-minus-square; } +.#{$fa-css-prefix}-minus-square-o:before { content: $fa-var-minus-square-o; } +.#{$fa-css-prefix}-level-up:before { content: $fa-var-level-up; } +.#{$fa-css-prefix}-level-down:before { content: $fa-var-level-down; } +.#{$fa-css-prefix}-check-square:before { content: $fa-var-check-square; } +.#{$fa-css-prefix}-pencil-square:before { content: $fa-var-pencil-square; } +.#{$fa-css-prefix}-external-link-square:before { content: $fa-var-external-link-square; } +.#{$fa-css-prefix}-share-square:before { content: $fa-var-share-square; } +.#{$fa-css-prefix}-compass:before { content: $fa-var-compass; } +.#{$fa-css-prefix}-toggle-down:before, +.#{$fa-css-prefix}-caret-square-o-down:before { content: $fa-var-caret-square-o-down; } +.#{$fa-css-prefix}-toggle-up:before, +.#{$fa-css-prefix}-caret-square-o-up:before { content: $fa-var-caret-square-o-up; } +.#{$fa-css-prefix}-toggle-right:before, +.#{$fa-css-prefix}-caret-square-o-right:before { content: $fa-var-caret-square-o-right; } +.#{$fa-css-prefix}-euro:before, +.#{$fa-css-prefix}-eur:before { content: $fa-var-eur; } +.#{$fa-css-prefix}-gbp:before { content: $fa-var-gbp; } +.#{$fa-css-prefix}-dollar:before, +.#{$fa-css-prefix}-usd:before { content: $fa-var-usd; } +.#{$fa-css-prefix}-rupee:before, +.#{$fa-css-prefix}-inr:before { content: $fa-var-inr; } +.#{$fa-css-prefix}-cny:before, +.#{$fa-css-prefix}-rmb:before, +.#{$fa-css-prefix}-yen:before, +.#{$fa-css-prefix}-jpy:before { content: $fa-var-jpy; } +.#{$fa-css-prefix}-ruble:before, +.#{$fa-css-prefix}-rouble:before, +.#{$fa-css-prefix}-rub:before { content: $fa-var-rub; } +.#{$fa-css-prefix}-won:before, +.#{$fa-css-prefix}-krw:before { content: $fa-var-krw; } +.#{$fa-css-prefix}-bitcoin:before, +.#{$fa-css-prefix}-btc:before { content: $fa-var-btc; } +.#{$fa-css-prefix}-file:before { content: $fa-var-file; } +.#{$fa-css-prefix}-file-text:before { content: $fa-var-file-text; } +.#{$fa-css-prefix}-sort-alpha-asc:before { content: $fa-var-sort-alpha-asc; } +.#{$fa-css-prefix}-sort-alpha-desc:before { content: $fa-var-sort-alpha-desc; } +.#{$fa-css-prefix}-sort-amount-asc:before { content: $fa-var-sort-amount-asc; } +.#{$fa-css-prefix}-sort-amount-desc:before { content: $fa-var-sort-amount-desc; } +.#{$fa-css-prefix}-sort-numeric-asc:before { content: $fa-var-sort-numeric-asc; } +.#{$fa-css-prefix}-sort-numeric-desc:before { content: $fa-var-sort-numeric-desc; } +.#{$fa-css-prefix}-thumbs-up:before { content: $fa-var-thumbs-up; } +.#{$fa-css-prefix}-thumbs-down:before { content: $fa-var-thumbs-down; } +.#{$fa-css-prefix}-youtube-square:before { content: $fa-var-youtube-square; } +.#{$fa-css-prefix}-youtube:before { content: $fa-var-youtube; } +.#{$fa-css-prefix}-xing:before { content: $fa-var-xing; } +.#{$fa-css-prefix}-xing-square:before { content: $fa-var-xing-square; } +.#{$fa-css-prefix}-youtube-play:before { content: $fa-var-youtube-play; } +.#{$fa-css-prefix}-dropbox:before { content: $fa-var-dropbox; } +.#{$fa-css-prefix}-stack-overflow:before { content: $fa-var-stack-overflow; } +.#{$fa-css-prefix}-instagram:before { content: $fa-var-instagram; } +.#{$fa-css-prefix}-flickr:before { content: $fa-var-flickr; } +.#{$fa-css-prefix}-adn:before { content: $fa-var-adn; } +.#{$fa-css-prefix}-bitbucket:before { content: $fa-var-bitbucket; } +.#{$fa-css-prefix}-bitbucket-square:before { content: $fa-var-bitbucket-square; } +.#{$fa-css-prefix}-tumblr:before { content: $fa-var-tumblr; } +.#{$fa-css-prefix}-tumblr-square:before { content: $fa-var-tumblr-square; } +.#{$fa-css-prefix}-long-arrow-down:before { content: $fa-var-long-arrow-down; } +.#{$fa-css-prefix}-long-arrow-up:before { content: $fa-var-long-arrow-up; } +.#{$fa-css-prefix}-long-arrow-left:before { content: $fa-var-long-arrow-left; } +.#{$fa-css-prefix}-long-arrow-right:before { content: $fa-var-long-arrow-right; } +.#{$fa-css-prefix}-apple:before { content: $fa-var-apple; } +.#{$fa-css-prefix}-windows:before { content: $fa-var-windows; } +.#{$fa-css-prefix}-android:before { content: $fa-var-android; } +.#{$fa-css-prefix}-linux:before { content: $fa-var-linux; } +.#{$fa-css-prefix}-dribbble:before { content: $fa-var-dribbble; } +.#{$fa-css-prefix}-skype:before { content: $fa-var-skype; } +.#{$fa-css-prefix}-foursquare:before { content: $fa-var-foursquare; } +.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; } +.#{$fa-css-prefix}-female:before { content: $fa-var-female; } +.#{$fa-css-prefix}-male:before { content: $fa-var-male; } +.#{$fa-css-prefix}-gittip:before, +.#{$fa-css-prefix}-gratipay:before { content: $fa-var-gratipay; } +.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; } +.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; } +.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; } +.#{$fa-css-prefix}-bug:before { content: $fa-var-bug; } +.#{$fa-css-prefix}-vk:before { content: $fa-var-vk; } +.#{$fa-css-prefix}-weibo:before { content: $fa-var-weibo; } +.#{$fa-css-prefix}-renren:before { content: $fa-var-renren; } +.#{$fa-css-prefix}-pagelines:before { content: $fa-var-pagelines; } +.#{$fa-css-prefix}-stack-exchange:before { content: $fa-var-stack-exchange; } +.#{$fa-css-prefix}-arrow-circle-o-right:before { content: $fa-var-arrow-circle-o-right; } +.#{$fa-css-prefix}-arrow-circle-o-left:before { content: $fa-var-arrow-circle-o-left; } +.#{$fa-css-prefix}-toggle-left:before, +.#{$fa-css-prefix}-caret-square-o-left:before { content: $fa-var-caret-square-o-left; } +.#{$fa-css-prefix}-dot-circle-o:before { content: $fa-var-dot-circle-o; } +.#{$fa-css-prefix}-wheelchair:before { content: $fa-var-wheelchair; } +.#{$fa-css-prefix}-vimeo-square:before { content: $fa-var-vimeo-square; } +.#{$fa-css-prefix}-turkish-lira:before, +.#{$fa-css-prefix}-try:before { content: $fa-var-try; } +.#{$fa-css-prefix}-plus-square-o:before { content: $fa-var-plus-square-o; } +.#{$fa-css-prefix}-space-shuttle:before { content: $fa-var-space-shuttle; } +.#{$fa-css-prefix}-slack:before { content: $fa-var-slack; } +.#{$fa-css-prefix}-envelope-square:before { content: $fa-var-envelope-square; } +.#{$fa-css-prefix}-wordpress:before { content: $fa-var-wordpress; } +.#{$fa-css-prefix}-openid:before { content: $fa-var-openid; } +.#{$fa-css-prefix}-institution:before, +.#{$fa-css-prefix}-bank:before, +.#{$fa-css-prefix}-university:before { content: $fa-var-university; } +.#{$fa-css-prefix}-mortar-board:before, +.#{$fa-css-prefix}-graduation-cap:before { content: $fa-var-graduation-cap; } +.#{$fa-css-prefix}-yahoo:before { content: $fa-var-yahoo; } +.#{$fa-css-prefix}-google:before { content: $fa-var-google; } +.#{$fa-css-prefix}-reddit:before { content: $fa-var-reddit; } +.#{$fa-css-prefix}-reddit-square:before { content: $fa-var-reddit-square; } +.#{$fa-css-prefix}-stumbleupon-circle:before { content: $fa-var-stumbleupon-circle; } +.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; } +.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; } +.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; } +.#{$fa-css-prefix}-pied-piper-pp:before { content: $fa-var-pied-piper-pp; } +.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; } +.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; } +.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; } +.#{$fa-css-prefix}-language:before { content: $fa-var-language; } +.#{$fa-css-prefix}-fax:before { content: $fa-var-fax; } +.#{$fa-css-prefix}-building:before { content: $fa-var-building; } +.#{$fa-css-prefix}-child:before { content: $fa-var-child; } +.#{$fa-css-prefix}-paw:before { content: $fa-var-paw; } +.#{$fa-css-prefix}-spoon:before { content: $fa-var-spoon; } +.#{$fa-css-prefix}-cube:before { content: $fa-var-cube; } +.#{$fa-css-prefix}-cubes:before { content: $fa-var-cubes; } +.#{$fa-css-prefix}-behance:before { content: $fa-var-behance; } +.#{$fa-css-prefix}-behance-square:before { content: $fa-var-behance-square; } +.#{$fa-css-prefix}-steam:before { content: $fa-var-steam; } +.#{$fa-css-prefix}-steam-square:before { content: $fa-var-steam-square; } +.#{$fa-css-prefix}-recycle:before { content: $fa-var-recycle; } +.#{$fa-css-prefix}-automobile:before, +.#{$fa-css-prefix}-car:before { content: $fa-var-car; } +.#{$fa-css-prefix}-cab:before, +.#{$fa-css-prefix}-taxi:before { content: $fa-var-taxi; } +.#{$fa-css-prefix}-tree:before { content: $fa-var-tree; } +.#{$fa-css-prefix}-spotify:before { content: $fa-var-spotify; } +.#{$fa-css-prefix}-deviantart:before { content: $fa-var-deviantart; } +.#{$fa-css-prefix}-soundcloud:before { content: $fa-var-soundcloud; } +.#{$fa-css-prefix}-database:before { content: $fa-var-database; } +.#{$fa-css-prefix}-file-pdf-o:before { content: $fa-var-file-pdf-o; } +.#{$fa-css-prefix}-file-word-o:before { content: $fa-var-file-word-o; } +.#{$fa-css-prefix}-file-excel-o:before { content: $fa-var-file-excel-o; } +.#{$fa-css-prefix}-file-powerpoint-o:before { content: $fa-var-file-powerpoint-o; } +.#{$fa-css-prefix}-file-photo-o:before, +.#{$fa-css-prefix}-file-picture-o:before, +.#{$fa-css-prefix}-file-image-o:before { content: $fa-var-file-image-o; } +.#{$fa-css-prefix}-file-zip-o:before, +.#{$fa-css-prefix}-file-archive-o:before { content: $fa-var-file-archive-o; } +.#{$fa-css-prefix}-file-sound-o:before, +.#{$fa-css-prefix}-file-audio-o:before { content: $fa-var-file-audio-o; } +.#{$fa-css-prefix}-file-movie-o:before, +.#{$fa-css-prefix}-file-video-o:before { content: $fa-var-file-video-o; } +.#{$fa-css-prefix}-file-code-o:before { content: $fa-var-file-code-o; } +.#{$fa-css-prefix}-vine:before { content: $fa-var-vine; } +.#{$fa-css-prefix}-codepen:before { content: $fa-var-codepen; } +.#{$fa-css-prefix}-jsfiddle:before { content: $fa-var-jsfiddle; } +.#{$fa-css-prefix}-life-bouy:before, +.#{$fa-css-prefix}-life-buoy:before, +.#{$fa-css-prefix}-life-saver:before, +.#{$fa-css-prefix}-support:before, +.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; } +.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; } +.#{$fa-css-prefix}-ra:before, +.#{$fa-css-prefix}-resistance:before, +.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; } +.#{$fa-css-prefix}-ge:before, +.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; } +.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; } +.#{$fa-css-prefix}-git:before { content: $fa-var-git; } +.#{$fa-css-prefix}-y-combinator-square:before, +.#{$fa-css-prefix}-yc-square:before, +.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; } +.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; } +.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; } +.#{$fa-css-prefix}-wechat:before, +.#{$fa-css-prefix}-weixin:before { content: $fa-var-weixin; } +.#{$fa-css-prefix}-send:before, +.#{$fa-css-prefix}-paper-plane:before { content: $fa-var-paper-plane; } +.#{$fa-css-prefix}-send-o:before, +.#{$fa-css-prefix}-paper-plane-o:before { content: $fa-var-paper-plane-o; } +.#{$fa-css-prefix}-history:before { content: $fa-var-history; } +.#{$fa-css-prefix}-circle-thin:before { content: $fa-var-circle-thin; } +.#{$fa-css-prefix}-header:before { content: $fa-var-header; } +.#{$fa-css-prefix}-paragraph:before { content: $fa-var-paragraph; } +.#{$fa-css-prefix}-sliders:before { content: $fa-var-sliders; } +.#{$fa-css-prefix}-share-alt:before { content: $fa-var-share-alt; } +.#{$fa-css-prefix}-share-alt-square:before { content: $fa-var-share-alt-square; } +.#{$fa-css-prefix}-bomb:before { content: $fa-var-bomb; } +.#{$fa-css-prefix}-soccer-ball-o:before, +.#{$fa-css-prefix}-futbol-o:before { content: $fa-var-futbol-o; } +.#{$fa-css-prefix}-tty:before { content: $fa-var-tty; } +.#{$fa-css-prefix}-binoculars:before { content: $fa-var-binoculars; } +.#{$fa-css-prefix}-plug:before { content: $fa-var-plug; } +.#{$fa-css-prefix}-slideshare:before { content: $fa-var-slideshare; } +.#{$fa-css-prefix}-twitch:before { content: $fa-var-twitch; } +.#{$fa-css-prefix}-yelp:before { content: $fa-var-yelp; } +.#{$fa-css-prefix}-newspaper-o:before { content: $fa-var-newspaper-o; } +.#{$fa-css-prefix}-wifi:before { content: $fa-var-wifi; } +.#{$fa-css-prefix}-calculator:before { content: $fa-var-calculator; } +.#{$fa-css-prefix}-paypal:before { content: $fa-var-paypal; } +.#{$fa-css-prefix}-google-wallet:before { content: $fa-var-google-wallet; } +.#{$fa-css-prefix}-cc-visa:before { content: $fa-var-cc-visa; } +.#{$fa-css-prefix}-cc-mastercard:before { content: $fa-var-cc-mastercard; } +.#{$fa-css-prefix}-cc-discover:before { content: $fa-var-cc-discover; } +.#{$fa-css-prefix}-cc-amex:before { content: $fa-var-cc-amex; } +.#{$fa-css-prefix}-cc-paypal:before { content: $fa-var-cc-paypal; } +.#{$fa-css-prefix}-cc-stripe:before { content: $fa-var-cc-stripe; } +.#{$fa-css-prefix}-bell-slash:before { content: $fa-var-bell-slash; } +.#{$fa-css-prefix}-bell-slash-o:before { content: $fa-var-bell-slash-o; } +.#{$fa-css-prefix}-trash:before { content: $fa-var-trash; } +.#{$fa-css-prefix}-copyright:before { content: $fa-var-copyright; } +.#{$fa-css-prefix}-at:before { content: $fa-var-at; } +.#{$fa-css-prefix}-eyedropper:before { content: $fa-var-eyedropper; } +.#{$fa-css-prefix}-paint-brush:before { content: $fa-var-paint-brush; } +.#{$fa-css-prefix}-birthday-cake:before { content: $fa-var-birthday-cake; } +.#{$fa-css-prefix}-area-chart:before { content: $fa-var-area-chart; } +.#{$fa-css-prefix}-pie-chart:before { content: $fa-var-pie-chart; } +.#{$fa-css-prefix}-line-chart:before { content: $fa-var-line-chart; } +.#{$fa-css-prefix}-lastfm:before { content: $fa-var-lastfm; } +.#{$fa-css-prefix}-lastfm-square:before { content: $fa-var-lastfm-square; } +.#{$fa-css-prefix}-toggle-off:before { content: $fa-var-toggle-off; } +.#{$fa-css-prefix}-toggle-on:before { content: $fa-var-toggle-on; } +.#{$fa-css-prefix}-bicycle:before { content: $fa-var-bicycle; } +.#{$fa-css-prefix}-bus:before { content: $fa-var-bus; } +.#{$fa-css-prefix}-ioxhost:before { content: $fa-var-ioxhost; } +.#{$fa-css-prefix}-angellist:before { content: $fa-var-angellist; } +.#{$fa-css-prefix}-cc:before { content: $fa-var-cc; } +.#{$fa-css-prefix}-shekel:before, +.#{$fa-css-prefix}-sheqel:before, +.#{$fa-css-prefix}-ils:before { content: $fa-var-ils; } +.#{$fa-css-prefix}-meanpath:before { content: $fa-var-meanpath; } +.#{$fa-css-prefix}-buysellads:before { content: $fa-var-buysellads; } +.#{$fa-css-prefix}-connectdevelop:before { content: $fa-var-connectdevelop; } +.#{$fa-css-prefix}-dashcube:before { content: $fa-var-dashcube; } +.#{$fa-css-prefix}-forumbee:before { content: $fa-var-forumbee; } +.#{$fa-css-prefix}-leanpub:before { content: $fa-var-leanpub; } +.#{$fa-css-prefix}-sellsy:before { content: $fa-var-sellsy; } +.#{$fa-css-prefix}-shirtsinbulk:before { content: $fa-var-shirtsinbulk; } +.#{$fa-css-prefix}-simplybuilt:before { content: $fa-var-simplybuilt; } +.#{$fa-css-prefix}-skyatlas:before { content: $fa-var-skyatlas; } +.#{$fa-css-prefix}-cart-plus:before { content: $fa-var-cart-plus; } +.#{$fa-css-prefix}-cart-arrow-down:before { content: $fa-var-cart-arrow-down; } +.#{$fa-css-prefix}-diamond:before { content: $fa-var-diamond; } +.#{$fa-css-prefix}-ship:before { content: $fa-var-ship; } +.#{$fa-css-prefix}-user-secret:before { content: $fa-var-user-secret; } +.#{$fa-css-prefix}-motorcycle:before { content: $fa-var-motorcycle; } +.#{$fa-css-prefix}-street-view:before { content: $fa-var-street-view; } +.#{$fa-css-prefix}-heartbeat:before { content: $fa-var-heartbeat; } +.#{$fa-css-prefix}-venus:before { content: $fa-var-venus; } +.#{$fa-css-prefix}-mars:before { content: $fa-var-mars; } +.#{$fa-css-prefix}-mercury:before { content: $fa-var-mercury; } +.#{$fa-css-prefix}-intersex:before, +.#{$fa-css-prefix}-transgender:before { content: $fa-var-transgender; } +.#{$fa-css-prefix}-transgender-alt:before { content: $fa-var-transgender-alt; } +.#{$fa-css-prefix}-venus-double:before { content: $fa-var-venus-double; } +.#{$fa-css-prefix}-mars-double:before { content: $fa-var-mars-double; } +.#{$fa-css-prefix}-venus-mars:before { content: $fa-var-venus-mars; } +.#{$fa-css-prefix}-mars-stroke:before { content: $fa-var-mars-stroke; } +.#{$fa-css-prefix}-mars-stroke-v:before { content: $fa-var-mars-stroke-v; } +.#{$fa-css-prefix}-mars-stroke-h:before { content: $fa-var-mars-stroke-h; } +.#{$fa-css-prefix}-neuter:before { content: $fa-var-neuter; } +.#{$fa-css-prefix}-genderless:before { content: $fa-var-genderless; } +.#{$fa-css-prefix}-facebook-official:before { content: $fa-var-facebook-official; } +.#{$fa-css-prefix}-pinterest-p:before { content: $fa-var-pinterest-p; } +.#{$fa-css-prefix}-whatsapp:before { content: $fa-var-whatsapp; } +.#{$fa-css-prefix}-server:before { content: $fa-var-server; } +.#{$fa-css-prefix}-user-plus:before { content: $fa-var-user-plus; } +.#{$fa-css-prefix}-user-times:before { content: $fa-var-user-times; } +.#{$fa-css-prefix}-hotel:before, +.#{$fa-css-prefix}-bed:before { content: $fa-var-bed; } +.#{$fa-css-prefix}-viacoin:before { content: $fa-var-viacoin; } +.#{$fa-css-prefix}-train:before { content: $fa-var-train; } +.#{$fa-css-prefix}-subway:before { content: $fa-var-subway; } +.#{$fa-css-prefix}-medium:before { content: $fa-var-medium; } +.#{$fa-css-prefix}-yc:before, +.#{$fa-css-prefix}-y-combinator:before { content: $fa-var-y-combinator; } +.#{$fa-css-prefix}-optin-monster:before { content: $fa-var-optin-monster; } +.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; } +.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; } +.#{$fa-css-prefix}-battery-4:before, +.#{$fa-css-prefix}-battery:before, +.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; } +.#{$fa-css-prefix}-battery-3:before, +.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; } +.#{$fa-css-prefix}-battery-2:before, +.#{$fa-css-prefix}-battery-half:before { content: $fa-var-battery-half; } +.#{$fa-css-prefix}-battery-1:before, +.#{$fa-css-prefix}-battery-quarter:before { content: $fa-var-battery-quarter; } +.#{$fa-css-prefix}-battery-0:before, +.#{$fa-css-prefix}-battery-empty:before { content: $fa-var-battery-empty; } +.#{$fa-css-prefix}-mouse-pointer:before { content: $fa-var-mouse-pointer; } +.#{$fa-css-prefix}-i-cursor:before { content: $fa-var-i-cursor; } +.#{$fa-css-prefix}-object-group:before { content: $fa-var-object-group; } +.#{$fa-css-prefix}-object-ungroup:before { content: $fa-var-object-ungroup; } +.#{$fa-css-prefix}-sticky-note:before { content: $fa-var-sticky-note; } +.#{$fa-css-prefix}-sticky-note-o:before { content: $fa-var-sticky-note-o; } +.#{$fa-css-prefix}-cc-jcb:before { content: $fa-var-cc-jcb; } +.#{$fa-css-prefix}-cc-diners-club:before { content: $fa-var-cc-diners-club; } +.#{$fa-css-prefix}-clone:before { content: $fa-var-clone; } +.#{$fa-css-prefix}-balance-scale:before { content: $fa-var-balance-scale; } +.#{$fa-css-prefix}-hourglass-o:before { content: $fa-var-hourglass-o; } +.#{$fa-css-prefix}-hourglass-1:before, +.#{$fa-css-prefix}-hourglass-start:before { content: $fa-var-hourglass-start; } +.#{$fa-css-prefix}-hourglass-2:before, +.#{$fa-css-prefix}-hourglass-half:before { content: $fa-var-hourglass-half; } +.#{$fa-css-prefix}-hourglass-3:before, +.#{$fa-css-prefix}-hourglass-end:before { content: $fa-var-hourglass-end; } +.#{$fa-css-prefix}-hourglass:before { content: $fa-var-hourglass; } +.#{$fa-css-prefix}-hand-grab-o:before, +.#{$fa-css-prefix}-hand-rock-o:before { content: $fa-var-hand-rock-o; } +.#{$fa-css-prefix}-hand-stop-o:before, +.#{$fa-css-prefix}-hand-paper-o:before { content: $fa-var-hand-paper-o; } +.#{$fa-css-prefix}-hand-scissors-o:before { content: $fa-var-hand-scissors-o; } +.#{$fa-css-prefix}-hand-lizard-o:before { content: $fa-var-hand-lizard-o; } +.#{$fa-css-prefix}-hand-spock-o:before { content: $fa-var-hand-spock-o; } +.#{$fa-css-prefix}-hand-pointer-o:before { content: $fa-var-hand-pointer-o; } +.#{$fa-css-prefix}-hand-peace-o:before { content: $fa-var-hand-peace-o; } +.#{$fa-css-prefix}-trademark:before { content: $fa-var-trademark; } +.#{$fa-css-prefix}-registered:before { content: $fa-var-registered; } +.#{$fa-css-prefix}-creative-commons:before { content: $fa-var-creative-commons; } +.#{$fa-css-prefix}-gg:before { content: $fa-var-gg; } +.#{$fa-css-prefix}-gg-circle:before { content: $fa-var-gg-circle; } +.#{$fa-css-prefix}-tripadvisor:before { content: $fa-var-tripadvisor; } +.#{$fa-css-prefix}-odnoklassniki:before { content: $fa-var-odnoklassniki; } +.#{$fa-css-prefix}-odnoklassniki-square:before { content: $fa-var-odnoklassniki-square; } +.#{$fa-css-prefix}-get-pocket:before { content: $fa-var-get-pocket; } +.#{$fa-css-prefix}-wikipedia-w:before { content: $fa-var-wikipedia-w; } +.#{$fa-css-prefix}-safari:before { content: $fa-var-safari; } +.#{$fa-css-prefix}-chrome:before { content: $fa-var-chrome; } +.#{$fa-css-prefix}-firefox:before { content: $fa-var-firefox; } +.#{$fa-css-prefix}-opera:before { content: $fa-var-opera; } +.#{$fa-css-prefix}-internet-explorer:before { content: $fa-var-internet-explorer; } +.#{$fa-css-prefix}-tv:before, +.#{$fa-css-prefix}-television:before { content: $fa-var-television; } +.#{$fa-css-prefix}-contao:before { content: $fa-var-contao; } +.#{$fa-css-prefix}-500px:before { content: $fa-var-500px; } +.#{$fa-css-prefix}-amazon:before { content: $fa-var-amazon; } +.#{$fa-css-prefix}-calendar-plus-o:before { content: $fa-var-calendar-plus-o; } +.#{$fa-css-prefix}-calendar-minus-o:before { content: $fa-var-calendar-minus-o; } +.#{$fa-css-prefix}-calendar-times-o:before { content: $fa-var-calendar-times-o; } +.#{$fa-css-prefix}-calendar-check-o:before { content: $fa-var-calendar-check-o; } +.#{$fa-css-prefix}-industry:before { content: $fa-var-industry; } +.#{$fa-css-prefix}-map-pin:before { content: $fa-var-map-pin; } +.#{$fa-css-prefix}-map-signs:before { content: $fa-var-map-signs; } +.#{$fa-css-prefix}-map-o:before { content: $fa-var-map-o; } +.#{$fa-css-prefix}-map:before { content: $fa-var-map; } +.#{$fa-css-prefix}-commenting:before { content: $fa-var-commenting; } +.#{$fa-css-prefix}-commenting-o:before { content: $fa-var-commenting-o; } +.#{$fa-css-prefix}-houzz:before { content: $fa-var-houzz; } +.#{$fa-css-prefix}-vimeo:before { content: $fa-var-vimeo; } +.#{$fa-css-prefix}-black-tie:before { content: $fa-var-black-tie; } +.#{$fa-css-prefix}-fonticons:before { content: $fa-var-fonticons; } +.#{$fa-css-prefix}-reddit-alien:before { content: $fa-var-reddit-alien; } +.#{$fa-css-prefix}-edge:before { content: $fa-var-edge; } +.#{$fa-css-prefix}-credit-card-alt:before { content: $fa-var-credit-card-alt; } +.#{$fa-css-prefix}-codiepie:before { content: $fa-var-codiepie; } +.#{$fa-css-prefix}-modx:before { content: $fa-var-modx; } +.#{$fa-css-prefix}-fort-awesome:before { content: $fa-var-fort-awesome; } +.#{$fa-css-prefix}-usb:before { content: $fa-var-usb; } +.#{$fa-css-prefix}-product-hunt:before { content: $fa-var-product-hunt; } +.#{$fa-css-prefix}-mixcloud:before { content: $fa-var-mixcloud; } +.#{$fa-css-prefix}-scribd:before { content: $fa-var-scribd; } +.#{$fa-css-prefix}-pause-circle:before { content: $fa-var-pause-circle; } +.#{$fa-css-prefix}-pause-circle-o:before { content: $fa-var-pause-circle-o; } +.#{$fa-css-prefix}-stop-circle:before { content: $fa-var-stop-circle; } +.#{$fa-css-prefix}-stop-circle-o:before { content: $fa-var-stop-circle-o; } +.#{$fa-css-prefix}-shopping-bag:before { content: $fa-var-shopping-bag; } +.#{$fa-css-prefix}-shopping-basket:before { content: $fa-var-shopping-basket; } +.#{$fa-css-prefix}-hashtag:before { content: $fa-var-hashtag; } +.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; } +.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; } +.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; } +.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; } +.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; } +.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; } +.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; } +.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; } +.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; } +.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; } +.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; } +.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; } +.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; } +.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; } +.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; } +.#{$fa-css-prefix}-asl-interpreting:before, +.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; } +.#{$fa-css-prefix}-deafness:before, +.#{$fa-css-prefix}-hard-of-hearing:before, +.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; } +.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; } +.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; } +.#{$fa-css-prefix}-signing:before, +.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; } +.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; } +.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; } +.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; } +.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; } +.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; } +.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; } +.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; } +.#{$fa-css-prefix}-first-order:before { content: $fa-var-first-order; } +.#{$fa-css-prefix}-yoast:before { content: $fa-var-yoast; } +.#{$fa-css-prefix}-themeisle:before { content: $fa-var-themeisle; } +.#{$fa-css-prefix}-google-plus-circle:before, +.#{$fa-css-prefix}-google-plus-official:before { content: $fa-var-google-plus-official; } +.#{$fa-css-prefix}-fa:before, +.#{$fa-css-prefix}-font-awesome:before { content: $fa-var-font-awesome; } +.#{$fa-css-prefix}-handshake-o:before { content: $fa-var-handshake-o; } +.#{$fa-css-prefix}-envelope-open:before { content: $fa-var-envelope-open; } +.#{$fa-css-prefix}-envelope-open-o:before { content: $fa-var-envelope-open-o; } +.#{$fa-css-prefix}-linode:before { content: $fa-var-linode; } +.#{$fa-css-prefix}-address-book:before { content: $fa-var-address-book; } +.#{$fa-css-prefix}-address-book-o:before { content: $fa-var-address-book-o; } +.#{$fa-css-prefix}-vcard:before, +.#{$fa-css-prefix}-address-card:before { content: $fa-var-address-card; } +.#{$fa-css-prefix}-vcard-o:before, +.#{$fa-css-prefix}-address-card-o:before { content: $fa-var-address-card-o; } +.#{$fa-css-prefix}-user-circle:before { content: $fa-var-user-circle; } +.#{$fa-css-prefix}-user-circle-o:before { content: $fa-var-user-circle-o; } +.#{$fa-css-prefix}-user-o:before { content: $fa-var-user-o; } +.#{$fa-css-prefix}-id-badge:before { content: $fa-var-id-badge; } +.#{$fa-css-prefix}-drivers-license:before, +.#{$fa-css-prefix}-id-card:before { content: $fa-var-id-card; } +.#{$fa-css-prefix}-drivers-license-o:before, +.#{$fa-css-prefix}-id-card-o:before { content: $fa-var-id-card-o; } +.#{$fa-css-prefix}-quora:before { content: $fa-var-quora; } +.#{$fa-css-prefix}-free-code-camp:before { content: $fa-var-free-code-camp; } +.#{$fa-css-prefix}-telegram:before { content: $fa-var-telegram; } +.#{$fa-css-prefix}-thermometer-4:before, +.#{$fa-css-prefix}-thermometer:before, +.#{$fa-css-prefix}-thermometer-full:before { content: $fa-var-thermometer-full; } +.#{$fa-css-prefix}-thermometer-3:before, +.#{$fa-css-prefix}-thermometer-three-quarters:before { content: $fa-var-thermometer-three-quarters; } +.#{$fa-css-prefix}-thermometer-2:before, +.#{$fa-css-prefix}-thermometer-half:before { content: $fa-var-thermometer-half; } +.#{$fa-css-prefix}-thermometer-1:before, +.#{$fa-css-prefix}-thermometer-quarter:before { content: $fa-var-thermometer-quarter; } +.#{$fa-css-prefix}-thermometer-0:before, +.#{$fa-css-prefix}-thermometer-empty:before { content: $fa-var-thermometer-empty; } +.#{$fa-css-prefix}-shower:before { content: $fa-var-shower; } +.#{$fa-css-prefix}-bathtub:before, +.#{$fa-css-prefix}-s15:before, +.#{$fa-css-prefix}-bath:before { content: $fa-var-bath; } +.#{$fa-css-prefix}-podcast:before { content: $fa-var-podcast; } +.#{$fa-css-prefix}-window-maximize:before { content: $fa-var-window-maximize; } +.#{$fa-css-prefix}-window-minimize:before { content: $fa-var-window-minimize; } +.#{$fa-css-prefix}-window-restore:before { content: $fa-var-window-restore; } +.#{$fa-css-prefix}-times-rectangle:before, +.#{$fa-css-prefix}-window-close:before { content: $fa-var-window-close; } +.#{$fa-css-prefix}-times-rectangle-o:before, +.#{$fa-css-prefix}-window-close-o:before { content: $fa-var-window-close-o; } +.#{$fa-css-prefix}-bandcamp:before { content: $fa-var-bandcamp; } +.#{$fa-css-prefix}-grav:before { content: $fa-var-grav; } +.#{$fa-css-prefix}-etsy:before { content: $fa-var-etsy; } +.#{$fa-css-prefix}-imdb:before { content: $fa-var-imdb; } +.#{$fa-css-prefix}-ravelry:before { content: $fa-var-ravelry; } +.#{$fa-css-prefix}-eercast:before { content: $fa-var-eercast; } +.#{$fa-css-prefix}-microchip:before { content: $fa-var-microchip; } +.#{$fa-css-prefix}-snowflake-o:before { content: $fa-var-snowflake-o; } +.#{$fa-css-prefix}-superpowers:before { content: $fa-var-superpowers; } +.#{$fa-css-prefix}-wpexplorer:before { content: $fa-var-wpexplorer; } +.#{$fa-css-prefix}-meetup:before { content: $fa-var-meetup; } diff --git a/public/installer/css/scss/font-awesome/_larger.scss b/public/installer/css/scss/font-awesome/_larger.scss new file mode 100755 index 000000000..41e9a8184 --- /dev/null +++ b/public/installer/css/scss/font-awesome/_larger.scss @@ -0,0 +1,13 @@ +// Icon Sizes +// ------------------------- + +/* makes the font 33% larger relative to the icon container */ +.#{$fa-css-prefix}-lg { + font-size: (4em / 3); + line-height: (3em / 4); + vertical-align: -15%; +} +.#{$fa-css-prefix}-2x { font-size: 2em; } +.#{$fa-css-prefix}-3x { font-size: 3em; } +.#{$fa-css-prefix}-4x { font-size: 4em; } +.#{$fa-css-prefix}-5x { font-size: 5em; } diff --git a/public/installer/css/scss/font-awesome/_list.scss b/public/installer/css/scss/font-awesome/_list.scss new file mode 100755 index 000000000..7d1e4d54d --- /dev/null +++ b/public/installer/css/scss/font-awesome/_list.scss @@ -0,0 +1,19 @@ +// List Icons +// ------------------------- + +.#{$fa-css-prefix}-ul { + padding-left: 0; + margin-left: $fa-li-width; + list-style-type: none; + > li { position: relative; } +} +.#{$fa-css-prefix}-li { + position: absolute; + left: -$fa-li-width; + width: $fa-li-width; + top: (2em / 14); + text-align: center; + &.#{$fa-css-prefix}-lg { + left: -$fa-li-width + (4em / 14); + } +} diff --git a/public/installer/css/scss/font-awesome/_mixins.scss b/public/installer/css/scss/font-awesome/_mixins.scss new file mode 100755 index 000000000..c3bbd5745 --- /dev/null +++ b/public/installer/css/scss/font-awesome/_mixins.scss @@ -0,0 +1,60 @@ +// Mixins +// -------------------------- + +@mixin fa-icon() { + display: inline-block; + font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration + font-size: inherit; // can't have font-size inherit on line above, so need to override + text-rendering: auto; // optimizelegibility throws things off #1094 + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + +} + +@mixin fa-icon-rotate($degrees, $rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; + -webkit-transform: rotate($degrees); + -ms-transform: rotate($degrees); + transform: rotate($degrees); +} + +@mixin fa-icon-flip($horiz, $vert, $rotation) { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; + -webkit-transform: scale($horiz, $vert); + -ms-transform: scale($horiz, $vert); + transform: scale($horiz, $vert); +} + + +// Only display content to screen readers. A la Bootstrap 4. +// +// See: http://a11yproject.com/posts/how-to-hide-content/ + +@mixin sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0,0,0,0); + border: 0; +} + +// Use in conjunction with .sr-only to only display content when it's focused. +// +// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 +// +// Credit: HTML5 Boilerplate + +@mixin sr-only-focusable { + &:active, + &:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; + } +} diff --git a/public/installer/css/scss/font-awesome/_path.scss b/public/installer/css/scss/font-awesome/_path.scss new file mode 100755 index 000000000..bb457c23a --- /dev/null +++ b/public/installer/css/scss/font-awesome/_path.scss @@ -0,0 +1,15 @@ +/* FONT PATH + * -------------------------- */ + +@font-face { + font-family: 'FontAwesome'; + src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); + src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), + url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), + url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), + url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), + url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); +// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts + font-weight: normal; + font-style: normal; +} diff --git a/public/installer/css/scss/font-awesome/_rotated-flipped.scss b/public/installer/css/scss/font-awesome/_rotated-flipped.scss new file mode 100755 index 000000000..a3558fd09 --- /dev/null +++ b/public/installer/css/scss/font-awesome/_rotated-flipped.scss @@ -0,0 +1,20 @@ +// Rotated & Flipped Icons +// ------------------------- + +.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } +.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } +.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } + +.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } +.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } + +// Hook for IE8-9 +// ------------------------- + +:root .#{$fa-css-prefix}-rotate-90, +:root .#{$fa-css-prefix}-rotate-180, +:root .#{$fa-css-prefix}-rotate-270, +:root .#{$fa-css-prefix}-flip-horizontal, +:root .#{$fa-css-prefix}-flip-vertical { + filter: none; +} diff --git a/public/installer/css/scss/font-awesome/_screen-reader.scss b/public/installer/css/scss/font-awesome/_screen-reader.scss new file mode 100755 index 000000000..637426f0d --- /dev/null +++ b/public/installer/css/scss/font-awesome/_screen-reader.scss @@ -0,0 +1,5 @@ +// Screen Readers +// ------------------------- + +.sr-only { @include sr-only(); } +.sr-only-focusable { @include sr-only-focusable(); } diff --git a/public/installer/css/scss/font-awesome/_stacked.scss b/public/installer/css/scss/font-awesome/_stacked.scss new file mode 100755 index 000000000..aef740366 --- /dev/null +++ b/public/installer/css/scss/font-awesome/_stacked.scss @@ -0,0 +1,20 @@ +// Stacked Icons +// ------------------------- + +.#{$fa-css-prefix}-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.#{$fa-css-prefix}-stack-1x { line-height: inherit; } +.#{$fa-css-prefix}-stack-2x { font-size: 2em; } +.#{$fa-css-prefix}-inverse { color: $fa-inverse; } diff --git a/public/installer/css/scss/font-awesome/_variables.scss b/public/installer/css/scss/font-awesome/_variables.scss new file mode 100755 index 000000000..498fc4a08 --- /dev/null +++ b/public/installer/css/scss/font-awesome/_variables.scss @@ -0,0 +1,800 @@ +// Variables +// -------------------------- + +$fa-font-path: "../fonts" !default; +$fa-font-size-base: 14px !default; +$fa-line-height-base: 1 !default; +//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly +$fa-css-prefix: fa !default; +$fa-version: "4.7.0" !default; +$fa-border-color: #eee !default; +$fa-inverse: #fff !default; +$fa-li-width: (30em / 14) !default; + +$fa-var-500px: "\f26e"; +$fa-var-address-book: "\f2b9"; +$fa-var-address-book-o: "\f2ba"; +$fa-var-address-card: "\f2bb"; +$fa-var-address-card-o: "\f2bc"; +$fa-var-adjust: "\f042"; +$fa-var-adn: "\f170"; +$fa-var-align-center: "\f037"; +$fa-var-align-justify: "\f039"; +$fa-var-align-left: "\f036"; +$fa-var-align-right: "\f038"; +$fa-var-amazon: "\f270"; +$fa-var-ambulance: "\f0f9"; +$fa-var-american-sign-language-interpreting: "\f2a3"; +$fa-var-anchor: "\f13d"; +$fa-var-android: "\f17b"; +$fa-var-angellist: "\f209"; +$fa-var-angle-double-down: "\f103"; +$fa-var-angle-double-left: "\f100"; +$fa-var-angle-double-right: "\f101"; +$fa-var-angle-double-up: "\f102"; +$fa-var-angle-down: "\f107"; +$fa-var-angle-left: "\f104"; +$fa-var-angle-right: "\f105"; +$fa-var-angle-up: "\f106"; +$fa-var-apple: "\f179"; +$fa-var-archive: "\f187"; +$fa-var-area-chart: "\f1fe"; +$fa-var-arrow-circle-down: "\f0ab"; +$fa-var-arrow-circle-left: "\f0a8"; +$fa-var-arrow-circle-o-down: "\f01a"; +$fa-var-arrow-circle-o-left: "\f190"; +$fa-var-arrow-circle-o-right: "\f18e"; +$fa-var-arrow-circle-o-up: "\f01b"; +$fa-var-arrow-circle-right: "\f0a9"; +$fa-var-arrow-circle-up: "\f0aa"; +$fa-var-arrow-down: "\f063"; +$fa-var-arrow-left: "\f060"; +$fa-var-arrow-right: "\f061"; +$fa-var-arrow-up: "\f062"; +$fa-var-arrows: "\f047"; +$fa-var-arrows-alt: "\f0b2"; +$fa-var-arrows-h: "\f07e"; +$fa-var-arrows-v: "\f07d"; +$fa-var-asl-interpreting: "\f2a3"; +$fa-var-assistive-listening-systems: "\f2a2"; +$fa-var-asterisk: "\f069"; +$fa-var-at: "\f1fa"; +$fa-var-audio-description: "\f29e"; +$fa-var-automobile: "\f1b9"; +$fa-var-backward: "\f04a"; +$fa-var-balance-scale: "\f24e"; +$fa-var-ban: "\f05e"; +$fa-var-bandcamp: "\f2d5"; +$fa-var-bank: "\f19c"; +$fa-var-bar-chart: "\f080"; +$fa-var-bar-chart-o: "\f080"; +$fa-var-barcode: "\f02a"; +$fa-var-bars: "\f0c9"; +$fa-var-bath: "\f2cd"; +$fa-var-bathtub: "\f2cd"; +$fa-var-battery: "\f240"; +$fa-var-battery-0: "\f244"; +$fa-var-battery-1: "\f243"; +$fa-var-battery-2: "\f242"; +$fa-var-battery-3: "\f241"; +$fa-var-battery-4: "\f240"; +$fa-var-battery-empty: "\f244"; +$fa-var-battery-full: "\f240"; +$fa-var-battery-half: "\f242"; +$fa-var-battery-quarter: "\f243"; +$fa-var-battery-three-quarters: "\f241"; +$fa-var-bed: "\f236"; +$fa-var-beer: "\f0fc"; +$fa-var-behance: "\f1b4"; +$fa-var-behance-square: "\f1b5"; +$fa-var-bell: "\f0f3"; +$fa-var-bell-o: "\f0a2"; +$fa-var-bell-slash: "\f1f6"; +$fa-var-bell-slash-o: "\f1f7"; +$fa-var-bicycle: "\f206"; +$fa-var-binoculars: "\f1e5"; +$fa-var-birthday-cake: "\f1fd"; +$fa-var-bitbucket: "\f171"; +$fa-var-bitbucket-square: "\f172"; +$fa-var-bitcoin: "\f15a"; +$fa-var-black-tie: "\f27e"; +$fa-var-blind: "\f29d"; +$fa-var-bluetooth: "\f293"; +$fa-var-bluetooth-b: "\f294"; +$fa-var-bold: "\f032"; +$fa-var-bolt: "\f0e7"; +$fa-var-bomb: "\f1e2"; +$fa-var-book: "\f02d"; +$fa-var-bookmark: "\f02e"; +$fa-var-bookmark-o: "\f097"; +$fa-var-braille: "\f2a1"; +$fa-var-briefcase: "\f0b1"; +$fa-var-btc: "\f15a"; +$fa-var-bug: "\f188"; +$fa-var-building: "\f1ad"; +$fa-var-building-o: "\f0f7"; +$fa-var-bullhorn: "\f0a1"; +$fa-var-bullseye: "\f140"; +$fa-var-bus: "\f207"; +$fa-var-buysellads: "\f20d"; +$fa-var-cab: "\f1ba"; +$fa-var-calculator: "\f1ec"; +$fa-var-calendar: "\f073"; +$fa-var-calendar-check-o: "\f274"; +$fa-var-calendar-minus-o: "\f272"; +$fa-var-calendar-o: "\f133"; +$fa-var-calendar-plus-o: "\f271"; +$fa-var-calendar-times-o: "\f273"; +$fa-var-camera: "\f030"; +$fa-var-camera-retro: "\f083"; +$fa-var-car: "\f1b9"; +$fa-var-caret-down: "\f0d7"; +$fa-var-caret-left: "\f0d9"; +$fa-var-caret-right: "\f0da"; +$fa-var-caret-square-o-down: "\f150"; +$fa-var-caret-square-o-left: "\f191"; +$fa-var-caret-square-o-right: "\f152"; +$fa-var-caret-square-o-up: "\f151"; +$fa-var-caret-up: "\f0d8"; +$fa-var-cart-arrow-down: "\f218"; +$fa-var-cart-plus: "\f217"; +$fa-var-cc: "\f20a"; +$fa-var-cc-amex: "\f1f3"; +$fa-var-cc-diners-club: "\f24c"; +$fa-var-cc-discover: "\f1f2"; +$fa-var-cc-jcb: "\f24b"; +$fa-var-cc-mastercard: "\f1f1"; +$fa-var-cc-paypal: "\f1f4"; +$fa-var-cc-stripe: "\f1f5"; +$fa-var-cc-visa: "\f1f0"; +$fa-var-certificate: "\f0a3"; +$fa-var-chain: "\f0c1"; +$fa-var-chain-broken: "\f127"; +$fa-var-check: "\f00c"; +$fa-var-check-circle: "\f058"; +$fa-var-check-circle-o: "\f05d"; +$fa-var-check-square: "\f14a"; +$fa-var-check-square-o: "\f046"; +$fa-var-chevron-circle-down: "\f13a"; +$fa-var-chevron-circle-left: "\f137"; +$fa-var-chevron-circle-right: "\f138"; +$fa-var-chevron-circle-up: "\f139"; +$fa-var-chevron-down: "\f078"; +$fa-var-chevron-left: "\f053"; +$fa-var-chevron-right: "\f054"; +$fa-var-chevron-up: "\f077"; +$fa-var-child: "\f1ae"; +$fa-var-chrome: "\f268"; +$fa-var-circle: "\f111"; +$fa-var-circle-o: "\f10c"; +$fa-var-circle-o-notch: "\f1ce"; +$fa-var-circle-thin: "\f1db"; +$fa-var-clipboard: "\f0ea"; +$fa-var-clock-o: "\f017"; +$fa-var-clone: "\f24d"; +$fa-var-close: "\f00d"; +$fa-var-cloud: "\f0c2"; +$fa-var-cloud-download: "\f0ed"; +$fa-var-cloud-upload: "\f0ee"; +$fa-var-cny: "\f157"; +$fa-var-code: "\f121"; +$fa-var-code-fork: "\f126"; +$fa-var-codepen: "\f1cb"; +$fa-var-codiepie: "\f284"; +$fa-var-coffee: "\f0f4"; +$fa-var-cog: "\f013"; +$fa-var-cogs: "\f085"; +$fa-var-columns: "\f0db"; +$fa-var-comment: "\f075"; +$fa-var-comment-o: "\f0e5"; +$fa-var-commenting: "\f27a"; +$fa-var-commenting-o: "\f27b"; +$fa-var-comments: "\f086"; +$fa-var-comments-o: "\f0e6"; +$fa-var-compass: "\f14e"; +$fa-var-compress: "\f066"; +$fa-var-connectdevelop: "\f20e"; +$fa-var-contao: "\f26d"; +$fa-var-copy: "\f0c5"; +$fa-var-copyright: "\f1f9"; +$fa-var-creative-commons: "\f25e"; +$fa-var-credit-card: "\f09d"; +$fa-var-credit-card-alt: "\f283"; +$fa-var-crop: "\f125"; +$fa-var-crosshairs: "\f05b"; +$fa-var-css3: "\f13c"; +$fa-var-cube: "\f1b2"; +$fa-var-cubes: "\f1b3"; +$fa-var-cut: "\f0c4"; +$fa-var-cutlery: "\f0f5"; +$fa-var-dashboard: "\f0e4"; +$fa-var-dashcube: "\f210"; +$fa-var-database: "\f1c0"; +$fa-var-deaf: "\f2a4"; +$fa-var-deafness: "\f2a4"; +$fa-var-dedent: "\f03b"; +$fa-var-delicious: "\f1a5"; +$fa-var-desktop: "\f108"; +$fa-var-deviantart: "\f1bd"; +$fa-var-diamond: "\f219"; +$fa-var-digg: "\f1a6"; +$fa-var-dollar: "\f155"; +$fa-var-dot-circle-o: "\f192"; +$fa-var-download: "\f019"; +$fa-var-dribbble: "\f17d"; +$fa-var-drivers-license: "\f2c2"; +$fa-var-drivers-license-o: "\f2c3"; +$fa-var-dropbox: "\f16b"; +$fa-var-drupal: "\f1a9"; +$fa-var-edge: "\f282"; +$fa-var-edit: "\f044"; +$fa-var-eercast: "\f2da"; +$fa-var-eject: "\f052"; +$fa-var-ellipsis-h: "\f141"; +$fa-var-ellipsis-v: "\f142"; +$fa-var-empire: "\f1d1"; +$fa-var-envelope: "\f0e0"; +$fa-var-envelope-o: "\f003"; +$fa-var-envelope-open: "\f2b6"; +$fa-var-envelope-open-o: "\f2b7"; +$fa-var-envelope-square: "\f199"; +$fa-var-envira: "\f299"; +$fa-var-eraser: "\f12d"; +$fa-var-etsy: "\f2d7"; +$fa-var-eur: "\f153"; +$fa-var-euro: "\f153"; +$fa-var-exchange: "\f0ec"; +$fa-var-exclamation: "\f12a"; +$fa-var-exclamation-circle: "\f06a"; +$fa-var-exclamation-triangle: "\f071"; +$fa-var-expand: "\f065"; +$fa-var-expeditedssl: "\f23e"; +$fa-var-external-link: "\f08e"; +$fa-var-external-link-square: "\f14c"; +$fa-var-eye: "\f06e"; +$fa-var-eye-slash: "\f070"; +$fa-var-eyedropper: "\f1fb"; +$fa-var-fa: "\f2b4"; +$fa-var-facebook: "\f09a"; +$fa-var-facebook-f: "\f09a"; +$fa-var-facebook-official: "\f230"; +$fa-var-facebook-square: "\f082"; +$fa-var-fast-backward: "\f049"; +$fa-var-fast-forward: "\f050"; +$fa-var-fax: "\f1ac"; +$fa-var-feed: "\f09e"; +$fa-var-female: "\f182"; +$fa-var-fighter-jet: "\f0fb"; +$fa-var-file: "\f15b"; +$fa-var-file-archive-o: "\f1c6"; +$fa-var-file-audio-o: "\f1c7"; +$fa-var-file-code-o: "\f1c9"; +$fa-var-file-excel-o: "\f1c3"; +$fa-var-file-image-o: "\f1c5"; +$fa-var-file-movie-o: "\f1c8"; +$fa-var-file-o: "\f016"; +$fa-var-file-pdf-o: "\f1c1"; +$fa-var-file-photo-o: "\f1c5"; +$fa-var-file-picture-o: "\f1c5"; +$fa-var-file-powerpoint-o: "\f1c4"; +$fa-var-file-sound-o: "\f1c7"; +$fa-var-file-text: "\f15c"; +$fa-var-file-text-o: "\f0f6"; +$fa-var-file-video-o: "\f1c8"; +$fa-var-file-word-o: "\f1c2"; +$fa-var-file-zip-o: "\f1c6"; +$fa-var-files-o: "\f0c5"; +$fa-var-film: "\f008"; +$fa-var-filter: "\f0b0"; +$fa-var-fire: "\f06d"; +$fa-var-fire-extinguisher: "\f134"; +$fa-var-firefox: "\f269"; +$fa-var-first-order: "\f2b0"; +$fa-var-flag: "\f024"; +$fa-var-flag-checkered: "\f11e"; +$fa-var-flag-o: "\f11d"; +$fa-var-flash: "\f0e7"; +$fa-var-flask: "\f0c3"; +$fa-var-flickr: "\f16e"; +$fa-var-floppy-o: "\f0c7"; +$fa-var-folder: "\f07b"; +$fa-var-folder-o: "\f114"; +$fa-var-folder-open: "\f07c"; +$fa-var-folder-open-o: "\f115"; +$fa-var-font: "\f031"; +$fa-var-font-awesome: "\f2b4"; +$fa-var-fonticons: "\f280"; +$fa-var-fort-awesome: "\f286"; +$fa-var-forumbee: "\f211"; +$fa-var-forward: "\f04e"; +$fa-var-foursquare: "\f180"; +$fa-var-free-code-camp: "\f2c5"; +$fa-var-frown-o: "\f119"; +$fa-var-futbol-o: "\f1e3"; +$fa-var-gamepad: "\f11b"; +$fa-var-gavel: "\f0e3"; +$fa-var-gbp: "\f154"; +$fa-var-ge: "\f1d1"; +$fa-var-gear: "\f013"; +$fa-var-gears: "\f085"; +$fa-var-genderless: "\f22d"; +$fa-var-get-pocket: "\f265"; +$fa-var-gg: "\f260"; +$fa-var-gg-circle: "\f261"; +$fa-var-gift: "\f06b"; +$fa-var-git: "\f1d3"; +$fa-var-git-square: "\f1d2"; +$fa-var-github: "\f09b"; +$fa-var-github-alt: "\f113"; +$fa-var-github-square: "\f092"; +$fa-var-gitlab: "\f296"; +$fa-var-gittip: "\f184"; +$fa-var-glass: "\f000"; +$fa-var-glide: "\f2a5"; +$fa-var-glide-g: "\f2a6"; +$fa-var-globe: "\f0ac"; +$fa-var-google: "\f1a0"; +$fa-var-google-plus: "\f0d5"; +$fa-var-google-plus-circle: "\f2b3"; +$fa-var-google-plus-official: "\f2b3"; +$fa-var-google-plus-square: "\f0d4"; +$fa-var-google-wallet: "\f1ee"; +$fa-var-graduation-cap: "\f19d"; +$fa-var-gratipay: "\f184"; +$fa-var-grav: "\f2d6"; +$fa-var-group: "\f0c0"; +$fa-var-h-square: "\f0fd"; +$fa-var-hacker-news: "\f1d4"; +$fa-var-hand-grab-o: "\f255"; +$fa-var-hand-lizard-o: "\f258"; +$fa-var-hand-o-down: "\f0a7"; +$fa-var-hand-o-left: "\f0a5"; +$fa-var-hand-o-right: "\f0a4"; +$fa-var-hand-o-up: "\f0a6"; +$fa-var-hand-paper-o: "\f256"; +$fa-var-hand-peace-o: "\f25b"; +$fa-var-hand-pointer-o: "\f25a"; +$fa-var-hand-rock-o: "\f255"; +$fa-var-hand-scissors-o: "\f257"; +$fa-var-hand-spock-o: "\f259"; +$fa-var-hand-stop-o: "\f256"; +$fa-var-handshake-o: "\f2b5"; +$fa-var-hard-of-hearing: "\f2a4"; +$fa-var-hashtag: "\f292"; +$fa-var-hdd-o: "\f0a0"; +$fa-var-header: "\f1dc"; +$fa-var-headphones: "\f025"; +$fa-var-heart: "\f004"; +$fa-var-heart-o: "\f08a"; +$fa-var-heartbeat: "\f21e"; +$fa-var-history: "\f1da"; +$fa-var-home: "\f015"; +$fa-var-hospital-o: "\f0f8"; +$fa-var-hotel: "\f236"; +$fa-var-hourglass: "\f254"; +$fa-var-hourglass-1: "\f251"; +$fa-var-hourglass-2: "\f252"; +$fa-var-hourglass-3: "\f253"; +$fa-var-hourglass-end: "\f253"; +$fa-var-hourglass-half: "\f252"; +$fa-var-hourglass-o: "\f250"; +$fa-var-hourglass-start: "\f251"; +$fa-var-houzz: "\f27c"; +$fa-var-html5: "\f13b"; +$fa-var-i-cursor: "\f246"; +$fa-var-id-badge: "\f2c1"; +$fa-var-id-card: "\f2c2"; +$fa-var-id-card-o: "\f2c3"; +$fa-var-ils: "\f20b"; +$fa-var-image: "\f03e"; +$fa-var-imdb: "\f2d8"; +$fa-var-inbox: "\f01c"; +$fa-var-indent: "\f03c"; +$fa-var-industry: "\f275"; +$fa-var-info: "\f129"; +$fa-var-info-circle: "\f05a"; +$fa-var-inr: "\f156"; +$fa-var-instagram: "\f16d"; +$fa-var-institution: "\f19c"; +$fa-var-internet-explorer: "\f26b"; +$fa-var-intersex: "\f224"; +$fa-var-ioxhost: "\f208"; +$fa-var-italic: "\f033"; +$fa-var-joomla: "\f1aa"; +$fa-var-jpy: "\f157"; +$fa-var-jsfiddle: "\f1cc"; +$fa-var-key: "\f084"; +$fa-var-keyboard-o: "\f11c"; +$fa-var-krw: "\f159"; +$fa-var-language: "\f1ab"; +$fa-var-laptop: "\f109"; +$fa-var-lastfm: "\f202"; +$fa-var-lastfm-square: "\f203"; +$fa-var-leaf: "\f06c"; +$fa-var-leanpub: "\f212"; +$fa-var-legal: "\f0e3"; +$fa-var-lemon-o: "\f094"; +$fa-var-level-down: "\f149"; +$fa-var-level-up: "\f148"; +$fa-var-life-bouy: "\f1cd"; +$fa-var-life-buoy: "\f1cd"; +$fa-var-life-ring: "\f1cd"; +$fa-var-life-saver: "\f1cd"; +$fa-var-lightbulb-o: "\f0eb"; +$fa-var-line-chart: "\f201"; +$fa-var-link: "\f0c1"; +$fa-var-linkedin: "\f0e1"; +$fa-var-linkedin-square: "\f08c"; +$fa-var-linode: "\f2b8"; +$fa-var-linux: "\f17c"; +$fa-var-list: "\f03a"; +$fa-var-list-alt: "\f022"; +$fa-var-list-ol: "\f0cb"; +$fa-var-list-ul: "\f0ca"; +$fa-var-location-arrow: "\f124"; +$fa-var-lock: "\f023"; +$fa-var-long-arrow-down: "\f175"; +$fa-var-long-arrow-left: "\f177"; +$fa-var-long-arrow-right: "\f178"; +$fa-var-long-arrow-up: "\f176"; +$fa-var-low-vision: "\f2a8"; +$fa-var-magic: "\f0d0"; +$fa-var-magnet: "\f076"; +$fa-var-mail-forward: "\f064"; +$fa-var-mail-reply: "\f112"; +$fa-var-mail-reply-all: "\f122"; +$fa-var-male: "\f183"; +$fa-var-map: "\f279"; +$fa-var-map-marker: "\f041"; +$fa-var-map-o: "\f278"; +$fa-var-map-pin: "\f276"; +$fa-var-map-signs: "\f277"; +$fa-var-mars: "\f222"; +$fa-var-mars-double: "\f227"; +$fa-var-mars-stroke: "\f229"; +$fa-var-mars-stroke-h: "\f22b"; +$fa-var-mars-stroke-v: "\f22a"; +$fa-var-maxcdn: "\f136"; +$fa-var-meanpath: "\f20c"; +$fa-var-medium: "\f23a"; +$fa-var-medkit: "\f0fa"; +$fa-var-meetup: "\f2e0"; +$fa-var-meh-o: "\f11a"; +$fa-var-mercury: "\f223"; +$fa-var-microchip: "\f2db"; +$fa-var-microphone: "\f130"; +$fa-var-microphone-slash: "\f131"; +$fa-var-minus: "\f068"; +$fa-var-minus-circle: "\f056"; +$fa-var-minus-square: "\f146"; +$fa-var-minus-square-o: "\f147"; +$fa-var-mixcloud: "\f289"; +$fa-var-mobile: "\f10b"; +$fa-var-mobile-phone: "\f10b"; +$fa-var-modx: "\f285"; +$fa-var-money: "\f0d6"; +$fa-var-moon-o: "\f186"; +$fa-var-mortar-board: "\f19d"; +$fa-var-motorcycle: "\f21c"; +$fa-var-mouse-pointer: "\f245"; +$fa-var-music: "\f001"; +$fa-var-navicon: "\f0c9"; +$fa-var-neuter: "\f22c"; +$fa-var-newspaper-o: "\f1ea"; +$fa-var-object-group: "\f247"; +$fa-var-object-ungroup: "\f248"; +$fa-var-odnoklassniki: "\f263"; +$fa-var-odnoklassniki-square: "\f264"; +$fa-var-opencart: "\f23d"; +$fa-var-openid: "\f19b"; +$fa-var-opera: "\f26a"; +$fa-var-optin-monster: "\f23c"; +$fa-var-outdent: "\f03b"; +$fa-var-pagelines: "\f18c"; +$fa-var-paint-brush: "\f1fc"; +$fa-var-paper-plane: "\f1d8"; +$fa-var-paper-plane-o: "\f1d9"; +$fa-var-paperclip: "\f0c6"; +$fa-var-paragraph: "\f1dd"; +$fa-var-paste: "\f0ea"; +$fa-var-pause: "\f04c"; +$fa-var-pause-circle: "\f28b"; +$fa-var-pause-circle-o: "\f28c"; +$fa-var-paw: "\f1b0"; +$fa-var-paypal: "\f1ed"; +$fa-var-pencil: "\f040"; +$fa-var-pencil-square: "\f14b"; +$fa-var-pencil-square-o: "\f044"; +$fa-var-percent: "\f295"; +$fa-var-phone: "\f095"; +$fa-var-phone-square: "\f098"; +$fa-var-photo: "\f03e"; +$fa-var-picture-o: "\f03e"; +$fa-var-pie-chart: "\f200"; +$fa-var-pied-piper: "\f2ae"; +$fa-var-pied-piper-alt: "\f1a8"; +$fa-var-pied-piper-pp: "\f1a7"; +$fa-var-pinterest: "\f0d2"; +$fa-var-pinterest-p: "\f231"; +$fa-var-pinterest-square: "\f0d3"; +$fa-var-plane: "\f072"; +$fa-var-play: "\f04b"; +$fa-var-play-circle: "\f144"; +$fa-var-play-circle-o: "\f01d"; +$fa-var-plug: "\f1e6"; +$fa-var-plus: "\f067"; +$fa-var-plus-circle: "\f055"; +$fa-var-plus-square: "\f0fe"; +$fa-var-plus-square-o: "\f196"; +$fa-var-podcast: "\f2ce"; +$fa-var-power-off: "\f011"; +$fa-var-print: "\f02f"; +$fa-var-product-hunt: "\f288"; +$fa-var-puzzle-piece: "\f12e"; +$fa-var-qq: "\f1d6"; +$fa-var-qrcode: "\f029"; +$fa-var-question: "\f128"; +$fa-var-question-circle: "\f059"; +$fa-var-question-circle-o: "\f29c"; +$fa-var-quora: "\f2c4"; +$fa-var-quote-left: "\f10d"; +$fa-var-quote-right: "\f10e"; +$fa-var-ra: "\f1d0"; +$fa-var-random: "\f074"; +$fa-var-ravelry: "\f2d9"; +$fa-var-rebel: "\f1d0"; +$fa-var-recycle: "\f1b8"; +$fa-var-reddit: "\f1a1"; +$fa-var-reddit-alien: "\f281"; +$fa-var-reddit-square: "\f1a2"; +$fa-var-refresh: "\f021"; +$fa-var-registered: "\f25d"; +$fa-var-remove: "\f00d"; +$fa-var-renren: "\f18b"; +$fa-var-reorder: "\f0c9"; +$fa-var-repeat: "\f01e"; +$fa-var-reply: "\f112"; +$fa-var-reply-all: "\f122"; +$fa-var-resistance: "\f1d0"; +$fa-var-retweet: "\f079"; +$fa-var-rmb: "\f157"; +$fa-var-road: "\f018"; +$fa-var-rocket: "\f135"; +$fa-var-rotate-left: "\f0e2"; +$fa-var-rotate-right: "\f01e"; +$fa-var-rouble: "\f158"; +$fa-var-rss: "\f09e"; +$fa-var-rss-square: "\f143"; +$fa-var-rub: "\f158"; +$fa-var-ruble: "\f158"; +$fa-var-rupee: "\f156"; +$fa-var-s15: "\f2cd"; +$fa-var-safari: "\f267"; +$fa-var-save: "\f0c7"; +$fa-var-scissors: "\f0c4"; +$fa-var-scribd: "\f28a"; +$fa-var-search: "\f002"; +$fa-var-search-minus: "\f010"; +$fa-var-search-plus: "\f00e"; +$fa-var-sellsy: "\f213"; +$fa-var-send: "\f1d8"; +$fa-var-send-o: "\f1d9"; +$fa-var-server: "\f233"; +$fa-var-share: "\f064"; +$fa-var-share-alt: "\f1e0"; +$fa-var-share-alt-square: "\f1e1"; +$fa-var-share-square: "\f14d"; +$fa-var-share-square-o: "\f045"; +$fa-var-shekel: "\f20b"; +$fa-var-sheqel: "\f20b"; +$fa-var-shield: "\f132"; +$fa-var-ship: "\f21a"; +$fa-var-shirtsinbulk: "\f214"; +$fa-var-shopping-bag: "\f290"; +$fa-var-shopping-basket: "\f291"; +$fa-var-shopping-cart: "\f07a"; +$fa-var-shower: "\f2cc"; +$fa-var-sign-in: "\f090"; +$fa-var-sign-language: "\f2a7"; +$fa-var-sign-out: "\f08b"; +$fa-var-signal: "\f012"; +$fa-var-signing: "\f2a7"; +$fa-var-simplybuilt: "\f215"; +$fa-var-sitemap: "\f0e8"; +$fa-var-skyatlas: "\f216"; +$fa-var-skype: "\f17e"; +$fa-var-slack: "\f198"; +$fa-var-sliders: "\f1de"; +$fa-var-slideshare: "\f1e7"; +$fa-var-smile-o: "\f118"; +$fa-var-snapchat: "\f2ab"; +$fa-var-snapchat-ghost: "\f2ac"; +$fa-var-snapchat-square: "\f2ad"; +$fa-var-snowflake-o: "\f2dc"; +$fa-var-soccer-ball-o: "\f1e3"; +$fa-var-sort: "\f0dc"; +$fa-var-sort-alpha-asc: "\f15d"; +$fa-var-sort-alpha-desc: "\f15e"; +$fa-var-sort-amount-asc: "\f160"; +$fa-var-sort-amount-desc: "\f161"; +$fa-var-sort-asc: "\f0de"; +$fa-var-sort-desc: "\f0dd"; +$fa-var-sort-down: "\f0dd"; +$fa-var-sort-numeric-asc: "\f162"; +$fa-var-sort-numeric-desc: "\f163"; +$fa-var-sort-up: "\f0de"; +$fa-var-soundcloud: "\f1be"; +$fa-var-space-shuttle: "\f197"; +$fa-var-spinner: "\f110"; +$fa-var-spoon: "\f1b1"; +$fa-var-spotify: "\f1bc"; +$fa-var-square: "\f0c8"; +$fa-var-square-o: "\f096"; +$fa-var-stack-exchange: "\f18d"; +$fa-var-stack-overflow: "\f16c"; +$fa-var-star: "\f005"; +$fa-var-star-half: "\f089"; +$fa-var-star-half-empty: "\f123"; +$fa-var-star-half-full: "\f123"; +$fa-var-star-half-o: "\f123"; +$fa-var-star-o: "\f006"; +$fa-var-steam: "\f1b6"; +$fa-var-steam-square: "\f1b7"; +$fa-var-step-backward: "\f048"; +$fa-var-step-forward: "\f051"; +$fa-var-stethoscope: "\f0f1"; +$fa-var-sticky-note: "\f249"; +$fa-var-sticky-note-o: "\f24a"; +$fa-var-stop: "\f04d"; +$fa-var-stop-circle: "\f28d"; +$fa-var-stop-circle-o: "\f28e"; +$fa-var-street-view: "\f21d"; +$fa-var-strikethrough: "\f0cc"; +$fa-var-stumbleupon: "\f1a4"; +$fa-var-stumbleupon-circle: "\f1a3"; +$fa-var-subscript: "\f12c"; +$fa-var-subway: "\f239"; +$fa-var-suitcase: "\f0f2"; +$fa-var-sun-o: "\f185"; +$fa-var-superpowers: "\f2dd"; +$fa-var-superscript: "\f12b"; +$fa-var-support: "\f1cd"; +$fa-var-table: "\f0ce"; +$fa-var-tablet: "\f10a"; +$fa-var-tachometer: "\f0e4"; +$fa-var-tag: "\f02b"; +$fa-var-tags: "\f02c"; +$fa-var-tasks: "\f0ae"; +$fa-var-taxi: "\f1ba"; +$fa-var-telegram: "\f2c6"; +$fa-var-television: "\f26c"; +$fa-var-tencent-weibo: "\f1d5"; +$fa-var-terminal: "\f120"; +$fa-var-text-height: "\f034"; +$fa-var-text-width: "\f035"; +$fa-var-th: "\f00a"; +$fa-var-th-large: "\f009"; +$fa-var-th-list: "\f00b"; +$fa-var-themeisle: "\f2b2"; +$fa-var-thermometer: "\f2c7"; +$fa-var-thermometer-0: "\f2cb"; +$fa-var-thermometer-1: "\f2ca"; +$fa-var-thermometer-2: "\f2c9"; +$fa-var-thermometer-3: "\f2c8"; +$fa-var-thermometer-4: "\f2c7"; +$fa-var-thermometer-empty: "\f2cb"; +$fa-var-thermometer-full: "\f2c7"; +$fa-var-thermometer-half: "\f2c9"; +$fa-var-thermometer-quarter: "\f2ca"; +$fa-var-thermometer-three-quarters: "\f2c8"; +$fa-var-thumb-tack: "\f08d"; +$fa-var-thumbs-down: "\f165"; +$fa-var-thumbs-o-down: "\f088"; +$fa-var-thumbs-o-up: "\f087"; +$fa-var-thumbs-up: "\f164"; +$fa-var-ticket: "\f145"; +$fa-var-times: "\f00d"; +$fa-var-times-circle: "\f057"; +$fa-var-times-circle-o: "\f05c"; +$fa-var-times-rectangle: "\f2d3"; +$fa-var-times-rectangle-o: "\f2d4"; +$fa-var-tint: "\f043"; +$fa-var-toggle-down: "\f150"; +$fa-var-toggle-left: "\f191"; +$fa-var-toggle-off: "\f204"; +$fa-var-toggle-on: "\f205"; +$fa-var-toggle-right: "\f152"; +$fa-var-toggle-up: "\f151"; +$fa-var-trademark: "\f25c"; +$fa-var-train: "\f238"; +$fa-var-transgender: "\f224"; +$fa-var-transgender-alt: "\f225"; +$fa-var-trash: "\f1f8"; +$fa-var-trash-o: "\f014"; +$fa-var-tree: "\f1bb"; +$fa-var-trello: "\f181"; +$fa-var-tripadvisor: "\f262"; +$fa-var-trophy: "\f091"; +$fa-var-truck: "\f0d1"; +$fa-var-try: "\f195"; +$fa-var-tty: "\f1e4"; +$fa-var-tumblr: "\f173"; +$fa-var-tumblr-square: "\f174"; +$fa-var-turkish-lira: "\f195"; +$fa-var-tv: "\f26c"; +$fa-var-twitch: "\f1e8"; +$fa-var-twitter: "\f099"; +$fa-var-twitter-square: "\f081"; +$fa-var-umbrella: "\f0e9"; +$fa-var-underline: "\f0cd"; +$fa-var-undo: "\f0e2"; +$fa-var-universal-access: "\f29a"; +$fa-var-university: "\f19c"; +$fa-var-unlink: "\f127"; +$fa-var-unlock: "\f09c"; +$fa-var-unlock-alt: "\f13e"; +$fa-var-unsorted: "\f0dc"; +$fa-var-upload: "\f093"; +$fa-var-usb: "\f287"; +$fa-var-usd: "\f155"; +$fa-var-user: "\f007"; +$fa-var-user-circle: "\f2bd"; +$fa-var-user-circle-o: "\f2be"; +$fa-var-user-md: "\f0f0"; +$fa-var-user-o: "\f2c0"; +$fa-var-user-plus: "\f234"; +$fa-var-user-secret: "\f21b"; +$fa-var-user-times: "\f235"; +$fa-var-users: "\f0c0"; +$fa-var-vcard: "\f2bb"; +$fa-var-vcard-o: "\f2bc"; +$fa-var-venus: "\f221"; +$fa-var-venus-double: "\f226"; +$fa-var-venus-mars: "\f228"; +$fa-var-viacoin: "\f237"; +$fa-var-viadeo: "\f2a9"; +$fa-var-viadeo-square: "\f2aa"; +$fa-var-video-camera: "\f03d"; +$fa-var-vimeo: "\f27d"; +$fa-var-vimeo-square: "\f194"; +$fa-var-vine: "\f1ca"; +$fa-var-vk: "\f189"; +$fa-var-volume-control-phone: "\f2a0"; +$fa-var-volume-down: "\f027"; +$fa-var-volume-off: "\f026"; +$fa-var-volume-up: "\f028"; +$fa-var-warning: "\f071"; +$fa-var-wechat: "\f1d7"; +$fa-var-weibo: "\f18a"; +$fa-var-weixin: "\f1d7"; +$fa-var-whatsapp: "\f232"; +$fa-var-wheelchair: "\f193"; +$fa-var-wheelchair-alt: "\f29b"; +$fa-var-wifi: "\f1eb"; +$fa-var-wikipedia-w: "\f266"; +$fa-var-window-close: "\f2d3"; +$fa-var-window-close-o: "\f2d4"; +$fa-var-window-maximize: "\f2d0"; +$fa-var-window-minimize: "\f2d1"; +$fa-var-window-restore: "\f2d2"; +$fa-var-windows: "\f17a"; +$fa-var-won: "\f159"; +$fa-var-wordpress: "\f19a"; +$fa-var-wpbeginner: "\f297"; +$fa-var-wpexplorer: "\f2de"; +$fa-var-wpforms: "\f298"; +$fa-var-wrench: "\f0ad"; +$fa-var-xing: "\f168"; +$fa-var-xing-square: "\f169"; +$fa-var-y-combinator: "\f23b"; +$fa-var-y-combinator-square: "\f1d4"; +$fa-var-yahoo: "\f19e"; +$fa-var-yc: "\f23b"; +$fa-var-yc-square: "\f1d4"; +$fa-var-yelp: "\f1e9"; +$fa-var-yen: "\f157"; +$fa-var-yoast: "\f2b1"; +$fa-var-youtube: "\f167"; +$fa-var-youtube-play: "\f16a"; +$fa-var-youtube-square: "\f166"; + diff --git a/public/installer/css/scss/font-awesome/font-awesome.scss b/public/installer/css/scss/font-awesome/font-awesome.scss new file mode 100755 index 000000000..f1c83aaa5 --- /dev/null +++ b/public/installer/css/scss/font-awesome/font-awesome.scss @@ -0,0 +1,18 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@import "variables"; +@import "mixins"; +@import "path"; +@import "core"; +@import "larger"; +@import "fixed-width"; +@import "list"; +@import "bordered-pulled"; +@import "animated"; +@import "rotated-flipped"; +@import "stacked"; +@import "icons"; +@import "screen-reader"; diff --git a/public/installer/css/scss/style.scss b/public/installer/css/scss/style.scss new file mode 100755 index 000000000..7fee72bd2 --- /dev/null +++ b/public/installer/css/scss/style.scss @@ -0,0 +1,1348 @@ +// Variables +@import "variables"; + +// Font Awesome +@import "font-awesome/font-awesome"; + +//@extend-elements +//original selectors +//sub, sup +%extend_1 { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +//original selectors +//button, input, optgroup, select, textarea +%extend_2 { + color: inherit; + font: inherit; + margin: 0; +} + +@import url($url_0); +html { + font-family: $font_0; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + font-family: $font_2, $font_3, $font_4, $font_5, $font_0; + font-weight: 300; + color: $color_3; + font-size: 12px; + line-height: 1.75em; + input[type=button] { + -webkit-appearance: button; + cursor: pointer; + } + input[disabled] { + cursor: default; + } +} +body { + margin: 0; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -moz-font-feature-settings: "liga" on; +} +article { + display: block; +} +aside { + display: block; +} +details { + display: block; +} +figcaption { + display: block; +} +figure { + display: block; + margin: 1em 40px; +} +footer { + display: block; +} +header { + display: block; +} +hgroup { + display: block; +} +main { + display: block; +} +menu { + display: block; +} +nav { + display: block; +} +section { + display: block; +} +summary { + display: block; +} +audio { + display: inline-block; + vertical-align: baseline; + &:not([controls]) { + display: none; + height: 0; + } +} +canvas { + display: inline-block; + vertical-align: baseline; +} +progress { + display: inline-block; + vertical-align: baseline; +} +video { + display: inline-block; + vertical-align: baseline; +} +[hidden] { + display: none; +} +template { + display: none; +} +a { + background-color: transparent; + text-decoration: none; + color: $color_5; + //Instead of the line below you could use @include transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: all .2s; + margin: 0; + padding: 0; + cursor: pointer; + &:active { + outline: 0; + } + &:hover { + outline: 0; + color: $color_6; + } +} +abbr[title] { + border-bottom: 1px dotted; +} +b { + font-weight: 700; + margin: 0; + padding: 0; +} +strong { + font-weight: 700; + margin: 0; + padding: 0; +} +dfn { + font-style: italic; + margin: 0; + padding: 0; +} +h1 { + font-size: 2em; + margin: .67em 0; + margin-top: .942400822452556em; + line-height: 1.130880986943067em; + margin-bottom: .188480164490511em; + margin: 0; + padding: 0; + font-family: $font_2, $font_3, $font_4, $font_5, $font_0; + font-weight: 500; + color: $color_4; + clear: both; +} +mark { + background: $color_0; + color: $color_1; +} +small { + font-size: 80%; + margin: 0; + padding: 0; + line-height: 0; +} +sub { + @extend %extend_1; + bottom: -.25em; + margin: 0; + padding: 0; + line-height: 0; +} +sup { + @extend %extend_1; + top: -.5em; + margin: 0; + padding: 0; + line-height: 0; + a .fa { + font-size: 1em; + } +} +img { + border: 0; + margin: 0; + padding: 0; +} +hr { + //Instead of the line below you could use @include box-sizing($bs) + box-sizing: content-box; + height: 0; +} +pre { + overflow: auto; + padding: .875em; + margin-bottom: 1.75em; + background: $color_18; + line-height: 1; + color: $color_30; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px; + font-size: 10px; + font-family: $font_1; + font-size: 1em; + margin: 0; + padding: 0; + margin-bottom: 1.75em; + code { + padding: 0; + } +} +code { + font-family: $font_1; + font-size: 1em; + margin: 0; + padding: 0; + font-family: $font_6, $font_7, $font_8, $font_9, $font_1; + padding: .0875em .2625em; + line-height: 0; +} +kbd { + font-family: $font_1; + font-size: 1em; + margin: 0; + padding: 0; +} +samp { + font-family: $font_1; + font-size: 1em; + margin: 0; + padding: 0; +} +button { + @extend %extend_2; + overflow: visible; + text-transform: none; + -webkit-appearance: button; + cursor: pointer; + display: block; + cursor: pointer; + font-size: 12px; + padding: .4375em 1.75em; + margin-bottom: 1.18125em; +} +input { + @extend %extend_2; + line-height: normal; + &:focus { + background: $color_30; + } +} +optgroup { + @extend %extend_2; + font-weight: 700; +} +select { + @extend %extend_2; + text-transform: none; + outline: none; + border: 1px solid $color_27; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; + width: 100%; + height: 35px; +} +textarea { + @extend %extend_2; + overflow: auto; + display: block; + max-width: 100%; + padding: .4375em; + font-size: 12px; + margin-bottom: 1.18125em; + outline: none; + border: 1px solid $color_27; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; +} +input[type=reset] { + -webkit-appearance: button; + cursor: pointer; +} +input[type=submit] { + -webkit-appearance: button; + cursor: pointer; + display: block; + cursor: pointer; + font-size: 12px; + padding: .4375em 1.75em; + margin-bottom: 1.18125em; +} +button[disabled] { + cursor: default; +} +button::-moz-focus-inner { + border: 0; + padding: 0; +} +input::-moz-focus-inner { + border: 0; + padding: 0; +} +input[type=checkbox] { + //Instead of the line below you could use @include box-sizing($bs) + box-sizing: border-box; + padding: 0; +} +input[type=radio] { + //Instead of the line below you could use @include box-sizing($bs) + box-sizing: border-box; + padding: 0; +} +input[type=number]::-webkit-inner-spin-button { + height: auto; +} +input[type=number]::-webkit-outer-spin-button { + height: auto; +} +input[type=search] { + -webkit-appearance: textfield; + //Instead of the line below you could use @include box-sizing($bs) + box-sizing: content-box; +} +input[type=search]::-webkit-search-cancel-button { + -webkit-appearance: none; +} +input[type=search]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + border: 1px solid $color_2; + margin: 0 2px; + padding: .35em .625em .75em; + padding: .875em 1.75em 1.75em; + border-width: 1px; + border-style: solid; + max-width: 100%; + margin-bottom: 1.8375em; + margin: 0; + padding: 0; + button { + margin-bottom: 0; + } + input[type=submit] { + margin-bottom: 0; + } +} +legend { + border: 0; + padding: 0; + color: $color_4; + font-weight: 700; + margin: 0; + padding: 0; +} +table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + margin-bottom: 2.1875em; + margin: 0; + padding: 0; + margin-bottom: 1.75em; +} +td { + padding: 0; + margin: 0; + padding: 0; + padding: .21875em .875em; +} +th { + padding: 0; + margin: 0; + padding: 0; + text-align: left; + color: $color_4; + padding: .21875em .875em; +} +@media(min-width:600px) { + html { + font-size: 12px; + } + h1 { + font-size: calc(27.85438995234061px +18.56959 *((100vw - 600px) / 540)); + } + h2 { + font-size: calc(23.53700340860508px +15.69134 *((100vw - 600px) / 540)); + } + h3 { + font-size: calc(19.888804974891777px +13.2592 *((100vw - 600px) / 540)); + } + h4 { + font-size: calc(16.806071548796314px +11.20405 *((100vw - 600px) / 540)); + } + h5 { + font-size: calc(14.201156945318074px +9.46744 *((100vw - 600px) / 540)); + } + h6 { + font-size: calc(12px +8 *((100vw - 600px) / 540)); + } +} +abbr { + margin: 0; + padding: 0; + border-bottom: 1px dotted currentColor; + cursor: help; +} +acronym { + margin: 0; + padding: 0; + border-bottom: 1px dotted currentColor; + cursor: help; +} +address { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + font-style: normal; +} +big { + margin: 0; + padding: 0; + line-height: 0; +} +blockquote { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + font-style: italic; + cite { + display: block; + font-style: normal; + } +} +caption { + margin: 0; + padding: 0; +} +center { + margin: 0; + padding: 0; +} +cite { + margin: 0; + padding: 0; +} +dd { + margin: 0; + padding: 0; +} +del { + margin: 0; + padding: 0; +} +dl { + margin: 0; + padding: 0; + margin-bottom: 1.75em; +} +dt { + margin: 0; + padding: 0; + color: $color_4; + font-weight: 700; +} +em { + margin: 0; + padding: 0; +} +form { + margin: 0; + padding: 0; +} +h2 { + margin: 0; + padding: 0; + font-family: $font_2, $font_3, $font_4, $font_5, $font_0; + font-weight: 500; + color: $color_4; + clear: both; + font-size: 23.53700340860508px; + margin-top: 1.115265165420465em; + line-height: 1.338318198504558em; + margin-bottom: .251483121980101em; +} +h3 { + margin: 0; + padding: 0; + font-family: $font_2, $font_3, $font_4, $font_5, $font_0; + font-weight: 500; + color: $color_4; + clear: both; + font-size: 19.888804974891777px; + margin-top: 1.319837970815179em; + line-height: 1.583805564978215em; + margin-bottom: .303784103173448em; +} +h4 { + margin: 0; + padding: 0; + font-family: $font_2, $font_3, $font_4, $font_5, $font_0; + font-weight: 500; + color: $color_4; + clear: both; + font-size: 16.806071548796314px; + margin-top: 1.561935513828041em; + line-height: 1.87432261659365em; + margin-bottom: .368150361036632em; +} +h5 { + margin: 0; + padding: 0; + font-family: $font_2, $font_3, $font_4, $font_5, $font_0; + font-weight: 500; + color: $color_4; + clear: both; + font-size: 14.201156945318074px; + margin-top: 1.84844094752817em; + line-height: 2.218129137033805em; + margin-bottom: .369688189505634em; +} +h6 { + margin: 0; + padding: 0; + font-family: $font_2, $font_3, $font_4, $font_5, $font_0; + font-weight: 500; + color: $color_4; + clear: both; + font-size: 12px; + margin-top: 2.1875em; + line-height: 2.625em; + margin-bottom: .619791666666667em; +} +i { + margin: 0; + padding: 0; +} +ins { + margin: 0; + padding: 0; +} +label { + margin: 0; + padding: 0; + display: block; + padding-bottom: .21875em; + margin-bottom: -.21875em; + cursor: pointer; +} +li { + margin: 0; + padding: 0; +} +ol { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + padding-left: 1.4em; +} +p { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + margin: 0 0 1rem 0; +} +q { + margin: 0; + padding: 0; +} +s { + margin: 0; + padding: 0; +} +strike { + margin: 0; + padding: 0; +} +tbody { + margin: 0; + padding: 0; +} +tfoot { + margin: 0; + padding: 0; +} +thead { + margin: 0; + padding: 0; +} +tr { + margin: 0; + padding: 0; +} +tt { + margin: 0; + padding: 0; +} +u { + margin: 0; + padding: 0; +} +ul { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + padding-left: 1.1em; +} +var { + margin: 0; + padding: 0; +} +@media(min-width:1140px) { + h1 { + font-size: 46.423983253901014px; + margin-top: .942400822452556em; + line-height: 1.130880986943067em; + margin-bottom: .188480164490511em; + } + h2 { + font-size: 39.228339014341806px; + margin-top: 1.115265165420465em; + line-height: 1.338318198504558em; + margin-bottom: .240111086421698em; + } + h3 { + font-size: 33.14800829148629px; + margin-top: 1.319837970815179em; + line-height: 1.583805564978215em; + margin-bottom: .287857499569283em; + } + h4 { + font-size: 28.01011924799386px; + margin-top: 1.561935513828041em; + line-height: 1.87432261659365em; + margin-bottom: .345845057728222em; + } + h5 { + font-size: 23.668594908863454px; + margin-top: 1.84844094752817em; + line-height: 2.218129137033805em; + margin-bottom: .369688189505634em; + } + h6 { + font-size: 20px; + margin-top: 2.1875em; + line-height: 2.625em; + margin-bottom: .473958333333333em; + } + fieldset { + margin-bottom: 2.078125em; + } + input[type=email] { + font-size: 20px; + margin-bottom: .5140625em; + } + input[type=password] { + font-size: 20px; + margin-bottom: .5140625em; + } + input[type=text] { + font-size: 20px; + margin-bottom: .5140625em; + } + textarea { + font-size: 20px; + margin-bottom: .5140625em; + } + button { + font-size: 20px; + margin-bottom: 1.3125em; + } + input[type=submit] { + font-size: 20px; + margin-bottom: 1.3125em; + } + table { + margin-bottom: 2.05625em; + } + th { + padding: .4375em .875em; + } + td { + padding: .4375em .875em; + } +} +input[type=email] { + display: block; + max-width: 100%; + padding: .4375em; + font-size: 12px; + margin-bottom: 1.18125em; +} +input[type=password] { + display: block; + max-width: 100%; + padding: .4375em; + font-size: 12px; + margin-bottom: 1.18125em; +} +input[type=text] { + display: block; + max-width: 100%; + padding: .4375em; + font-size: 12px; + margin-bottom: 1.18125em; +} +.master { + background-image: url($url_1); + background-size: cover; + background-position: top; + min-height: 100vh; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; +} +.box { + width: 450px; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 0 0 3px 3px; + overflow: hidden; + //Instead of the line below you could use @include box-sizing($bs) + box-sizing: border-box; + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 10px 10px $color_7, 0 6px 3px $color_8; +} +.header { + background-color: $color_9; + padding: 30px 30px 40px; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px 3px 0 0; + text-align: center; +} +.header__step { + font-weight: 300; + text-transform: uppercase; + font-size: 14px; + letter-spacing: 1.1px; + margin: 0 0 10px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + //Instead of the line below you could use @include user-select($select) + user-select: none; + color: $color_10; +} +.header__title { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + //Instead of the line below you could use @include user-select($select) + user-select: none; + color: $color_10; + font-weight: 400; + font-size: 20px; + margin: 0 0 15px; +} +.step { + position: relative; + z-index: 1; + padding-left: 0; + list-style: none; + margin-bottom: 0; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-direction: row-reverse; + -ms-flex-direction: row-reverse; + flex-direction: row-reverse; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + margin-top: -20px; +} +.step__divider { + background-color: $color_11; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + //Instead of the line below you could use @include user-select($select) + user-select: none; + width: 60px; + height: 3px; + &:first-child { + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + } + &:last-child { + -webkit-flex: 1 0 auto; + -ms-flex: 1 0 auto; + flex: 1 0 auto; + } +} +.step__icon { + background-color: $color_11; + font-style: normal; + width: 40px; + height: 40px; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 50%; + color: $color_10; + &.welcome:before { + content: '\f144'; + font-family: $font_10; + } + &.requirements:before { + content: '\f127'; + font-family: $font_10; + } + &.permissions:before { + content: '\f296'; + font-family: $font_10; + } + &.database:before { + content: '\f454'; + font-family: $font_10; + } + &.update:before { + content: '\f2bf'; + font-family: $font_10; + } +} +.main { + margin-top: -20px; + background-color: $color_10; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 0 0 3px 3px; + padding: 40px 40px 30px; +} +.buttons { + text-align: center; + .button { + margin: .5em; + } +} +.buttons--right { + text-align: right; +} +.button { + display: inline-block; + background-color: $color_12; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 2px; + padding: 7px 20px; + color: $color_10; + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 1px 1.5px $color_14, 0 1px 1px $color_15; + text-decoration: none; + outline: none; + border: none; + //Instead of the line below you could use @include transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: box-shadow .2s ease, background-color .2s ease; + cursor: pointer; + &:hover { + color: $color_10; + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 10px 10px $color_7, 0 6px 3px $color_8; + background-color: $color_16; + } +} +.button--light { + padding: 3px 16px; + font-size: 16px; + border-top: 1px solid $color_17; + color: $color_18; + background: $color_10; + &:hover { + color: $color_18; + background: $color_10; + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 3px 3px $color_19, 0 3px 3px $color_8; + } +} +.list { + padding-left: 0; + list-style: none; + margin-bottom: 0; + margin: 20px 0 35px; + border: 1px solid $color_14; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 2px; + .list__item.list__title { + background: $color_14; + &.success { + span { + color: $color_31; + } + .fa:before { + color: $color_31; + } + } + &.error { + span { + color: $color_24; + } + .fa:before { + color: $color_24; + } + } + } +} +.list__item { + position: relative; + overflow: hidden; + padding: 7px 20px; + border-bottom: 1px solid $color_14; + &:first-letter { + text-transform: uppercase; + } + &:last-child { + border-bottom: none; + } + .fa.row-icon:before { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + padding: 7px 20px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + } + &.success .fa:before { + color: $color_20; + } + &.error .fa:before { + color: $color_21; + } +} +.list__item--permissions { + &:before { + content: '' !important; + } + span { + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-justify-content: center; + -ms-flex-pack: center; + justify-content: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + padding: 7px 20px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + background-color: $color_22; + font-weight: 700; + font-size: 16px; + &:before { + margin-right: 7px; + font-weight: 400; + } + } + &.success i:before { + color: $color_20; + vertical-align: 1px; + } + &.error i:before { + color: $color_21; + vertical-align: 1px; + } +} +.textarea { + //Instead of the line below you could use @include box-sizing($bs) + box-sizing: border-box; + width: 100%; + font-size: 14px; + line-height: 25px; + height: 150px; + outline: none; + border: 1px solid $color_23; + ~ .button { + margin-bottom: 35px; + } +} +.text-center { + text-align: center; +} +.form-control { + height: 14px; + width: 100%; +} +.has-error { + color: $color_24; + input { + color: $color_25; + border: 1px solid $color_26; + } +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +input[type='text'] { + outline: none; + border: 1px solid $color_27; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; +} +input[type='password'] { + outline: none; + border: 1px solid $color_27; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; +} +input[type='url'] { + outline: none; + border: 1px solid $color_27; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; +} +input[type='number'] { + outline: none; + border: 1px solid $color_27; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; +} +.tabs { + padding: 0; + .tab-input { + display: none; + &:checked + .tab-label { + background-color: $color_10; + color: $color_29; + } + } + .tab-label { + color: $color_28; + cursor: pointer; + float: left; + padding: 1em; + text-align: center; + -webkit-transition: all 0.1s ease-in-out; + -moz-transition: all 0.1s ease-in-out; + -o-transition: all 0.1s ease-in-out; + //Instead of the line below you could use @include transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: all 0.1s ease-in-out; + &:hover { + color: $color_29; + } + } + .tabs-wrap { + clear: both; + } + .tab { + display: none; + > *:last-child { + margin-bottom: 0; + } + } +} +.float-left { + float: left; +} +.float-right { + float: right; +} +.buttons-container { + min-height: 37px; + margin: 1em 0 0; +} +.block { + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: 0 3px 1px $color_32; + input[type='radio'] { + width: 100%; + display: none; + + label { + background: $color_33; + color: $color_10; + padding-top: 5px; + -webkit-transition: all 0.1s ease-in-out; + -moz-transition: all 0.1s ease-in-out; + -o-transition: all 0.1s ease-in-out; + //Instead of the line below you could use @include transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: all 0.1s ease-in-out; + &:hover { + background: $color_34; + color: $color_10; + padding-top: 5px; + } + } + &:checked { + + label { + background-color: $color_35; + } + ~ .info { + height: 200px; + overflow-y: auto; + -webkit-transition: all 0.3s ease-in-out; + -moz-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + //Instead of the line below you could use @include transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: all 0.3s ease-in-out; + } + } + ~ .info > div { + padding-top: 15px; + } + } + label { + width: 450px; + max-width: 100%; + cursor: pointer; + } + span { + font-family: $font_5; + font-weight: 700; + display: block; + padding: 10px 12px 12px 15px; + margin: 0; + cursor: pointer; + } +} +.info { + background: $color_10; + color: $color_18; + width: 100%; + height: 0; + line-height: 2; + padding-left: 15px; + padding-right: 15px; + display: block; + overflow: hidden; + //Instead of the line below you could use @include box-sizing($bs) + box-sizing: border-box; + //Instead of the line below you could use @include transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: .3s ease-out; +} +::selection { + background: $color_18; + color: $color_10; +} +::-webkit-scrollbar { + width: 12px; +} +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px $color_36; + -webkit-border-radius: 0; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 0; +} +::-webkit-scrollbar-thumb { + -webkit-border-radius: 0; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 0; + background: $color_37; + -webkit-box-shadow: inset 0 0 6px $color_38; +} +.margin-bottom-1 { + margin-bottom: 1em; +} +.margin-bottom-2 { + margin-bottom: 1em; +} +.margin-top-1 { + margin-top: 1em; +} +.margin-top-2 { + margin-top: 1em; +} +.alert { + margin: 0 0 10px; + font-size: 1.1em; + background-color: $color_22; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px; + padding: 10px; + position: relative; + &.alert-danger { + background: $color_24; + color: $color_39; + padding: 10px 20px 15px; + h4 { + color: $color_39; + margin: 0; + } + ul { + margin: 0; + } + } + .close { + width: 25px; + height: 25px; + padding: 0; + margin: 0; + //Instead of the line below you could use @include box-shadow($shadow-1, $shadow-2, $shadow-3, $shadow-4, $shadow-5, $shadow-6, $shadow-7, $shadow-8, $shadow-9, $shadow-10) + box-shadow: none; + border: 2px solid $color_26; + outline: none; + float: right; + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 50%; + position: absolute; + right: 0; + top: 0; + background-color: transparent; + cursor: pointer; + -webkit-transition: all 0.1s ease-in-out; + -moz-transition: all 0.1s ease-in-out; + -o-transition: all 0.1s ease-in-out; + //Instead of the line below you could use @include transition($transition-1, $transition-2, $transition-3, $transition-4, $transition-5, $transition-6, $transition-7, $transition-8, $transition-9, $transition-10) + transition: all 0.1s ease-in-out; + &:hover { + background-color: $color_39; + color: $color_24; + } + } +} +svg:not(:root) { + overflow: hidden; +} +.step__item.active .step__icon, +.step__item.active~.step__divider, +.step__item.active~.step__item .step__icon { + background-color: $color_12; + -webkit-transition: all 0.1s ease-in-out; + -moz-transition: all 0.1s ease-in-out; + -o-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; +} +.step__item.active .step__icon:hover, +.step__item.active~.step__item .step__icon:hover { + background-color: $color_13; +} +.form-group.has-error { + select { + border-color: $color_24; + } + textarea { + border-color: $color_24; + } + input[type='text'] { + border-color: $color_24; + } + input[type='password'] { + border-color: $color_24; + } + input[type='url'] { + border-color: $color_24; + } + input[type='number'] { + border-color: $color_24; + } + .error-block { + margin: -12px 0 0; + display: block; + width: 100%; + font-size: .9em; + color: $color_24; + font-weight: 500; + } +} +.tabs-full .tab-label { + display: table-cell; + float: none; + width: 1%; +} +#tab1:checked ~ .tabs-wrap #tab1content { + display: block; +} +#tab2:checked ~ .tabs-wrap #tab2content { + display: block; +} +#tab3:checked ~ .tabs-wrap #tab3content { + display: block; +} +#tab4:checked ~ .tabs-wrap #tab4content { + display: block; +} +#tab5:checked ~ .tabs-wrap #tab5content { + display: block; +} +.github img { + position: absolute; + top: 0; + right: 0; + border: 0; +} +#tab3content .block { + &:first-child { + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 3px 3px 0 0; + } + &:last-child { + //Instead of the line below you could use @include border-radius($radius, $vertical-radius) + border-radius: 0 0 3px 3px; + } +} \ No newline at end of file diff --git a/public/installer/css/style.css b/public/installer/css/style.css new file mode 100755 index 000000000..47a8be865 --- /dev/null +++ b/public/installer/css/style.css @@ -0,0 +1,3539 @@ +@charset "UTF-8"; +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ +/* FONT PATH + * -------------------------- */ +@import url("https://fonts.googleapis.com/css?family=Roboto:400,300,500,700,900"); +@font-face { + font-family: 'FontAwesome'; + src: url("../fonts/fontawesome-webfont.eot?v=4.7.0"); + src: url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"), url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"), url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"), url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"), url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg"); + font-weight: normal; + font-style: normal; } +.fa { + display: inline-block; + font: normal normal normal 14px/1 FontAwesome; + font-size: inherit; + text-rendering: auto; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.3333333333em; + line-height: 0.75em; + vertical-align: -15%; } + +.fa-2x { + font-size: 2em; } + +.fa-3x { + font-size: 3em; } + +.fa-4x { + font-size: 4em; } + +.fa-5x { + font-size: 5em; } + +.fa-fw { + width: 1.2857142857em; + text-align: center; } + +.fa-ul { + padding-left: 0; + margin-left: 2.1428571429em; + list-style-type: none; } + .fa-ul > li { + position: relative; } + +.fa-li { + position: absolute; + left: -2.1428571429em; + width: 2.1428571429em; + top: 0.1428571429em; + text-align: center; } + .fa-li.fa-lg { + left: -1.8571428571em; } + +.fa-border { + padding: .2em .25em .15em; + border: solid 0.08em #eee; + border-radius: .1em; } + +.fa-pull-left { + float: left; } + +.fa-pull-right { + float: right; } + +.fa.fa-pull-left { + margin-right: .3em; } +.fa.fa-pull-right { + margin-left: .3em; } + +/* Deprecated as of 4.4.0 */ +.pull-right { + float: right; } + +.pull-left { + float: left; } + +.fa.pull-left { + margin-right: .3em; } +.fa.pull-right { + margin-left: .3em; } + +.fa-spin { + -webkit-animation: fa-spin 2s infinite linear; + animation: fa-spin 2s infinite linear; } + +.fa-pulse { + -webkit-animation: fa-spin 1s infinite steps(8); + animation: fa-spin 1s infinite steps(8); } + +@-webkit-keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); } } +@keyframes fa-spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); } } +.fa-rotate-90 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); } + +.fa-rotate-180 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + +.fa-rotate-270 { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; + -webkit-transform: rotate(270deg); + transform: rotate(270deg); } + +.fa-flip-horizontal { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; + -webkit-transform: scale(-1, 1); + transform: scale(-1, 1); } + +.fa-flip-vertical { + -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; + -webkit-transform: scale(1, -1); + transform: scale(1, -1); } + +:root .fa-rotate-90, +:root .fa-rotate-180, +:root .fa-rotate-270, +:root .fa-flip-horizontal, +:root .fa-flip-vertical { + -webkit-filter: none; + filter: none; } + +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; } + +.fa-stack-1x, .fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; } + +.fa-stack-1x { + line-height: inherit; } + +.fa-stack-2x { + font-size: 2em; } + +.fa-inverse { + color: #fff; } + +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: ""; } + +.fa-music:before { + content: ""; } + +.fa-search:before { + content: ""; } + +.fa-envelope-o:before { + content: ""; } + +.fa-heart:before { + content: ""; } + +.fa-star:before { + content: ""; } + +.fa-star-o:before { + content: ""; } + +.fa-user:before { + content: ""; } + +.fa-film:before { + content: ""; } + +.fa-th-large:before { + content: ""; } + +.fa-th:before { + content: ""; } + +.fa-th-list:before { + content: ""; } + +.fa-check:before { + content: ""; } + +.fa-remove:before, +.fa-close:before, +.fa-times:before { + content: ""; } + +.fa-search-plus:before { + content: ""; } + +.fa-search-minus:before { + content: ""; } + +.fa-power-off:before { + content: ""; } + +.fa-signal:before { + content: ""; } + +.fa-gear:before, +.fa-cog:before { + content: ""; } + +.fa-trash-o:before { + content: ""; } + +.fa-home:before { + content: ""; } + +.fa-file-o:before { + content: ""; } + +.fa-clock-o:before { + content: ""; } + +.fa-road:before { + content: ""; } + +.fa-download:before { + content: ""; } + +.fa-arrow-circle-o-down:before { + content: ""; } + +.fa-arrow-circle-o-up:before { + content: ""; } + +.fa-inbox:before { + content: ""; } + +.fa-play-circle-o:before { + content: ""; } + +.fa-rotate-right:before, +.fa-repeat:before { + content: ""; } + +.fa-refresh:before { + content: ""; } + +.fa-list-alt:before { + content: ""; } + +.fa-lock:before { + content: ""; } + +.fa-flag:before { + content: ""; } + +.fa-headphones:before { + content: ""; } + +.fa-volume-off:before { + content: ""; } + +.fa-volume-down:before { + content: ""; } + +.fa-volume-up:before { + content: ""; } + +.fa-qrcode:before { + content: ""; } + +.fa-barcode:before { + content: ""; } + +.fa-tag:before { + content: ""; } + +.fa-tags:before { + content: ""; } + +.fa-book:before { + content: ""; } + +.fa-bookmark:before { + content: ""; } + +.fa-print:before { + content: ""; } + +.fa-camera:before { + content: ""; } + +.fa-font:before { + content: ""; } + +.fa-bold:before { + content: ""; } + +.fa-italic:before { + content: ""; } + +.fa-text-height:before { + content: ""; } + +.fa-text-width:before { + content: ""; } + +.fa-align-left:before { + content: ""; } + +.fa-align-center:before { + content: ""; } + +.fa-align-right:before { + content: ""; } + +.fa-align-justify:before { + content: ""; } + +.fa-list:before { + content: ""; } + +.fa-dedent:before, +.fa-outdent:before { + content: ""; } + +.fa-indent:before { + content: ""; } + +.fa-video-camera:before { + content: ""; } + +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: ""; } + +.fa-pencil:before { + content: ""; } + +.fa-map-marker:before { + content: ""; } + +.fa-adjust:before { + content: ""; } + +.fa-tint:before { + content: ""; } + +.fa-edit:before, +.fa-pencil-square-o:before { + content: ""; } + +.fa-share-square-o:before { + content: ""; } + +.fa-check-square-o:before { + content: ""; } + +.fa-arrows:before { + content: ""; } + +.fa-step-backward:before { + content: ""; } + +.fa-fast-backward:before { + content: ""; } + +.fa-backward:before { + content: ""; } + +.fa-play:before { + content: ""; } + +.fa-pause:before { + content: ""; } + +.fa-stop:before { + content: ""; } + +.fa-forward:before { + content: ""; } + +.fa-fast-forward:before { + content: ""; } + +.fa-step-forward:before { + content: ""; } + +.fa-eject:before { + content: ""; } + +.fa-chevron-left:before { + content: ""; } + +.fa-chevron-right:before { + content: ""; } + +.fa-plus-circle:before { + content: ""; } + +.fa-minus-circle:before { + content: ""; } + +.fa-times-circle:before { + content: ""; } + +.fa-check-circle:before { + content: ""; } + +.fa-question-circle:before { + content: ""; } + +.fa-info-circle:before { + content: ""; } + +.fa-crosshairs:before { + content: ""; } + +.fa-times-circle-o:before { + content: ""; } + +.fa-check-circle-o:before { + content: ""; } + +.fa-ban:before { + content: ""; } + +.fa-arrow-left:before { + content: ""; } + +.fa-arrow-right:before { + content: ""; } + +.fa-arrow-up:before { + content: ""; } + +.fa-arrow-down:before { + content: ""; } + +.fa-mail-forward:before, +.fa-share:before { + content: ""; } + +.fa-expand:before { + content: ""; } + +.fa-compress:before { + content: ""; } + +.fa-plus:before { + content: ""; } + +.fa-minus:before { + content: ""; } + +.fa-asterisk:before { + content: ""; } + +.fa-exclamation-circle:before { + content: ""; } + +.fa-gift:before { + content: ""; } + +.fa-leaf:before { + content: ""; } + +.fa-fire:before { + content: ""; } + +.fa-eye:before { + content: ""; } + +.fa-eye-slash:before { + content: ""; } + +.fa-warning:before, +.fa-exclamation-triangle:before { + content: ""; } + +.fa-plane:before { + content: ""; } + +.fa-calendar:before { + content: ""; } + +.fa-random:before { + content: ""; } + +.fa-comment:before { + content: ""; } + +.fa-magnet:before { + content: ""; } + +.fa-chevron-up:before { + content: ""; } + +.fa-chevron-down:before { + content: ""; } + +.fa-retweet:before { + content: ""; } + +.fa-shopping-cart:before { + content: ""; } + +.fa-folder:before { + content: ""; } + +.fa-folder-open:before { + content: ""; } + +.fa-arrows-v:before { + content: ""; } + +.fa-arrows-h:before { + content: ""; } + +.fa-bar-chart-o:before, +.fa-bar-chart:before { + content: ""; } + +.fa-twitter-square:before { + content: ""; } + +.fa-facebook-square:before { + content: ""; } + +.fa-camera-retro:before { + content: ""; } + +.fa-key:before { + content: ""; } + +.fa-gears:before, +.fa-cogs:before { + content: ""; } + +.fa-comments:before { + content: ""; } + +.fa-thumbs-o-up:before { + content: ""; } + +.fa-thumbs-o-down:before { + content: ""; } + +.fa-star-half:before { + content: ""; } + +.fa-heart-o:before { + content: ""; } + +.fa-sign-out:before { + content: ""; } + +.fa-linkedin-square:before { + content: ""; } + +.fa-thumb-tack:before { + content: ""; } + +.fa-external-link:before { + content: ""; } + +.fa-sign-in:before { + content: ""; } + +.fa-trophy:before { + content: ""; } + +.fa-github-square:before { + content: ""; } + +.fa-upload:before { + content: ""; } + +.fa-lemon-o:before { + content: ""; } + +.fa-phone:before { + content: ""; } + +.fa-square-o:before { + content: ""; } + +.fa-bookmark-o:before { + content: ""; } + +.fa-phone-square:before { + content: ""; } + +.fa-twitter:before { + content: ""; } + +.fa-facebook-f:before, +.fa-facebook:before { + content: ""; } + +.fa-github:before { + content: ""; } + +.fa-unlock:before { + content: ""; } + +.fa-credit-card:before { + content: ""; } + +.fa-feed:before, +.fa-rss:before { + content: ""; } + +.fa-hdd-o:before { + content: ""; } + +.fa-bullhorn:before { + content: ""; } + +.fa-bell:before { + content: ""; } + +.fa-certificate:before { + content: ""; } + +.fa-hand-o-right:before { + content: ""; } + +.fa-hand-o-left:before { + content: ""; } + +.fa-hand-o-up:before { + content: ""; } + +.fa-hand-o-down:before { + content: ""; } + +.fa-arrow-circle-left:before { + content: ""; } + +.fa-arrow-circle-right:before { + content: ""; } + +.fa-arrow-circle-up:before { + content: ""; } + +.fa-arrow-circle-down:before { + content: ""; } + +.fa-globe:before { + content: ""; } + +.fa-wrench:before { + content: ""; } + +.fa-tasks:before { + content: ""; } + +.fa-filter:before { + content: ""; } + +.fa-briefcase:before { + content: ""; } + +.fa-arrows-alt:before { + content: ""; } + +.fa-group:before, +.fa-users:before { + content: ""; } + +.fa-chain:before, +.fa-link:before { + content: ""; } + +.fa-cloud:before { + content: ""; } + +.fa-flask:before { + content: ""; } + +.fa-cut:before, +.fa-scissors:before { + content: ""; } + +.fa-copy:before, +.fa-files-o:before { + content: ""; } + +.fa-paperclip:before { + content: ""; } + +.fa-save:before, +.fa-floppy-o:before { + content: ""; } + +.fa-square:before { + content: ""; } + +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: ""; } + +.fa-list-ul:before { + content: ""; } + +.fa-list-ol:before { + content: ""; } + +.fa-strikethrough:before { + content: ""; } + +.fa-underline:before { + content: ""; } + +.fa-table:before { + content: ""; } + +.fa-magic:before { + content: ""; } + +.fa-truck:before { + content: ""; } + +.fa-pinterest:before { + content: ""; } + +.fa-pinterest-square:before { + content: ""; } + +.fa-google-plus-square:before { + content: ""; } + +.fa-google-plus:before { + content: ""; } + +.fa-money:before { + content: ""; } + +.fa-caret-down:before { + content: ""; } + +.fa-caret-up:before { + content: ""; } + +.fa-caret-left:before { + content: ""; } + +.fa-caret-right:before { + content: ""; } + +.fa-columns:before { + content: ""; } + +.fa-unsorted:before, +.fa-sort:before { + content: ""; } + +.fa-sort-down:before, +.fa-sort-desc:before { + content: ""; } + +.fa-sort-up:before, +.fa-sort-asc:before { + content: ""; } + +.fa-envelope:before { + content: ""; } + +.fa-linkedin:before { + content: ""; } + +.fa-rotate-left:before, +.fa-undo:before { + content: ""; } + +.fa-legal:before, +.fa-gavel:before { + content: ""; } + +.fa-dashboard:before, +.fa-tachometer:before { + content: ""; } + +.fa-comment-o:before { + content: ""; } + +.fa-comments-o:before { + content: ""; } + +.fa-flash:before, +.fa-bolt:before { + content: ""; } + +.fa-sitemap:before { + content: ""; } + +.fa-umbrella:before { + content: ""; } + +.fa-paste:before, +.fa-clipboard:before { + content: ""; } + +.fa-lightbulb-o:before { + content: ""; } + +.fa-exchange:before { + content: ""; } + +.fa-cloud-download:before { + content: ""; } + +.fa-cloud-upload:before { + content: ""; } + +.fa-user-md:before { + content: ""; } + +.fa-stethoscope:before { + content: ""; } + +.fa-suitcase:before { + content: ""; } + +.fa-bell-o:before { + content: ""; } + +.fa-coffee:before { + content: ""; } + +.fa-cutlery:before { + content: ""; } + +.fa-file-text-o:before { + content: ""; } + +.fa-building-o:before { + content: ""; } + +.fa-hospital-o:before { + content: ""; } + +.fa-ambulance:before { + content: ""; } + +.fa-medkit:before { + content: ""; } + +.fa-fighter-jet:before { + content: ""; } + +.fa-beer:before { + content: ""; } + +.fa-h-square:before { + content: ""; } + +.fa-plus-square:before { + content: ""; } + +.fa-angle-double-left:before { + content: ""; } + +.fa-angle-double-right:before { + content: ""; } + +.fa-angle-double-up:before { + content: ""; } + +.fa-angle-double-down:before { + content: ""; } + +.fa-angle-left:before { + content: ""; } + +.fa-angle-right:before { + content: ""; } + +.fa-angle-up:before { + content: ""; } + +.fa-angle-down:before { + content: ""; } + +.fa-desktop:before { + content: ""; } + +.fa-laptop:before { + content: ""; } + +.fa-tablet:before { + content: ""; } + +.fa-mobile-phone:before, +.fa-mobile:before { + content: ""; } + +.fa-circle-o:before { + content: ""; } + +.fa-quote-left:before { + content: ""; } + +.fa-quote-right:before { + content: ""; } + +.fa-spinner:before { + content: ""; } + +.fa-circle:before { + content: ""; } + +.fa-mail-reply:before, +.fa-reply:before { + content: ""; } + +.fa-github-alt:before { + content: ""; } + +.fa-folder-o:before { + content: ""; } + +.fa-folder-open-o:before { + content: ""; } + +.fa-smile-o:before { + content: ""; } + +.fa-frown-o:before { + content: ""; } + +.fa-meh-o:before { + content: ""; } + +.fa-gamepad:before { + content: ""; } + +.fa-keyboard-o:before { + content: ""; } + +.fa-flag-o:before { + content: ""; } + +.fa-flag-checkered:before { + content: ""; } + +.fa-terminal:before { + content: ""; } + +.fa-code:before { + content: ""; } + +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: ""; } + +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: ""; } + +.fa-location-arrow:before { + content: ""; } + +.fa-crop:before { + content: ""; } + +.fa-code-fork:before { + content: ""; } + +.fa-unlink:before, +.fa-chain-broken:before { + content: ""; } + +.fa-question:before { + content: ""; } + +.fa-info:before { + content: ""; } + +.fa-exclamation:before { + content: ""; } + +.fa-superscript:before { + content: ""; } + +.fa-subscript:before { + content: ""; } + +.fa-eraser:before { + content: ""; } + +.fa-puzzle-piece:before { + content: ""; } + +.fa-microphone:before { + content: ""; } + +.fa-microphone-slash:before { + content: ""; } + +.fa-shield:before { + content: ""; } + +.fa-calendar-o:before { + content: ""; } + +.fa-fire-extinguisher:before { + content: ""; } + +.fa-rocket:before { + content: ""; } + +.fa-maxcdn:before { + content: ""; } + +.fa-chevron-circle-left:before { + content: ""; } + +.fa-chevron-circle-right:before { + content: ""; } + +.fa-chevron-circle-up:before { + content: ""; } + +.fa-chevron-circle-down:before { + content: ""; } + +.fa-html5:before { + content: ""; } + +.fa-css3:before { + content: ""; } + +.fa-anchor:before { + content: ""; } + +.fa-unlock-alt:before { + content: ""; } + +.fa-bullseye:before { + content: ""; } + +.fa-ellipsis-h:before { + content: ""; } + +.fa-ellipsis-v:before { + content: ""; } + +.fa-rss-square:before { + content: ""; } + +.fa-play-circle:before { + content: ""; } + +.fa-ticket:before { + content: ""; } + +.fa-minus-square:before { + content: ""; } + +.fa-minus-square-o:before { + content: ""; } + +.fa-level-up:before { + content: ""; } + +.fa-level-down:before { + content: ""; } + +.fa-check-square:before { + content: ""; } + +.fa-pencil-square:before { + content: ""; } + +.fa-external-link-square:before { + content: ""; } + +.fa-share-square:before { + content: ""; } + +.fa-compass:before { + content: ""; } + +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: ""; } + +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: ""; } + +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: ""; } + +.fa-euro:before, +.fa-eur:before { + content: ""; } + +.fa-gbp:before { + content: ""; } + +.fa-dollar:before, +.fa-usd:before { + content: ""; } + +.fa-rupee:before, +.fa-inr:before { + content: ""; } + +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: ""; } + +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: ""; } + +.fa-won:before, +.fa-krw:before { + content: ""; } + +.fa-bitcoin:before, +.fa-btc:before { + content: ""; } + +.fa-file:before { + content: ""; } + +.fa-file-text:before { + content: ""; } + +.fa-sort-alpha-asc:before { + content: ""; } + +.fa-sort-alpha-desc:before { + content: ""; } + +.fa-sort-amount-asc:before { + content: ""; } + +.fa-sort-amount-desc:before { + content: ""; } + +.fa-sort-numeric-asc:before { + content: ""; } + +.fa-sort-numeric-desc:before { + content: ""; } + +.fa-thumbs-up:before { + content: ""; } + +.fa-thumbs-down:before { + content: ""; } + +.fa-youtube-square:before { + content: ""; } + +.fa-youtube:before { + content: ""; } + +.fa-xing:before { + content: ""; } + +.fa-xing-square:before { + content: ""; } + +.fa-youtube-play:before { + content: ""; } + +.fa-dropbox:before { + content: ""; } + +.fa-stack-overflow:before { + content: ""; } + +.fa-instagram:before { + content: ""; } + +.fa-flickr:before { + content: ""; } + +.fa-adn:before { + content: ""; } + +.fa-bitbucket:before { + content: ""; } + +.fa-bitbucket-square:before { + content: ""; } + +.fa-tumblr:before { + content: ""; } + +.fa-tumblr-square:before { + content: ""; } + +.fa-long-arrow-down:before { + content: ""; } + +.fa-long-arrow-up:before { + content: ""; } + +.fa-long-arrow-left:before { + content: ""; } + +.fa-long-arrow-right:before { + content: ""; } + +.fa-apple:before { + content: ""; } + +.fa-windows:before { + content: ""; } + +.fa-android:before { + content: ""; } + +.fa-linux:before { + content: ""; } + +.fa-dribbble:before { + content: ""; } + +.fa-skype:before { + content: ""; } + +.fa-foursquare:before { + content: ""; } + +.fa-trello:before { + content: ""; } + +.fa-female:before { + content: ""; } + +.fa-male:before { + content: ""; } + +.fa-gittip:before, +.fa-gratipay:before { + content: ""; } + +.fa-sun-o:before { + content: ""; } + +.fa-moon-o:before { + content: ""; } + +.fa-archive:before { + content: ""; } + +.fa-bug:before { + content: ""; } + +.fa-vk:before { + content: ""; } + +.fa-weibo:before { + content: ""; } + +.fa-renren:before { + content: ""; } + +.fa-pagelines:before { + content: ""; } + +.fa-stack-exchange:before { + content: ""; } + +.fa-arrow-circle-o-right:before { + content: ""; } + +.fa-arrow-circle-o-left:before { + content: ""; } + +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: ""; } + +.fa-dot-circle-o:before { + content: ""; } + +.fa-wheelchair:before { + content: ""; } + +.fa-vimeo-square:before { + content: ""; } + +.fa-turkish-lira:before, +.fa-try:before { + content: ""; } + +.fa-plus-square-o:before { + content: ""; } + +.fa-space-shuttle:before { + content: ""; } + +.fa-slack:before { + content: ""; } + +.fa-envelope-square:before { + content: ""; } + +.fa-wordpress:before { + content: ""; } + +.fa-openid:before { + content: ""; } + +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: ""; } + +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: ""; } + +.fa-yahoo:before { + content: ""; } + +.fa-google:before { + content: ""; } + +.fa-reddit:before { + content: ""; } + +.fa-reddit-square:before { + content: ""; } + +.fa-stumbleupon-circle:before { + content: ""; } + +.fa-stumbleupon:before { + content: ""; } + +.fa-delicious:before { + content: ""; } + +.fa-digg:before { + content: ""; } + +.fa-pied-piper-pp:before { + content: ""; } + +.fa-pied-piper-alt:before { + content: ""; } + +.fa-drupal:before { + content: ""; } + +.fa-joomla:before { + content: ""; } + +.fa-language:before { + content: ""; } + +.fa-fax:before { + content: ""; } + +.fa-building:before { + content: ""; } + +.fa-child:before { + content: ""; } + +.fa-paw:before { + content: ""; } + +.fa-spoon:before { + content: ""; } + +.fa-cube:before { + content: ""; } + +.fa-cubes:before { + content: ""; } + +.fa-behance:before { + content: ""; } + +.fa-behance-square:before { + content: ""; } + +.fa-steam:before { + content: ""; } + +.fa-steam-square:before { + content: ""; } + +.fa-recycle:before { + content: ""; } + +.fa-automobile:before, +.fa-car:before { + content: ""; } + +.fa-cab:before, +.fa-taxi:before { + content: ""; } + +.fa-tree:before { + content: ""; } + +.fa-spotify:before { + content: ""; } + +.fa-deviantart:before { + content: ""; } + +.fa-soundcloud:before { + content: ""; } + +.fa-database:before { + content: ""; } + +.fa-file-pdf-o:before { + content: ""; } + +.fa-file-word-o:before { + content: ""; } + +.fa-file-excel-o:before { + content: ""; } + +.fa-file-powerpoint-o:before { + content: ""; } + +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: ""; } + +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: ""; } + +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: ""; } + +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: ""; } + +.fa-file-code-o:before { + content: ""; } + +.fa-vine:before { + content: ""; } + +.fa-codepen:before { + content: ""; } + +.fa-jsfiddle:before { + content: ""; } + +.fa-life-bouy:before, +.fa-life-buoy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: ""; } + +.fa-circle-o-notch:before { + content: ""; } + +.fa-ra:before, +.fa-resistance:before, +.fa-rebel:before { + content: ""; } + +.fa-ge:before, +.fa-empire:before { + content: ""; } + +.fa-git-square:before { + content: ""; } + +.fa-git:before { + content: ""; } + +.fa-y-combinator-square:before, +.fa-yc-square:before, +.fa-hacker-news:before { + content: ""; } + +.fa-tencent-weibo:before { + content: ""; } + +.fa-qq:before { + content: ""; } + +.fa-wechat:before, +.fa-weixin:before { + content: ""; } + +.fa-send:before, +.fa-paper-plane:before { + content: ""; } + +.fa-send-o:before, +.fa-paper-plane-o:before { + content: ""; } + +.fa-history:before { + content: ""; } + +.fa-circle-thin:before { + content: ""; } + +.fa-header:before { + content: ""; } + +.fa-paragraph:before { + content: ""; } + +.fa-sliders:before { + content: ""; } + +.fa-share-alt:before { + content: ""; } + +.fa-share-alt-square:before { + content: ""; } + +.fa-bomb:before { + content: ""; } + +.fa-soccer-ball-o:before, +.fa-futbol-o:before { + content: ""; } + +.fa-tty:before { + content: ""; } + +.fa-binoculars:before { + content: ""; } + +.fa-plug:before { + content: ""; } + +.fa-slideshare:before { + content: ""; } + +.fa-twitch:before { + content: ""; } + +.fa-yelp:before { + content: ""; } + +.fa-newspaper-o:before { + content: ""; } + +.fa-wifi:before { + content: ""; } + +.fa-calculator:before { + content: ""; } + +.fa-paypal:before { + content: ""; } + +.fa-google-wallet:before { + content: ""; } + +.fa-cc-visa:before { + content: ""; } + +.fa-cc-mastercard:before { + content: ""; } + +.fa-cc-discover:before { + content: ""; } + +.fa-cc-amex:before { + content: ""; } + +.fa-cc-paypal:before { + content: ""; } + +.fa-cc-stripe:before { + content: ""; } + +.fa-bell-slash:before { + content: ""; } + +.fa-bell-slash-o:before { + content: ""; } + +.fa-trash:before { + content: ""; } + +.fa-copyright:before { + content: ""; } + +.fa-at:before { + content: ""; } + +.fa-eyedropper:before { + content: ""; } + +.fa-paint-brush:before { + content: ""; } + +.fa-birthday-cake:before { + content: ""; } + +.fa-area-chart:before { + content: ""; } + +.fa-pie-chart:before { + content: ""; } + +.fa-line-chart:before { + content: ""; } + +.fa-lastfm:before { + content: ""; } + +.fa-lastfm-square:before { + content: ""; } + +.fa-toggle-off:before { + content: ""; } + +.fa-toggle-on:before { + content: ""; } + +.fa-bicycle:before { + content: ""; } + +.fa-bus:before { + content: ""; } + +.fa-ioxhost:before { + content: ""; } + +.fa-angellist:before { + content: ""; } + +.fa-cc:before { + content: ""; } + +.fa-shekel:before, +.fa-sheqel:before, +.fa-ils:before { + content: ""; } + +.fa-meanpath:before { + content: ""; } + +.fa-buysellads:before { + content: ""; } + +.fa-connectdevelop:before { + content: ""; } + +.fa-dashcube:before { + content: ""; } + +.fa-forumbee:before { + content: ""; } + +.fa-leanpub:before { + content: ""; } + +.fa-sellsy:before { + content: ""; } + +.fa-shirtsinbulk:before { + content: ""; } + +.fa-simplybuilt:before { + content: ""; } + +.fa-skyatlas:before { + content: ""; } + +.fa-cart-plus:before { + content: ""; } + +.fa-cart-arrow-down:before { + content: ""; } + +.fa-diamond:before { + content: ""; } + +.fa-ship:before { + content: ""; } + +.fa-user-secret:before { + content: ""; } + +.fa-motorcycle:before { + content: ""; } + +.fa-street-view:before { + content: ""; } + +.fa-heartbeat:before { + content: ""; } + +.fa-venus:before { + content: ""; } + +.fa-mars:before { + content: ""; } + +.fa-mercury:before { + content: ""; } + +.fa-intersex:before, +.fa-transgender:before { + content: ""; } + +.fa-transgender-alt:before { + content: ""; } + +.fa-venus-double:before { + content: ""; } + +.fa-mars-double:before { + content: ""; } + +.fa-venus-mars:before { + content: ""; } + +.fa-mars-stroke:before { + content: ""; } + +.fa-mars-stroke-v:before { + content: ""; } + +.fa-mars-stroke-h:before { + content: ""; } + +.fa-neuter:before { + content: ""; } + +.fa-genderless:before { + content: ""; } + +.fa-facebook-official:before { + content: ""; } + +.fa-pinterest-p:before { + content: ""; } + +.fa-whatsapp:before { + content: ""; } + +.fa-server:before { + content: ""; } + +.fa-user-plus:before { + content: ""; } + +.fa-user-times:before { + content: ""; } + +.fa-hotel:before, +.fa-bed:before { + content: ""; } + +.fa-viacoin:before { + content: ""; } + +.fa-train:before { + content: ""; } + +.fa-subway:before { + content: ""; } + +.fa-medium:before { + content: ""; } + +.fa-yc:before, +.fa-y-combinator:before { + content: ""; } + +.fa-optin-monster:before { + content: ""; } + +.fa-opencart:before { + content: ""; } + +.fa-expeditedssl:before { + content: ""; } + +.fa-battery-4:before, +.fa-battery:before, +.fa-battery-full:before { + content: ""; } + +.fa-battery-3:before, +.fa-battery-three-quarters:before { + content: ""; } + +.fa-battery-2:before, +.fa-battery-half:before { + content: ""; } + +.fa-battery-1:before, +.fa-battery-quarter:before { + content: ""; } + +.fa-battery-0:before, +.fa-battery-empty:before { + content: ""; } + +.fa-mouse-pointer:before { + content: ""; } + +.fa-i-cursor:before { + content: ""; } + +.fa-object-group:before { + content: ""; } + +.fa-object-ungroup:before { + content: ""; } + +.fa-sticky-note:before { + content: ""; } + +.fa-sticky-note-o:before { + content: ""; } + +.fa-cc-jcb:before { + content: ""; } + +.fa-cc-diners-club:before { + content: ""; } + +.fa-clone:before { + content: ""; } + +.fa-balance-scale:before { + content: ""; } + +.fa-hourglass-o:before { + content: ""; } + +.fa-hourglass-1:before, +.fa-hourglass-start:before { + content: ""; } + +.fa-hourglass-2:before, +.fa-hourglass-half:before { + content: ""; } + +.fa-hourglass-3:before, +.fa-hourglass-end:before { + content: ""; } + +.fa-hourglass:before { + content: ""; } + +.fa-hand-grab-o:before, +.fa-hand-rock-o:before { + content: ""; } + +.fa-hand-stop-o:before, +.fa-hand-paper-o:before { + content: ""; } + +.fa-hand-scissors-o:before { + content: ""; } + +.fa-hand-lizard-o:before { + content: ""; } + +.fa-hand-spock-o:before { + content: ""; } + +.fa-hand-pointer-o:before { + content: ""; } + +.fa-hand-peace-o:before { + content: ""; } + +.fa-trademark:before { + content: ""; } + +.fa-registered:before { + content: ""; } + +.fa-creative-commons:before { + content: ""; } + +.fa-gg:before { + content: ""; } + +.fa-gg-circle:before { + content: ""; } + +.fa-tripadvisor:before { + content: ""; } + +.fa-odnoklassniki:before { + content: ""; } + +.fa-odnoklassniki-square:before { + content: ""; } + +.fa-get-pocket:before { + content: ""; } + +.fa-wikipedia-w:before { + content: ""; } + +.fa-safari:before { + content: ""; } + +.fa-chrome:before { + content: ""; } + +.fa-firefox:before { + content: ""; } + +.fa-opera:before { + content: ""; } + +.fa-internet-explorer:before { + content: ""; } + +.fa-tv:before, +.fa-television:before { + content: ""; } + +.fa-contao:before { + content: ""; } + +.fa-500px:before { + content: ""; } + +.fa-amazon:before { + content: ""; } + +.fa-calendar-plus-o:before { + content: ""; } + +.fa-calendar-minus-o:before { + content: ""; } + +.fa-calendar-times-o:before { + content: ""; } + +.fa-calendar-check-o:before { + content: ""; } + +.fa-industry:before { + content: ""; } + +.fa-map-pin:before { + content: ""; } + +.fa-map-signs:before { + content: ""; } + +.fa-map-o:before { + content: ""; } + +.fa-map:before { + content: ""; } + +.fa-commenting:before { + content: ""; } + +.fa-commenting-o:before { + content: ""; } + +.fa-houzz:before { + content: ""; } + +.fa-vimeo:before { + content: ""; } + +.fa-black-tie:before { + content: ""; } + +.fa-fonticons:before { + content: ""; } + +.fa-reddit-alien:before { + content: ""; } + +.fa-edge:before { + content: ""; } + +.fa-credit-card-alt:before { + content: ""; } + +.fa-codiepie:before { + content: ""; } + +.fa-modx:before { + content: ""; } + +.fa-fort-awesome:before { + content: ""; } + +.fa-usb:before { + content: ""; } + +.fa-product-hunt:before { + content: ""; } + +.fa-mixcloud:before { + content: ""; } + +.fa-scribd:before { + content: ""; } + +.fa-pause-circle:before { + content: ""; } + +.fa-pause-circle-o:before { + content: ""; } + +.fa-stop-circle:before { + content: ""; } + +.fa-stop-circle-o:before { + content: ""; } + +.fa-shopping-bag:before { + content: ""; } + +.fa-shopping-basket:before { + content: ""; } + +.fa-hashtag:before { + content: ""; } + +.fa-bluetooth:before { + content: ""; } + +.fa-bluetooth-b:before { + content: ""; } + +.fa-percent:before { + content: ""; } + +.fa-gitlab:before { + content: ""; } + +.fa-wpbeginner:before { + content: ""; } + +.fa-wpforms:before { + content: ""; } + +.fa-envira:before { + content: ""; } + +.fa-universal-access:before { + content: ""; } + +.fa-wheelchair-alt:before { + content: ""; } + +.fa-question-circle-o:before { + content: ""; } + +.fa-blind:before { + content: ""; } + +.fa-audio-description:before { + content: ""; } + +.fa-volume-control-phone:before { + content: ""; } + +.fa-braille:before { + content: ""; } + +.fa-assistive-listening-systems:before { + content: ""; } + +.fa-asl-interpreting:before, +.fa-american-sign-language-interpreting:before { + content: ""; } + +.fa-deafness:before, +.fa-hard-of-hearing:before, +.fa-deaf:before { + content: ""; } + +.fa-glide:before { + content: ""; } + +.fa-glide-g:before { + content: ""; } + +.fa-signing:before, +.fa-sign-language:before { + content: ""; } + +.fa-low-vision:before { + content: ""; } + +.fa-viadeo:before { + content: ""; } + +.fa-viadeo-square:before { + content: ""; } + +.fa-snapchat:before { + content: ""; } + +.fa-snapchat-ghost:before { + content: ""; } + +.fa-snapchat-square:before { + content: ""; } + +.fa-pied-piper:before { + content: ""; } + +.fa-first-order:before { + content: ""; } + +.fa-yoast:before { + content: ""; } + +.fa-themeisle:before { + content: ""; } + +.fa-google-plus-circle:before, +.fa-google-plus-official:before { + content: ""; } + +.fa-fa:before, +.fa-font-awesome:before { + content: ""; } + +.fa-handshake-o:before { + content: ""; } + +.fa-envelope-open:before { + content: ""; } + +.fa-envelope-open-o:before { + content: ""; } + +.fa-linode:before { + content: ""; } + +.fa-address-book:before { + content: ""; } + +.fa-address-book-o:before { + content: ""; } + +.fa-vcard:before, +.fa-address-card:before { + content: ""; } + +.fa-vcard-o:before, +.fa-address-card-o:before { + content: ""; } + +.fa-user-circle:before { + content: ""; } + +.fa-user-circle-o:before { + content: ""; } + +.fa-user-o:before { + content: ""; } + +.fa-id-badge:before { + content: ""; } + +.fa-drivers-license:before, +.fa-id-card:before { + content: ""; } + +.fa-drivers-license-o:before, +.fa-id-card-o:before { + content: ""; } + +.fa-quora:before { + content: ""; } + +.fa-free-code-camp:before { + content: ""; } + +.fa-telegram:before { + content: ""; } + +.fa-thermometer-4:before, +.fa-thermometer:before, +.fa-thermometer-full:before { + content: ""; } + +.fa-thermometer-3:before, +.fa-thermometer-three-quarters:before { + content: ""; } + +.fa-thermometer-2:before, +.fa-thermometer-half:before { + content: ""; } + +.fa-thermometer-1:before, +.fa-thermometer-quarter:before { + content: ""; } + +.fa-thermometer-0:before, +.fa-thermometer-empty:before { + content: ""; } + +.fa-shower:before { + content: ""; } + +.fa-bathtub:before, +.fa-s15:before, +.fa-bath:before { + content: ""; } + +.fa-podcast:before { + content: ""; } + +.fa-window-maximize:before { + content: ""; } + +.fa-window-minimize:before { + content: ""; } + +.fa-window-restore:before { + content: ""; } + +.fa-times-rectangle:before, +.fa-window-close:before { + content: ""; } + +.fa-times-rectangle-o:before, +.fa-window-close-o:before { + content: ""; } + +.fa-bandcamp:before { + content: ""; } + +.fa-grav:before { + content: ""; } + +.fa-etsy:before { + content: ""; } + +.fa-imdb:before { + content: ""; } + +.fa-ravelry:before { + content: ""; } + +.fa-eercast:before { + content: ""; } + +.fa-microchip:before { + content: ""; } + +.fa-snowflake-o:before { + content: ""; } + +.fa-superpowers:before { + content: ""; } + +.fa-wpexplorer:before { + content: ""; } + +.fa-meetup:before { + content: ""; } + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; } + +.sr-only-focusable:active, .sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; } + +sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; } + +button, input, optgroup, select, textarea { + color: inherit; + font: inherit; + margin: 0; } + +html { + font-family: sans-serif; + -ms-text-size-adjust: 100%; + -webkit-text-size-adjust: 100%; + font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 300; + color: #666; + font-size: 12px; + line-height: 1.75em; } + html input[type=button] { + -webkit-appearance: button; + cursor: pointer; } + html input[disabled] { + cursor: default; } + +body { + margin: 0; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + -moz-font-feature-settings: "liga" on; } + +article { + display: block; } + +aside { + display: block; } + +details { + display: block; } + +figcaption { + display: block; } + +figure { + display: block; + margin: 1em 40px; } + +footer { + display: block; } + +header { + display: block; } + +hgroup { + display: block; } + +main { + display: block; } + +menu { + display: block; } + +nav { + display: block; } + +section { + display: block; } + +summary { + display: block; } + +audio { + display: inline-block; + vertical-align: baseline; } + audio:not([controls]) { + display: none; + height: 0; } + +canvas { + display: inline-block; + vertical-align: baseline; } + +progress { + display: inline-block; + vertical-align: baseline; } + +video { + display: inline-block; + vertical-align: baseline; } + +[hidden] { + display: none; } + +template { + display: none; } + +a { + background-color: transparent; + text-decoration: none; + color: #1d73a2; + -webkit-transition: all .2s; + transition: all .2s; + margin: 0; + padding: 0; + cursor: pointer; } + a:active { + outline: 0; } + a:hover { + outline: 0; + color: #175c82; } + +abbr[title] { + border-bottom: 1px dotted; } + +b { + font-weight: 700; + margin: 0; + padding: 0; } + +strong { + font-weight: 700; + margin: 0; + padding: 0; } + +dfn { + font-style: italic; + margin: 0; + padding: 0; } + +h1 { + font-size: 2em; + margin: .67em 0; + margin-top: .942400822452556em; + line-height: 1.130880986943067em; + margin-bottom: .188480164490511em; + margin: 0; + padding: 0; + font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 500; + color: #111; + clear: both; } + +mark { + background: #ff0; + color: #000; } + +small { + font-size: 80%; + margin: 0; + padding: 0; + line-height: 0; } + +sub { + bottom: -.25em; + margin: 0; + padding: 0; + line-height: 0; } + +sup { + top: -.5em; + margin: 0; + padding: 0; + line-height: 0; } + sup a .fa { + font-size: 1em; } + +img { + border: 0; + margin: 0; + padding: 0; } + +hr { + -webkit-box-sizing: content-box; + box-sizing: content-box; + height: 0; } + +pre { + overflow: auto; + padding: .875em; + margin-bottom: 1.75em; + background: #222; + line-height: 1; + color: #ffff00; + border-radius: 3px; + font-size: 10px; + font-family: monospace; + font-size: 1em; + margin: 0; + padding: 0; + margin-bottom: 1.75em; } + pre code { + padding: 0; } + +code { + font-family: monospace; + font-size: 1em; + margin: 0; + padding: 0; + font-family: Courier New, Courier, Lucida Sans Typewriter, Lucida Typewriter, monospace; + padding: .0875em .2625em; + line-height: 0; } + +kbd { + font-family: monospace; + font-size: 1em; + margin: 0; + padding: 0; } + +samp { + font-family: monospace; + font-size: 1em; + margin: 0; + padding: 0; } + +button { + overflow: visible; + text-transform: none; + -webkit-appearance: button; + cursor: pointer; + display: block; + cursor: pointer; + font-size: 12px; + padding: .4375em 1.75em; + margin-bottom: 1.18125em; } + +input { + line-height: normal; } + input:focus { + background: #ffff00; } + +optgroup { + font-weight: 700; } + +select { + text-transform: none; + outline: none; + border: 1px solid #dddddd; + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; + width: 100%; + height: 35px; } + +textarea { + overflow: auto; + display: block; + max-width: 100%; + padding: .4375em; + font-size: 12px; + margin-bottom: 1.18125em; + outline: none; + border: 1px solid #dddddd; + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; } + +input[type=reset] { + -webkit-appearance: button; + cursor: pointer; } + +input[type=submit] { + -webkit-appearance: button; + cursor: pointer; + display: block; + cursor: pointer; + font-size: 12px; + padding: .4375em 1.75em; + margin-bottom: 1.18125em; } + +button[disabled] { + cursor: default; } + +button::-moz-focus-inner { + border: 0; + padding: 0; } + +input::-moz-focus-inner { + border: 0; + padding: 0; } + +input[type=checkbox] { + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0; } + +input[type=radio] { + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding: 0; } + +input[type=number]::-webkit-inner-spin-button { + height: auto; } + +input[type=number]::-webkit-outer-spin-button { + height: auto; } + +input[type=search] { + -webkit-appearance: textfield; + -webkit-box-sizing: content-box; + box-sizing: content-box; } + +input[type=search]::-webkit-search-cancel-button { + -webkit-appearance: none; } + +input[type=search]::-webkit-search-decoration { + -webkit-appearance: none; } + +fieldset { + border: 1px solid silver; + margin: 0 2px; + padding: .35em .625em .75em; + padding: .875em 1.75em 1.75em; + border-width: 1px; + border-style: solid; + max-width: 100%; + margin-bottom: 1.8375em; + margin: 0; + padding: 0; } + fieldset button { + margin-bottom: 0; } + fieldset input[type=submit] { + margin-bottom: 0; } + +legend { + border: 0; + padding: 0; + color: #111; + font-weight: 700; + margin: 0; + padding: 0; } + +table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + margin-bottom: 2.1875em; + margin: 0; + padding: 0; + margin-bottom: 1.75em; } + +td { + padding: 0; + margin: 0; + padding: 0; + padding: .21875em .875em; } + +th { + padding: 0; + margin: 0; + padding: 0; + text-align: left; + color: #111; + padding: .21875em .875em; } + +@media (min-width: 600px) { + html { + font-size: 12px; } + + h1 { + font-size: calc(27.85438995234061px +18.56959 *((100vw - 600px) / 540)); } + + h2 { + font-size: calc(23.53700340860508px +15.69134 *((100vw - 600px) / 540)); } + + h3 { + font-size: calc(19.888804974891777px +13.2592 *((100vw - 600px) / 540)); } + + h4 { + font-size: calc(16.806071548796314px +11.20405 *((100vw - 600px) / 540)); } + + h5 { + font-size: calc(14.201156945318074px +9.46744 *((100vw - 600px) / 540)); } + + h6 { + font-size: calc(12px +8 *((100vw - 600px) / 540)); } } +abbr { + margin: 0; + padding: 0; + border-bottom: 1px dotted currentColor; + cursor: help; } + +acronym { + margin: 0; + padding: 0; + border-bottom: 1px dotted currentColor; + cursor: help; } + +address { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + font-style: normal; } + +big { + margin: 0; + padding: 0; + line-height: 0; } + +blockquote { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + font-style: italic; } + blockquote cite { + display: block; + font-style: normal; } + +caption { + margin: 0; + padding: 0; } + +center { + margin: 0; + padding: 0; } + +cite { + margin: 0; + padding: 0; } + +dd { + margin: 0; + padding: 0; } + +del { + margin: 0; + padding: 0; } + +dl { + margin: 0; + padding: 0; + margin-bottom: 1.75em; } + +dt { + margin: 0; + padding: 0; + color: #111; + font-weight: 700; } + +em { + margin: 0; + padding: 0; } + +form { + margin: 0; + padding: 0; } + +h2 { + margin: 0; + padding: 0; + font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 500; + color: #111; + clear: both; + font-size: 23.53700340860508px; + margin-top: 1.115265165420465em; + line-height: 1.338318198504558em; + margin-bottom: .251483121980101em; } + +h3 { + margin: 0; + padding: 0; + font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 500; + color: #111; + clear: both; + font-size: 19.888804974891777px; + margin-top: 1.319837970815179em; + line-height: 1.583805564978215em; + margin-bottom: .303784103173448em; } + +h4 { + margin: 0; + padding: 0; + font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 500; + color: #111; + clear: both; + font-size: 16.806071548796314px; + margin-top: 1.561935513828041em; + line-height: 1.87432261659365em; + margin-bottom: .368150361036632em; } + +h5 { + margin: 0; + padding: 0; + font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 500; + color: #111; + clear: both; + font-size: 14.201156945318074px; + margin-top: 1.84844094752817em; + line-height: 2.218129137033805em; + margin-bottom: .369688189505634em; } + +h6 { + margin: 0; + padding: 0; + font-family: Roboto, Helvetica Neue, Helvetica, Arial, sans-serif; + font-weight: 500; + color: #111; + clear: both; + font-size: 12px; + margin-top: 2.1875em; + line-height: 2.625em; + margin-bottom: .619791666666667em; } + +i { + margin: 0; + padding: 0; } + +ins { + margin: 0; + padding: 0; } + +label { + margin: 0; + padding: 0; + display: block; + padding-bottom: .21875em; + margin-bottom: -.21875em; + cursor: pointer; } + +li { + margin: 0; + padding: 0; } + +ol { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + padding-left: 1.4em; } + +p { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + margin: 0 0 1rem 0; } + +q { + margin: 0; + padding: 0; } + +s { + margin: 0; + padding: 0; } + +strike { + margin: 0; + padding: 0; } + +tbody { + margin: 0; + padding: 0; } + +tfoot { + margin: 0; + padding: 0; } + +thead { + margin: 0; + padding: 0; } + +tr { + margin: 0; + padding: 0; } + +tt { + margin: 0; + padding: 0; } + +u { + margin: 0; + padding: 0; } + +ul { + margin: 0; + padding: 0; + margin-bottom: 1.75em; + padding-left: 1.1em; } + +var { + margin: 0; + padding: 0; } + +@media (min-width: 1140px) { + h1 { + font-size: 46.423983253901014px; + margin-top: .942400822452556em; + line-height: 1.130880986943067em; + margin-bottom: .188480164490511em; } + + h2 { + font-size: 39.228339014341806px; + margin-top: 1.115265165420465em; + line-height: 1.338318198504558em; + margin-bottom: .240111086421698em; } + + h3 { + font-size: 33.14800829148629px; + margin-top: 1.319837970815179em; + line-height: 1.583805564978215em; + margin-bottom: .287857499569283em; } + + h4 { + font-size: 28.01011924799386px; + margin-top: 1.561935513828041em; + line-height: 1.87432261659365em; + margin-bottom: .345845057728222em; } + + h5 { + font-size: 23.668594908863454px; + margin-top: 1.84844094752817em; + line-height: 2.218129137033805em; + margin-bottom: .369688189505634em; } + + h6 { + font-size: 20px; + margin-top: 2.1875em; + line-height: 2.625em; + margin-bottom: .473958333333333em; } + + fieldset { + margin-bottom: 2.078125em; } + + input[type=email] { + font-size: 20px; + margin-bottom: .5140625em; } + + input[type=password] { + font-size: 20px; + margin-bottom: .5140625em; } + + input[type=text] { + font-size: 20px; + margin-bottom: .5140625em; } + + textarea { + font-size: 20px; + margin-bottom: .5140625em; } + + button { + font-size: 20px; + margin-bottom: 1.3125em; } + + input[type=submit] { + font-size: 20px; + margin-bottom: 1.3125em; } + + table { + margin-bottom: 2.05625em; } + + th { + padding: .4375em .875em; } + + td { + padding: .4375em .875em; } } +input[type=email] { + display: block; + max-width: 100%; + padding: .4375em; + font-size: 12px; + margin-bottom: 1.18125em; } + +input[type=password] { + display: block; + max-width: 100%; + padding: .4375em; + font-size: 12px; + margin-bottom: 1.18125em; } + +input[type=text] { + display: block; + max-width: 100%; + padding: .4375em; + font-size: 12px; + margin-bottom: 1.18125em; } + +.master { + background-image: url("../img/background.png"); + background-size: cover; + background-position: top; + min-height: 100vh; + display: -ms-flexbox; + display: -webkit-box; + display: flex; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; + -ms-flex-align: center; + -webkit-box-align: center; + align-items: center; } + +.box { + width: 450px; + border-radius: 0 0 3px 3px; + overflow: hidden; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: 0 10px 10px rgba(0, 0, 0, 0.19), 0 6px 3px rgba(0, 0, 0, 0.23); + box-shadow: 0 10px 10px rgba(0, 0, 0, 0.19), 0 6px 3px rgba(0, 0, 0, 0.23); } + +.header { + background-color: #357295; + padding: 30px 30px 40px; + border-radius: 3px 3px 0 0; + text-align: center; } + +.header__step { + font-weight: 300; + text-transform: uppercase; + font-size: 14px; + letter-spacing: 1.1px; + margin: 0 0 10px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + color: #fff; } + +.header__title { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + color: #fff; + font-weight: 400; + font-size: 20px; + margin: 0 0 15px; } + +.step { + position: relative; + z-index: 1; + padding-left: 0; + list-style: none; + margin-bottom: 0; + display: -ms-flexbox; + display: -webkit-box; + display: flex; + -ms-flex-direction: row-reverse; + -webkit-box-orient: horizontal; + -webkit-box-direction: reverse; + flex-direction: row-reverse; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; + -ms-flex-align: center; + -webkit-box-align: center; + align-items: center; + margin-top: -20px; } + +.step__divider { + background-color: #cacfd2; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + width: 60px; + height: 3px; } + .step__divider:first-child { + -ms-flex: 1 0 auto; + -webkit-box-flex: 1; + flex: 1 0 auto; } + .step__divider:last-child { + -ms-flex: 1 0 auto; + -webkit-box-flex: 1; + flex: 1 0 auto; } + +.step__icon { + background-color: #cacfd2; + font-style: normal; + width: 40px; + height: 40px; + display: -ms-flexbox; + display: -webkit-box; + display: flex; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; + -ms-flex-align: center; + -webkit-box-align: center; + align-items: center; + border-radius: 50%; + color: #fff; } + .step__icon.welcome:before { + content: '\f144'; + font-family: Ionicons; } + .step__icon.requirements:before { + content: '\f127'; + font-family: Ionicons; } + .step__icon.permissions:before { + content: '\f296'; + font-family: Ionicons; } + .step__icon.database:before { + content: '\f454'; + font-family: Ionicons; } + .step__icon.update:before { + content: '\f2bf'; + font-family: Ionicons; } + +.main { + margin-top: -20px; + background-color: #fff; + border-radius: 0 0 3px 3px; + padding: 40px 40px 30px; } + +.buttons { + text-align: center; } + .buttons .button { + margin: .5em; } + +.buttons--right { + text-align: right; } + +.button { + display: inline-block; + background-color: #34a0db; + border-radius: 2px; + padding: 7px 20px; + color: #fff; + -webkit-box-shadow: 0 1px 1.5px rgba(0, 0, 0, 0.12), 0 1px 1px rgba(0, 0, 0, 0.24); + box-shadow: 0 1px 1.5px rgba(0, 0, 0, 0.12), 0 1px 1px rgba(0, 0, 0, 0.24); + text-decoration: none; + outline: none; + border: none; + -webkit-transition: background-color .2s ease, -webkit-box-shadow .2s ease; + transition: background-color .2s ease, -webkit-box-shadow .2s ease; + transition: box-shadow .2s ease, background-color .2s ease; + transition: box-shadow .2s ease, background-color .2s ease, -webkit-box-shadow .2s ease; + cursor: pointer; } + .button:hover { + color: #fff; + -webkit-box-shadow: 0 10px 10px rgba(0, 0, 0, 0.19), 0 6px 3px rgba(0, 0, 0, 0.23); + box-shadow: 0 10px 10px rgba(0, 0, 0, 0.19), 0 6px 3px rgba(0, 0, 0, 0.23); + background-color: #2490cb; } + +.button--light { + padding: 3px 16px; + font-size: 16px; + border-top: 1px solid #eee; + color: #222; + background: #fff; } + .button--light:hover { + color: #222; + background: #fff; + -webkit-box-shadow: 0 3px 3px rgba(0, 0, 0, 0.16), 0 3px 3px rgba(0, 0, 0, 0.23); + box-shadow: 0 3px 3px rgba(0, 0, 0, 0.16), 0 3px 3px rgba(0, 0, 0, 0.23); } + +.list { + padding-left: 0; + list-style: none; + margin-bottom: 0; + margin: 20px 0 35px; + border: 1px solid rgba(0, 0, 0, 0.12); + border-radius: 2px; } + .list .list__item.list__title { + background: rgba(0, 0, 0, 0.12); } + .list .list__item.list__title.success span { + color: #008000; } + .list .list__item.list__title.success .fa:before { + color: #008000; } + .list .list__item.list__title.error span { + color: #ff0000; } + .list .list__item.list__title.error .fa:before { + color: #ff0000; } + +.list__item { + position: relative; + overflow: hidden; + padding: 7px 20px; + border-bottom: 1px solid rgba(0, 0, 0, 0.12); } + .list__item:first-letter { + text-transform: uppercase; } + .list__item:last-child { + border-bottom: none; } + .list__item .fa.row-icon:before { + display: -ms-flexbox; + display: -webkit-box; + display: flex; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; + -ms-flex-align: center; + -webkit-box-align: center; + align-items: center; + padding: 7px 20px; + position: absolute; + top: 0; + right: 0; + bottom: 0; } + .list__item.success .fa:before { + color: #2ecc71; } + .list__item.error .fa:before { + color: #e74c3c; } + +.list__item--permissions:before { + content: '' !important; } +.list__item--permissions span { + display: -ms-flexbox; + display: -webkit-box; + display: flex; + -ms-flex-pack: center; + -webkit-box-pack: center; + justify-content: center; + -ms-flex-align: center; + -webkit-box-align: center; + align-items: center; + padding: 7px 20px; + position: absolute; + top: 0; + right: 0; + bottom: 0; + background-color: #f5f5f5; + font-weight: 700; + font-size: 16px; } + .list__item--permissions span:before { + margin-right: 7px; + font-weight: 400; } +.list__item--permissions.success i:before { + color: #2ecc71; + vertical-align: 1px; } +.list__item--permissions.error i:before { + color: #e74c3c; + vertical-align: 1px; } + +.textarea { + -webkit-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + font-size: 14px; + line-height: 25px; + height: 150px; + outline: none; + border: 1px solid rgba(0, 0, 0, 0.2); } + .textarea ~ .button { + margin-bottom: 35px; } + +.text-center { + text-align: center; } + +.form-control { + height: 14px; + width: 100%; } + +.has-error { + color: #ff0000; } + .has-error input { + color: #000000; + border: 1px solid red; } + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; } + +input[type='text'] { + outline: none; + border: 1px solid #dddddd; + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; } + +input[type='password'] { + outline: none; + border: 1px solid #dddddd; + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; } + +input[type='url'] { + outline: none; + border: 1px solid #dddddd; + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; } + +input[type='number'] { + outline: none; + border: 1px solid #dddddd; + border-radius: 3px; + padding: 10px 12px; + width: calc(100% - 24px); + margin: 0 auto 1em; } + +.tabs { + padding: 0; } + .tabs .tab-input { + display: none; } + .tabs .tab-input:checked + .tab-label { + background-color: #fff; + color: #333; } + .tabs .tab-label { + color: #ddd; + cursor: pointer; + float: left; + padding: 1em; + text-align: center; + -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; } + .tabs .tab-label:hover { + color: #333; } + .tabs .tabs-wrap { + clear: both; } + .tabs .tab { + display: none; } + .tabs .tab > *:last-child { + margin-bottom: 0; } + +.float-left { + float: left; } + +.float-right { + float: right; } + +.buttons-container { + min-height: 37px; + margin: 1em 0 0; } + +.block { + -webkit-box-shadow: 0 3px 1px darkgray; + box-shadow: 0 3px 1px darkgray; } + .block input[type='radio'] { + width: 100%; + display: none; } + .block input[type='radio'] + label { + background: #008080; + color: #fff; + padding-top: 5px; + -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; } + .block input[type='radio'] + label:hover { + background: #144242; + color: #fff; + padding-top: 5px; } + .block input[type='radio']:checked + label { + background-color: #a9a9a9; } + .block input[type='radio']:checked ~ .info { + height: 200px; + overflow-y: auto; + -webkit-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; } + .block input[type='radio'] ~ .info > div { + padding-top: 15px; } + .block label { + width: 450px; + max-width: 100%; + cursor: pointer; } + .block span { + font-family: Arial; + font-weight: 700; + display: block; + padding: 10px 12px 12px 15px; + margin: 0; + cursor: pointer; } + +.info { + background: #fff; + color: #222; + width: 100%; + height: 0; + line-height: 2; + padding-left: 15px; + padding-right: 15px; + display: block; + overflow: hidden; + -webkit-box-sizing: border-box; + box-sizing: border-box; + -webkit-transition: .3s ease-out; + transition: .3s ease-out; } + +::-moz-selection { + background: #222; + color: #fff; } + +::selection { + background: #222; + color: #fff; } + +::-webkit-scrollbar { + width: 12px; } + +::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.1); + border-radius: 0; } + +::-webkit-scrollbar-thumb { + border-radius: 0; + background: #ccc; + -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); } + +.margin-bottom-1 { + margin-bottom: 1em; } + +.margin-bottom-2 { + margin-bottom: 1em; } + +.margin-top-1 { + margin-top: 1em; } + +.margin-top-2 { + margin-top: 1em; } + +.alert { + margin: 0 0 10px; + font-size: 1.1em; + background-color: #f5f5f5; + border-radius: 3px; + padding: 10px; + position: relative; } + .alert.alert-danger { + background: #ff0000; + color: #ffffff; + padding: 10px 20px 15px; } + .alert.alert-danger h4 { + color: #ffffff; + margin: 0; } + .alert.alert-danger ul { + margin: 0; } + .alert .close { + width: 25px; + height: 25px; + padding: 0; + margin: 0; + -webkit-box-shadow: none; + box-shadow: none; + border: 2px solid red; + outline: none; + float: right; + border-radius: 50%; + position: absolute; + right: 0; + top: 0; + background-color: transparent; + cursor: pointer; + -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; } + .alert .close:hover { + background-color: #ffffff; + color: #ff0000; } + +svg:not(:root) { + overflow: hidden; } + +.step__item.active .step__icon, +.step__item.active ~ .step__divider, +.step__item.active ~ .step__item .step__icon { + background-color: #34a0db; + -webkit-transition: all 0.1s ease-in-out; + transition: all 0.1s ease-in-out; } + +.step__item.active .step__icon:hover, +.step__item.active ~ .step__item .step__icon:hover { + background-color: #3d657b; } + +.form-group.has-error select { + border-color: #ff0000; } +.form-group.has-error textarea { + border-color: #ff0000; } +.form-group.has-error input[type='text'] { + border-color: #ff0000; } +.form-group.has-error input[type='password'] { + border-color: #ff0000; } +.form-group.has-error input[type='url'] { + border-color: #ff0000; } +.form-group.has-error input[type='number'] { + border-color: #ff0000; } +.form-group.has-error .error-block { + margin: -12px 0 0; + display: block; + width: 100%; + font-size: .9em; + color: #ff0000; + font-weight: 500; } + +.tabs-full .tab-label { + display: table-cell; + float: none; + width: 1%; } + +#tab1:checked ~ .tabs-wrap #tab1content { + display: block; } + +#tab2:checked ~ .tabs-wrap #tab2content { + display: block; } + +#tab3:checked ~ .tabs-wrap #tab3content { + display: block; } + +#tab4:checked ~ .tabs-wrap #tab4content { + display: block; } + +#tab5:checked ~ .tabs-wrap #tab5content { + display: block; } + +.github img { + position: absolute; + top: 0; + right: 0; + border: 0; } + +#tab3content .block:first-child { + border-radius: 3px 3px 0 0; } +#tab3content .block:last-child { + border-radius: 0 0 3px 3px; } + +/*# sourceMappingURL=style.css.map */ \ No newline at end of file diff --git a/public/installer/css/style.css.map b/public/installer/css/style.css.map new file mode 100755 index 000000000..a80450a40 --- /dev/null +++ b/public/installer/css/style.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": ";AAAA;;;GAGG;ACHH;gCACgC;ACuBxB,iFAAW;ADrBnB,UAWC;EAVC,WAAW,EAAE,aAAa;EAC1B,GAAG,EAAE,+CAAgE;EACrE,GAAG,EAAE,4WAI8F;EAEnG,WAAW,EAAE,MAAM;EACnB,UAAU,EAAE,MAAM;AEVpB,GAAmB;EACjB,OAAO,EAAE,YAAY;EACrB,IAAI,EAAE,uCAA8E;EACpF,SAAS,EAAE,OAAO;EAClB,cAAc,EAAE,IAAI;EACpB,sBAAsB,EAAE,WAAW;EACnC,uBAAuB,EAAE,SAAS;;ACNpC,8DAA8D;AAC9D,MAAsB;EACpB,SAAS,EAAE,cAAS;EACpB,WAAW,EAAE,MAAS;EACtB,cAAc,EAAE,IAAI;;AAEtB,MAAsB;EAAE,SAAS,EAAE,GAAG;;AACtC,MAAsB;EAAE,SAAS,EAAE,GAAG;;AACtC,MAAsB;EAAE,SAAS,EAAE,GAAG;;AACtC,MAAsB;EAAE,SAAS,EAAE,GAAG;;ACVtC,MAAsB;EACpB,KAAK,EAAE,cAAW;EAClB,UAAU,EAAE,MAAM;;ACDpB,MAAsB;EACpB,YAAY,EAAE,CAAC;EACf,WAAW,ECMU,cAAS;EDL9B,eAAe,EAAE,IAAI;EACrB,WAAK;IAAE,QAAQ,EAAE,QAAQ;;AAE3B,MAAsB;EACpB,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,eAAa;EACnB,KAAK,ECDgB,cAAS;EDE9B,GAAG,EAAE,cAAU;EACf,UAAU,EAAE,MAAM;EAClB,YAAuB;IACrB,IAAI,EAAE,eAA0B;;AEbpC,UAA0B;EACxB,OAAO,EAAE,gBAAgB;EACzB,MAAM,EAAE,iBAA4B;EACpC,aAAa,EAAE,IAAI;;AAGrB,aAA6B;EAAE,KAAK,EAAE,IAAI;;AAC1C,cAA8B;EAAE,KAAK,EAAE,KAAK;;AAG1C,gBAA8B;EAAE,YAAY,EAAE,IAAI;AAClD,iBAA+B;EAAE,WAAW,EAAE,IAAI;;AAGpD,4BAA4B;AAC5B,WAAY;EAAE,KAAK,EAAE,KAAK;;AAC1B,UAAW;EAAE,KAAK,EAAE,IAAI;;AAGtB,aAAY;EAAE,YAAY,EAAE,IAAI;AAChC,cAAa;EAAE,WAAW,EAAE,IAAI;;ACpBlC,QAAwB;EACtB,iBAAiB,EAAE,0BAA0B;EACrC,SAAS,EAAE,0BAA0B;;AAG/C,SAAyB;EACvB,iBAAiB,EAAE,4BAA4B;EACvC,SAAS,EAAE,4BAA4B;;AAGjD,0BASC;EARC,EAAG;IACD,iBAAiB,EAAE,YAAY;IACvB,SAAS,EAAE,YAAY;EAEjC,IAAK;IACH,iBAAiB,EAAE,cAAc;IACzB,SAAS,EAAE,cAAc;AAIrC,kBASC;EARC,EAAG;IACD,iBAAiB,EAAE,YAAY;IACvB,SAAS,EAAE,YAAY;EAEjC,IAAK;IACH,iBAAiB,EAAE,cAAc;IACzB,SAAS,EAAE,cAAc;AC5BrC,aAA8B;ECW5B,UAAU,EAAE,0DAAqE;EACjF,iBAAiB,EAAE,aAAgB;EAC/B,aAAa,EAAE,aAAgB;EAC3B,SAAS,EAAE,aAAgB;;ADbrC,cAA8B;ECU5B,UAAU,EAAE,0DAAqE;EACjF,iBAAiB,EAAE,cAAgB;EAC/B,aAAa,EAAE,cAAgB;EAC3B,SAAS,EAAE,cAAgB;;ADZrC,cAA8B;ECS5B,UAAU,EAAE,0DAAqE;EACjF,iBAAiB,EAAE,cAAgB;EAC/B,aAAa,EAAE,cAAgB;EAC3B,SAAS,EAAE,cAAgB;;ADVrC,mBAAmC;ECcjC,UAAU,EAAE,oEAA+E;EAC3F,iBAAiB,EAAE,YAAoB;EACnC,aAAa,EAAE,YAAoB;EAC/B,SAAS,EAAE,YAAoB;;ADhBzC,iBAAmC;ECajC,UAAU,EAAE,oEAA+E;EAC3F,iBAAiB,EAAE,YAAoB;EACnC,aAAa,EAAE,YAAoB;EAC/B,SAAS,EAAE,YAAoB;;ADXzC;;;;uBAIuC;EACrC,MAAM,EAAE,IAAI;;AEfd,SAAyB;EACvB,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,YAAY;EACrB,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,MAAM;;AAExB,0BAAyD;EACvD,QAAQ,EAAE,QAAQ;EAClB,IAAI,EAAE,CAAC;EACP,KAAK,EAAE,IAAI;EACX,UAAU,EAAE,MAAM;;AAEpB,YAA4B;EAAE,WAAW,EAAE,OAAO;;AAClD,YAA4B;EAAE,SAAS,EAAE,GAAG;;AAC5C,WAA2B;EAAE,KAAK,ELTZ,IAAI;;AMV1B;oEACoE;AAEpE,gBAAgC;EAAE,OAAO,ENwU1B,GAAO;;AMvUtB,gBAAgC;EAAE,OAAO,EN2d1B,GAAO;;AM1dtB,iBAAiC;EAAE,OAAO,EN0jB1B,GAAO;;AMzjBvB,qBAAqC;EAAE,OAAO,ENsO1B,GAAO;;AMrO3B,gBAAgC;EAAE,OAAO,ENuW1B,GAAO;;AMtWtB,eAA+B;EAAE,OAAO,ENknB1B,GAAO;;AMjnBrB,iBAAiC;EAAE,OAAO,ENsnB1B,GAAO;;AMrnBvB,eAA+B;EAAE,OAAO,ENytB1B,GAAO;;AMxtBrB,eAA+B;EAAE,OAAO,ENmR1B,GAAO;;AMlRrB,mBAAmC;EAAE,OAAO,ENupB1B,GAAO;;AMtpBzB,aAA6B;EAAE,OAAO,ENqpB1B,GAAO;;AMppBnB,kBAAkC;EAAE,OAAO,ENspB1B,GAAO;;AMrpBxB,gBAAgC;EAAE,OAAO,ENyI1B,GAAO;;AMxItB;;gBAEgC;EAAE,OAAO,ENqqB1B,GAAO;;AMpqBtB,sBAAsC;EAAE,OAAO,EN8iB1B,GAAO;;AM7iB5B,uBAAuC;EAAE,OAAO,EN4iB1B,GAAO;;AM3iB7B,oBAAoC;EAAE,OAAO,EN4f1B,GAAO;;AM3f1B,iBAAiC;EAAE,OAAO,ENikB1B,GAAO;;AMhkBvB;cAC8B;EAAE,OAAO,ENgK1B,GAAO;;AM/JpB,kBAAkC;EAAE,OAAO,EN+qB1B,GAAO;;AM9qBxB,eAA+B;EAAE,OAAO,ENwV1B,GAAO;;AMvVrB,iBAAiC;EAAE,OAAO,ENuP1B,GAAO;;AMtPvB,kBAAkC;EAAE,OAAO,ENgJ1B,GAAO;;AM/IxB,eAA+B;EAAE,OAAO,ENmhB1B,GAAO;;AMlhBrB,mBAAmC;EAAE,OAAO,ENgM1B,GAAO;;AM/LzB,8BAA8C;EAAE,OAAO,ENY1B,GAAO;;AMXpC,4BAA4C;EAAE,OAAO,ENc1B,GAAO;;AMblC,gBAAgC;EAAE,OAAO,ENqW1B,GAAO;;AMpWtB,wBAAwC;EAAE,OAAO,ENwe1B,GAAO;;AMve9B;iBACiC;EAAE,OAAO,ENsgB1B,GAAO;;AMrgBvB,kBAAkC;EAAE,OAAO,ENggB1B,GAAO;;AM/fxB,mBAAmC;EAAE,OAAO,ENwY1B,GAAO;;AMvYzB,eAA+B;EAAE,OAAO,EN2Y1B,GAAO;;AM1YrB,eAA+B;EAAE,OAAO,EN4P1B,GAAO;;AM3PrB,qBAAqC;EAAE,OAAO,ENoU1B,GAAO;;AMnU3B,qBAAqC;EAAE,OAAO,ENitB1B,GAAO;;AMhtB3B,sBAAsC;EAAE,OAAO,EN+sB1B,GAAO;;AM9sB5B,oBAAoC;EAAE,OAAO,ENgtB1B,GAAO;;AM/sB1B,iBAAiC;EAAE,OAAO,ENye1B,GAAO;;AMxevB,kBAAkC;EAAE,OAAO,ENwB1B,GAAO;;AMvBxB,cAA8B;EAAE,OAAO,ENymB1B,GAAO;;AMxmBpB,eAA+B;EAAE,OAAO,ENymB1B,GAAO;;AMxmBrB,eAA+B;EAAE,OAAO,ENyD1B,GAAO;;AMxDrB,mBAAmC;EAAE,OAAO,ENyD1B,GAAO;;AMxDzB,gBAAgC;EAAE,OAAO,EN+d1B,GAAO;;AM9dtB,iBAAiC;EAAE,OAAO,EN2E1B,GAAO;;AM1EvB,eAA+B;EAAE,OAAO,EN0P1B,GAAO;;AMzPrB,eAA+B;EAAE,OAAO,ENiD1B,GAAO;;AMhDrB,iBAAiC;EAAE,OAAO,EN0V1B,GAAO;;AMzVvB,sBAAsC;EAAE,OAAO,ENwmB1B,GAAO;;AMvmB5B,qBAAqC;EAAE,OAAO,ENwmB1B,GAAO;;AMvmB3B,qBAAqC;EAAE,OAAO,ENpC1B,GAAO;;AMqC3B,uBAAuC;EAAE,OAAO,ENvC1B,GAAO;;AMwC7B,sBAAsC;EAAE,OAAO,ENrC1B,GAAO;;AMsC5B,wBAAwC;EAAE,OAAO,ENxC1B,GAAO;;AMyC9B,eAA+B;EAAE,OAAO,EN+W1B,GAAO;;AM9WrB;kBACkC;EAAE,OAAO,EN2a1B,GAAO;;AM1axB,iBAAiC;EAAE,OAAO,ENsU1B,GAAO;;AMrUvB,uBAAuC;EAAE,OAAO,ENkrB1B,GAAO;;AMjrB7B;;oBAEoC;EAAE,OAAO,EN0b1B,GAAO;;AMzb1B,iBAAiC;EAAE,OAAO,ENkb1B,GAAO;;AMjbvB,qBAAqC;EAAE,OAAO,ENwX1B,GAAO;;AMvX3B,iBAAiC;EAAE,OAAO,ENtD1B,GAAO;;AMuDvB,eAA+B;EAAE,OAAO,ENmnB1B,GAAO;;AMlnBrB;0BAC0C;EAAE,OAAO,EN+a1B,GAAO;;AM9ahC,yBAAyC;EAAE,OAAO,EN8f1B,GAAO;;AM7f/B,yBAAyC;EAAE,OAAO,EN+E1B,GAAO;;AM9E/B,iBAAiC;EAAE,OAAO,ENzB1B,GAAO;;AM0BvB,wBAAwC;EAAE,OAAO,ENmjB1B,GAAO;;AMljB9B,wBAAwC;EAAE,OAAO,ENqL1B,GAAO;;AMpL9B,mBAAmC;EAAE,OAAO,ENlB1B,GAAO;;AMmBzB,eAA+B;EAAE,OAAO,ENsb1B,GAAO;;AMrbrB,gBAAgC;EAAE,OAAO,ENga1B,GAAO;;AM/ZtB,eAA+B;EAAE,OAAO,ENmjB1B,GAAO;;AMljBrB,kBAAkC;EAAE,OAAO,EN+N1B,GAAO;;AM9NxB,uBAAuC;EAAE,OAAO,ENgL1B,GAAO;;AM/K7B,uBAAuC;EAAE,OAAO,EN4iB1B,GAAO;;AM3iB7B,gBAAgC;EAAE,OAAO,EN+I1B,GAAO;;AM9ItB,uBAAuC;EAAE,OAAO,ENyE1B,GAAO;;AMxE7B,wBAAwC;EAAE,OAAO,ENyE1B,GAAO;;AMxE9B,sBAAsC;EAAE,OAAO,ENkb1B,GAAO;;AMjb5B,uBAAuC;EAAE,OAAO,ENuX1B,GAAO;;AMtX7B,uBAAuC;EAAE,OAAO,EN2lB1B,GAAO;;AM1lB7B,uBAAuC;EAAE,OAAO,EN2D1B,GAAO;;AM1D7B,0BAA0C;EAAE,OAAO,ENyb1B,GAAO;;AMxbhC,sBAAsC;EAAE,OAAO,EN0S1B,GAAO;;AMzS5B,qBAAqC;EAAE,OAAO,EN0G1B,GAAO;;AMzG3B,yBAAyC;EAAE,OAAO,ENulB1B,GAAO;;AMtlB/B,yBAAyC;EAAE,OAAO,ENuD1B,GAAO;;AMtD/B,cAA8B;EAAE,OAAO,ENnC1B,GAAO;;AMoCpB,qBAAqC;EAAE,OAAO,ENnD1B,GAAO;;AMoD3B,sBAAsC;EAAE,OAAO,ENnD1B,GAAO;;AMoD5B,mBAAmC;EAAE,OAAO,ENnD1B,GAAO;;AMoDzB,qBAAqC;EAAE,OAAO,ENvD1B,GAAO;;AMwD3B;gBACgC;EAAE,OAAO,EN4d1B,GAAO;;AM3dtB,iBAAiC;EAAE,OAAO,EN8I1B,GAAO;;AM7IvB,mBAAmC;EAAE,OAAO,ENsF1B,GAAO;;AMrFzB,eAA+B;EAAE,OAAO,EN+Z1B,GAAO;;AM9ZrB,gBAAgC;EAAE,OAAO,ENoW1B,GAAO;;AMnWtB,mBAAmC;EAAE,OAAO,ENpD1B,GAAO;;AMqDzB,6BAA6C;EAAE,OAAO,ENuI1B,GAAO;;AMtInC,eAA+B;EAAE,OAAO,ENkN1B,GAAO;;AMjNrB,eAA+B;EAAE,OAAO,EN0S1B,GAAO;;AMzSrB,eAA+B;EAAE,OAAO,EN6K1B,GAAO;;AM5KrB,cAA8B;EAAE,OAAO,ENyI1B,GAAO;;AMxIpB,oBAAoC;EAAE,OAAO,ENyI1B,GAAO;;AMxI1B;+BAC+C;EAAE,OAAO,ENiI1B,GAAO;;AMhIrC,gBAAgC;EAAE,OAAO,EN+Y1B,GAAO;;AM9YtB,mBAAmC;EAAE,OAAO,ENA1B,GAAO;;AMCzB,iBAAiC;EAAE,OAAO,ENoa1B,GAAO;;AMnavB,kBAAkC;EAAE,OAAO,ENgE1B,GAAO;;AM/DxB,iBAAiC;EAAE,OAAO,EN6T1B,GAAO;;AM5TvB,qBAAqC;EAAE,OAAO,ENuC1B,GAAO;;AMtC3B,uBAAuC;EAAE,OAAO,ENmC1B,GAAO;;AMlC7B,kBAAkC;EAAE,OAAO,EN+a1B,GAAO;;AM9axB,wBAAwC;EAAE,OAAO,ENkd1B,GAAO;;AMjd9B,iBAAiC;EAAE,OAAO,EN0K1B,GAAO;;AMzKvB,sBAAsC;EAAE,OAAO,EN2K1B,GAAO;;AM1K5B,mBAAmC;EAAE,OAAO,EN3E1B,GAAO;;AM4EzB,mBAAmC;EAAE,OAAO,EN7E1B,GAAO;;AM8EzB;oBACoC;EAAE,OAAO,ENlE1B,GAAO;;AMmE1B,yBAAyC;EAAE,OAAO,EN+kB1B,GAAO;;AM9kB/B,0BAA0C;EAAE,OAAO,EN4H1B,GAAO;;AM3HhC,uBAAuC;EAAE,OAAO,ENT1B,GAAO;;AMU7B,cAA8B;EAAE,OAAO,EN2Q1B,GAAO;;AM1QpB;eAC+B;EAAE,OAAO,EN6C1B,GAAO;;AM5CrB,mBAAmC;EAAE,OAAO,ENkD1B,GAAO;;AMjDzB,sBAAsC;EAAE,OAAO,ENsiB1B,GAAO;;AMriB5B,wBAAwC;EAAE,OAAO,ENoiB1B,GAAO;;AMniB9B,oBAAoC;EAAE,OAAO,EN2e1B,GAAO;;AM1e1B,kBAAkC;EAAE,OAAO,EN8N1B,GAAO;;AM7NxB,mBAAmC;EAAE,OAAO,ENoc1B,GAAO;;AMnczB,0BAA0C;EAAE,OAAO,ENuR1B,GAAO;;AMtRhC,qBAAqC;EAAE,OAAO,EN6hB1B,GAAO;;AM5hB3B,wBAAwC;EAAE,OAAO,ENsG1B,GAAO;;AMrG9B,kBAAkC;EAAE,OAAO,EN8b1B,GAAO;;AM7bxB,iBAAiC;EAAE,OAAO,ENqjB1B,GAAO;;AMpjBvB,wBAAwC;EAAE,OAAO,ENgL1B,GAAO;;AM/K9B,iBAAiC;EAAE,OAAO,ENukB1B,GAAO;;AMtkBvB,kBAAkC;EAAE,OAAO,ENqQ1B,GAAO;;AMpQxB,gBAAgC;EAAE,OAAO,ENiW1B,GAAO;;AMhWtB,mBAAmC;EAAE,OAAO,EN2d1B,GAAO;;AM1dzB,qBAAqC;EAAE,OAAO,ENjD1B,GAAO;;AMkD3B,uBAAuC;EAAE,OAAO,EN+V1B,GAAO;;AM9V7B,kBAAkC;EAAE,OAAO,ENsjB1B,GAAO;;AMrjBxB;mBACmC;EAAE,OAAO,ENgG1B,GAAO;;AM/FzB,iBAAiC;EAAE,OAAO,ENoK1B,GAAO;;AMnKvB,iBAAiC;EAAE,OAAO,EN0jB1B,GAAO;;AMzjBvB,sBAAsC;EAAE,OAAO,ENoC1B,GAAO;;AMnC5B;cAC8B;EAAE,OAAO,EN+Y1B,GAAO;;AM9YpB,gBAAgC;EAAE,OAAO,ENoM1B,GAAO;;AMnMtB,mBAAmC;EAAE,OAAO,ENrD1B,GAAO;;AMsDzB,eAA+B;EAAE,OAAO,ENhF1B,GAAO;;AMiFrB,sBAAsC;EAAE,OAAO,ENrB1B,GAAO;;AMsB5B,uBAAuC;EAAE,OAAO,ENoL1B,GAAO;;AMnL7B,sBAAsC;EAAE,OAAO,ENkL1B,GAAO;;AMjL5B,oBAAoC;EAAE,OAAO,ENmL1B,GAAO;;AMlL1B,sBAAsC;EAAE,OAAO,EN+K1B,GAAO;;AM9K5B,4BAA4C;EAAE,OAAO,ENrI1B,GAAO;;AMsIlC,6BAA6C;EAAE,OAAO,ENjI1B,GAAO;;AMkInC,0BAA0C;EAAE,OAAO,ENjI1B,GAAO;;AMkIhC,4BAA4C;EAAE,OAAO,ENzI1B,GAAO;;AM0IlC,gBAAgC;EAAE,OAAO,EN2J1B,GAAO;;AM1JtB,iBAAiC;EAAE,OAAO,EN6lB1B,GAAO;;AM5lBvB,gBAAgC;EAAE,OAAO,ENqe1B,GAAO;;AMpetB,iBAAiC;EAAE,OAAO,ENyG1B,GAAO;;AMxGvB,oBAAoC;EAAE,OAAO,ENzE1B,GAAO;;AM0E1B,qBAAqC;EAAE,OAAO,ENlI1B,GAAO;;AMmI3B;gBACgC;EAAE,OAAO,ENijB1B,GAAO;;AMhjBtB;eAC+B;EAAE,OAAO,EN4O1B,GAAO;;AM3OrB,gBAAgC;EAAE,OAAO,ENd1B,GAAO;;AMetB,gBAAgC;EAAE,OAAO,EN0G1B,GAAO;;AMzGtB;mBACmC;EAAE,OAAO,EN6X1B,GAAO;;AM5XzB;kBACkC;EAAE,OAAO,EN2F1B,GAAO;;AM1FxB,oBAAoC;EAAE,OAAO,EN6S1B,GAAO;;AM5S1B;mBACmC;EAAE,OAAO,ENqG1B,GAAO;;AMpGzB,iBAAiC;EAAE,OAAO,ENgb1B,GAAO;;AM/avB;;eAE+B;EAAE,OAAO,ENlI1B,GAAO;;AMmIrB,kBAAkC;EAAE,OAAO,ENsO1B,GAAO;;AMrOxB,kBAAkC;EAAE,OAAO,ENoO1B,GAAO;;AMnOxB,wBAAwC;EAAE,OAAO,EN+b1B,GAAO;;AM9b9B,oBAAoC;EAAE,OAAO,EN2gB1B,GAAO;;AM1gB1B,gBAAgC;EAAE,OAAO,ENuc1B,GAAO;;AMtctB,gBAAgC;EAAE,OAAO,ENyO1B,GAAO;;AMxOtB,gBAAgC;EAAE,OAAO,EN6f1B,GAAO;;AM5ftB,oBAAoC;EAAE,OAAO,ENmT1B,GAAO;;AMlT1B,2BAA2C;EAAE,OAAO,ENoT1B,GAAO;;AMnTjC,6BAA6C;EAAE,OAAO,ENgI1B,GAAO;;AM/HnC,sBAAsC;EAAE,OAAO,EN4H1B,GAAO;;AM3H5B,gBAAgC;EAAE,OAAO,ENqQ1B,GAAO;;AMpQtB,qBAAqC;EAAE,OAAO,ENpF1B,GAAO;;AMqF3B,mBAAmC;EAAE,OAAO,EN9E1B,GAAO;;AM+EzB,qBAAqC;EAAE,OAAO,ENrF1B,GAAO;;AMsF3B,sBAAsC;EAAE,OAAO,ENrF1B,GAAO;;AMsF5B,kBAAkC;EAAE,OAAO,ENhC1B,GAAO;;AMiCxB;eAC+B;EAAE,OAAO,EN0Y1B,GAAO;;AMzYrB;oBACoC;EAAE,OAAO,EN8Y1B,GAAO;;AM7Y1B;mBACmC;EAAE,OAAO,EN2Y1B,GAAO;;AM1YzB,mBAAmC;EAAE,OAAO,ENU1B,GAAO;;AMTzB,mBAAmC;EAAE,OAAO,ENuM1B,GAAO;;AMtMzB;eAC+B;EAAE,OAAO,ENqf1B,GAAO;;AMpfrB;gBACgC;EAAE,OAAO,ENoF1B,GAAO;;AMnFtB;qBACqC;EAAE,OAAO,EN+a1B,GAAO;;AM9a3B,oBAAoC;EAAE,OAAO,EN7C1B,GAAO;;AM8C1B,qBAAqC;EAAE,OAAO,EN1C1B,GAAO;;AM2C3B;eAC+B;EAAE,OAAO,ENpI1B,GAAO;;AMqIrB,kBAAkC;EAAE,OAAO,EN6W1B,GAAO;;AM5WxB,mBAAmC;EAAE,OAAO,ENye1B,GAAO;;AMxezB;oBACoC;EAAE,OAAO,ENrE1B,GAAO;;AMsE1B,sBAAsC;EAAE,OAAO,ENqL1B,GAAO;;AMpL5B,mBAAmC;EAAE,OAAO,ENG1B,GAAO;;AMFzB,yBAAyC;EAAE,OAAO,ENnE1B,GAAO;;AMoE/B,uBAAuC;EAAE,OAAO,ENnE1B,GAAO;;AMoE7B,kBAAkC;EAAE,OAAO,ENif1B,GAAO;;AMhfxB,sBAAsC;EAAE,OAAO,EN8Y1B,GAAO;;AM7Y5B,mBAAmC;EAAE,OAAO,ENyZ1B,GAAO;;AMxZzB,iBAAiC;EAAE,OAAO,EN9J1B,GAAO;;AM+JvB,iBAAiC;EAAE,OAAO,ENlE1B,GAAO;;AMmEvB,kBAAkC;EAAE,OAAO,EN1C1B,GAAO;;AM2CxB,sBAAsC;EAAE,OAAO,EN8B1B,GAAO;;AM7B5B,qBAAqC;EAAE,OAAO,EN1I1B,GAAO;;AM2I3B,qBAAqC;EAAE,OAAO,ENsH1B,GAAO;;AMrH3B,oBAAoC;EAAE,OAAO,ENrO1B,GAAO;;AMsO1B,iBAAiC;EAAE,OAAO,EN4M1B,GAAO;;AM3MvB,sBAAsC;EAAE,OAAO,ENU1B,GAAO;;AMT5B,eAA+B;EAAE,OAAO,EN3K1B,GAAO;;AM4KrB,mBAAmC;EAAE,OAAO,ENuF1B,GAAO;;AMtFzB,sBAAsC;EAAE,OAAO,EN2Q1B,GAAO;;AM1Q5B,4BAA4C;EAAE,OAAO,ENrO1B,GAAO;;AMsOlC,6BAA6C;EAAE,OAAO,ENrO1B,GAAO;;AMsOnC,0BAA0C;EAAE,OAAO,ENrO1B,GAAO;;AMsOhC,4BAA4C;EAAE,OAAO,ENzO1B,GAAO;;AM0OlC,qBAAqC;EAAE,OAAO,ENrO1B,GAAO;;AMsO3B,sBAAsC;EAAE,OAAO,ENrO1B,GAAO;;AMsO5B,mBAAmC;EAAE,OAAO,ENrO1B,GAAO;;AMsOzB,qBAAqC;EAAE,OAAO,ENzO1B,GAAO;;AM0O3B,kBAAkC;EAAE,OAAO,ENpD1B,GAAO;;AMqDxB,iBAAiC;EAAE,OAAO,EN4I1B,GAAO;;AM3IvB,iBAAiC;EAAE,OAAO,ENwY1B,GAAO;;AMvYvB;iBACiC;EAAE,OAAO,ENuM1B,GAAO;;AMtMvB,mBAAmC;EAAE,OAAO,ENzG1B,GAAO;;AM0GzB,qBAAqC;EAAE,OAAO,ENyQ1B,GAAO;;AMxQ3B,sBAAsC;EAAE,OAAO,ENyQ1B,GAAO;;AMxQ5B,kBAAkC;EAAE,OAAO,EN+V1B,GAAO;;AM9VxB,iBAAiC;EAAE,OAAO,EN9G1B,GAAO;;AM+GvB;gBACgC;EAAE,OAAO,ENoR1B,GAAO;;AMnRtB,qBAAqC;EAAE,OAAO,EN+C1B,GAAO;;AM9C3B,mBAAmC;EAAE,OAAO,ENmB1B,GAAO;;AMlBzB,wBAAwC;EAAE,OAAO,ENoB1B,GAAO;;AMnB9B,kBAAkC;EAAE,OAAO,ENqU1B,GAAO;;AMpUxB,kBAAkC;EAAE,OAAO,EN2B1B,GAAO;;AM1BxB,gBAAgC;EAAE,OAAO,ENgL1B,GAAO;;AM/KtB,kBAAkC;EAAE,OAAO,EN2B1B,GAAO;;AM1BxB,qBAAqC;EAAE,OAAO,ENuH1B,GAAO;;AMtH3B,iBAAiC;EAAE,OAAO,ENM1B,GAAO;;AMLvB,yBAAyC;EAAE,OAAO,ENI1B,GAAO;;AMH/B,mBAAmC;EAAE,OAAO,EN6X1B,GAAO;;AM5XzB,eAA+B;EAAE,OAAO,ENhH1B,GAAO;;AMiHrB;oBACoC;EAAE,OAAO,ENuQ1B,GAAO;;AMtQ1B;;sBAEsC;EAAE,OAAO,ENsV1B,GAAO;;AMrV5B,yBAAyC;EAAE,OAAO,ENwI1B,GAAO;;AMvI/B,eAA+B;EAAE,OAAO,ENhG1B,GAAO;;AMiGrB,oBAAoC;EAAE,OAAO,ENvH1B,GAAO;;AMwH1B;uBACuC;EAAE,OAAO,ENtJ1B,GAAO;;AMuJ7B,mBAAmC;EAAE,OAAO,ENyO1B,GAAO;;AMxOzB,eAA+B;EAAE,OAAO,EN0F1B,GAAO;;AMzFrB,sBAAsC;EAAE,OAAO,EN1D1B,GAAO;;AM2D5B,sBAAsC;EAAE,OAAO,ENkW1B,GAAO;;AMjW5B,oBAAoC;EAAE,OAAO,EN4V1B,GAAO;;AM3V1B,iBAAiC;EAAE,OAAO,ENlE1B,GAAO;;AMmEvB,uBAAuC;EAAE,OAAO,ENgO1B,GAAO;;AM/N7B,qBAAqC;EAAE,OAAO,EN2J1B,GAAO;;AM1J3B,2BAA2C;EAAE,OAAO,EN2J1B,GAAO;;AM1JjC,iBAAiC;EAAE,OAAO,ENsR1B,GAAO;;AMrRvB,qBAAqC;EAAE,OAAO,EN5L1B,GAAO;;AM6L3B,4BAA4C;EAAE,OAAO,ENxB1B,GAAO;;AMyBlC,iBAAiC;EAAE,OAAO,ENuP1B,GAAO;;AMtPvB,iBAAiC;EAAE,OAAO,EN6I1B,GAAO;;AM5IvB,8BAA8C;EAAE,OAAO,EN9J1B,GAAO;;AM+JpC,+BAA+C;EAAE,OAAO,EN9J1B,GAAO;;AM+JrC,4BAA4C;EAAE,OAAO,EN9J1B,GAAO;;AM+JlC,8BAA8C;EAAE,OAAO,ENlK1B,GAAO;;AMmKpC,gBAAgC;EAAE,OAAO,EN8D1B,GAAO;;AM7DtB,eAA+B;EAAE,OAAO,ENrH1B,GAAO;;AMsHrB,iBAAiC;EAAE,OAAO,ENvS1B,GAAO;;AMwSvB,qBAAqC;EAAE,OAAO,EN2Z1B,GAAO;;AM1Z3B,mBAAmC;EAAE,OAAO,ENhN1B,GAAO;;AMiNzB,qBAAqC;EAAE,OAAO,EN7F1B,GAAO;;AM8F3B,qBAAqC;EAAE,OAAO,EN7F1B,GAAO;;AM8F3B,qBAAqC;EAAE,OAAO,EN+O1B,GAAO;;AM9O3B,sBAAsC;EAAE,OAAO,ENiM1B,GAAO;;AMhM5B,iBAAiC;EAAE,OAAO,EN6W1B,GAAO;;AM5WvB,uBAAuC;EAAE,OAAO,EN0I1B,GAAO;;AMzI7B,yBAAyC;EAAE,OAAO,EN0I1B,GAAO;;AMzI/B,mBAAmC;EAAE,OAAO,ENqF1B,GAAO;;AMpFzB,qBAAqC;EAAE,OAAO,ENmF1B,GAAO;;AMlF3B,uBAAuC;EAAE,OAAO,ENnL1B,GAAO;;AMoL7B,wBAAwC;EAAE,OAAO,EN0K1B,GAAO;;AMzK9B,+BAA+C;EAAE,OAAO,ENpF1B,GAAO;;AMqFrC,uBAAuC;EAAE,OAAO,ENwP1B,GAAO;;AMvP7B,kBAAkC;EAAE,OAAO,ENjJ1B,GAAO;;AMkJxB;8BAC8C;EAAE,OAAO,EN/M1B,GAAO;;AMgNpC;4BAC4C;EAAE,OAAO,EN9M1B,GAAO;;AM+MlC;+BAC+C;EAAE,OAAO,ENjN1B,GAAO;;AMkNrC;cAC8B;EAAE,OAAO,ENvG1B,GAAO;;AMwGpB,cAA8B;EAAE,OAAO,ENhC1B,GAAO;;AMiCpB;cAC8B;EAAE,OAAO,ENqY1B,GAAO;;AMpYpB;cAC8B;EAAE,OAAO,EN4C1B,GAAO;;AM3CpB;;;cAG8B;EAAE,OAAO,ENgD1B,GAAO;;AM/CpB;;cAE8B;EAAE,OAAO,ENiN1B,GAAO;;AMhNpB;cAC8B;EAAE,OAAO,EN+C1B,GAAO;;AM9CpB;cAC8B;EAAE,OAAO,EN3P1B,GAAO;;AM4PpB,eAA+B;EAAE,OAAO,ENhG1B,GAAO;;AMiGrB,oBAAoC;EAAE,OAAO,ENpF1B,GAAO;;AMqF1B,yBAAyC;EAAE,OAAO,EN0P1B,GAAO;;AMzP/B,0BAA0C;EAAE,OAAO,EN0P1B,GAAO;;AMzPhC,0BAA0C;EAAE,OAAO,EN0P1B,GAAO;;AMzPhC,2BAA2C;EAAE,OAAO,EN0P1B,GAAO;;AMzPjC,2BAA2C;EAAE,OAAO,EN6P1B,GAAO;;AM5PjC,4BAA4C;EAAE,OAAO,EN6P1B,GAAO;;AM5PlC,oBAAoC;EAAE,OAAO,ENkU1B,GAAO;;AMjU1B,sBAAsC;EAAE,OAAO,EN8T1B,GAAO;;AM7T5B,yBAAyC;EAAE,OAAO,ENya1B,GAAO;;AMxa/B,kBAAkC;EAAE,OAAO,ENsa1B,GAAO;;AMraxB,eAA+B;EAAE,OAAO,EN2Z1B,GAAO;;AM1ZrB,sBAAsC;EAAE,OAAO,EN2Z1B,GAAO;;AM1Z5B,uBAAuC;EAAE,OAAO,ENoa1B,GAAO;;AMna7B,kBAAkC;EAAE,OAAO,ENxJ1B,GAAO;;AMyJxB,yBAAyC;EAAE,OAAO,EN8P1B,GAAO;;AM7P/B,oBAAoC;EAAE,OAAO,ENgB1B,GAAO;;AMf1B,iBAAiC;EAAE,OAAO,ENpF1B,GAAO;;AMqFvB,cAA8B;EAAE,OAAO,EN3W1B,GAAO;;AM4WpB,oBAAoC;EAAE,OAAO,EN/R1B,GAAO;;AMgS1B,2BAA2C;EAAE,OAAO,EN/R1B,GAAO;;AMgSjC,iBAAiC;EAAE,OAAO,EN+U1B,GAAO;;AM9UvB,wBAAwC;EAAE,OAAO,EN+U1B,GAAO;;AM9U9B,0BAA0C;EAAE,OAAO,ENgD1B,GAAO;;AM/ChC,wBAAwC;EAAE,OAAO,ENkD1B,GAAO;;AMjD9B,0BAA0C;EAAE,OAAO,EN+C1B,GAAO;;AM9ChC,2BAA2C;EAAE,OAAO,EN+C1B,GAAO;;AM9CjC,gBAAgC;EAAE,OAAO,ENjW1B,GAAO;;AMkWtB,kBAAkC;EAAE,OAAO,ENmY1B,GAAO;;AMlYxB,kBAAkC;EAAE,OAAO,EN7W1B,GAAO;;AM8WxB,gBAAgC;EAAE,OAAO,ENkC1B,GAAO;;AMjCtB,mBAAmC;EAAE,OAAO,EN5K1B,GAAO;;AM6KzB,gBAAgC;EAAE,OAAO,ENgN1B,GAAO;;AM/MtB,qBAAqC;EAAE,OAAO,ENxF1B,GAAO;;AMyF3B,iBAAiC;EAAE,OAAO,EN4T1B,GAAO;;AM3TvB,iBAAiC;EAAE,OAAO,ENtI1B,GAAO;;AMuIvB,eAA+B;EAAE,OAAO,EN6C1B,GAAO;;AM5CrB;mBACmC;EAAE,OAAO,EN5D1B,GAAO;;AM6DzB,gBAAgC;EAAE,OAAO,EN8P1B,GAAO;;AM7PtB,iBAAiC;EAAE,OAAO,ENuE1B,GAAO;;AMtEvB,kBAAkC;EAAE,OAAO,EN9W1B,GAAO;;AM+WxB,cAA8B;EAAE,OAAO,ENtS1B,GAAO;;AMuSpB,aAA6B;EAAE,OAAO,ENiW1B,GAAO;;AMhWnB,gBAAgC;EAAE,OAAO,ENuW1B,GAAO;;AMtWtB,iBAAiC;EAAE,OAAO,EN+I1B,GAAO;;AM9IvB,oBAAoC;EAAE,OAAO,ENkF1B,GAAO;;AMjF1B,yBAAyC;EAAE,OAAO,EN6N1B,GAAO;;AM5N/B,+BAA+C;EAAE,OAAO,EN/W1B,GAAO;;AMgXrC,8BAA8C;EAAE,OAAO,ENjX1B,GAAO;;AMkXpC;8BAC8C;EAAE,OAAO,ENzR1B,GAAO;;AM0RpC,uBAAuC;EAAE,OAAO,ENnM1B,GAAO;;AMoM7B,qBAAqC;EAAE,OAAO,ENiW1B,GAAO;;AMhW3B,uBAAuC;EAAE,OAAO,ENoV1B,GAAO;;AMnV7B;cAC8B;EAAE,OAAO,EN0S1B,GAAO;;AMzSpB,wBAAwC;EAAE,OAAO,EN0G1B,GAAO;;AMzG9B,wBAAwC;EAAE,OAAO,EN4M1B,GAAO;;AM3M9B,gBAAgC;EAAE,OAAO,ENsL1B,GAAO;;AMrLtB,0BAA0C;EAAE,OAAO,ENzL1B,GAAO;;AM0LhC,oBAAoC;EAAE,OAAO,ENoW1B,GAAO;;AMnW1B,iBAAiC;EAAE,OAAO,EN8D1B,GAAO;;AM7DvB;;qBAEqC;EAAE,OAAO,EN8S1B,GAAO;;AM7S3B;yBACyC;EAAE,OAAO,EN1F1B,GAAO;;AM2F/B,gBAAgC;EAAE,OAAO,ENsW1B,GAAO;;AMrWtB,iBAAiC;EAAE,OAAO,ENlG1B,GAAO;;AMmGvB,iBAAiC;EAAE,OAAO,ENgH1B,GAAO;;AM/GvB,wBAAwC;EAAE,OAAO,ENiH1B,GAAO;;AMhH9B,6BAA6C;EAAE,OAAO,ENyN1B,GAAO;;AMxNnC,sBAAsC;EAAE,OAAO,ENuN1B,GAAO;;AMtN5B,oBAAoC;EAAE,OAAO,EN/N1B,GAAO;;AMgO1B,eAA+B;EAAE,OAAO,EN5N1B,GAAO;;AM6NrB,wBAAwC;EAAE,OAAO,EN2E1B,GAAO;;AM1E9B,yBAAyC;EAAE,OAAO,ENyE1B,GAAO;;AMxE/B,iBAAiC;EAAE,OAAO,ENvN1B,GAAO;;AMwNvB,iBAAiC;EAAE,OAAO,ENzC1B,GAAO;;AM0CvB,mBAAmC;EAAE,OAAO,ENpC1B,GAAO;;AMqCzB,cAA8B;EAAE,OAAO,ENtL1B,GAAO;;AMuLpB,mBAAmC;EAAE,OAAO,EN7U1B,GAAO;;AM8UzB,gBAAgC;EAAE,OAAO,EN1R1B,GAAO;;AM2RtB,cAA8B;EAAE,OAAO,ENsD1B,GAAO;;AMrDpB,gBAAgC;EAAE,OAAO,ENmL1B,GAAO;;AMlLtB,eAA+B;EAAE,OAAO,ENrP1B,GAAO;;AMsPrB,gBAAgC;EAAE,OAAO,ENrP1B,GAAO;;AMsPtB,kBAAkC;EAAE,OAAO,EN7W1B,GAAO;;AM8WxB,yBAAyC;EAAE,OAAO,EN7W1B,GAAO;;AM8W/B,gBAAgC;EAAE,OAAO,EN0L1B,GAAO;;AMzLtB,uBAAuC;EAAE,OAAO,EN0L1B,GAAO;;AMzL7B,kBAAkC;EAAE,OAAO,ENyF1B,GAAO;;AMxFxB;cAC8B;EAAE,OAAO,ENzU1B,GAAO;;AM0UpB;eAC+B;EAAE,OAAO,EN+M1B,GAAO;;AM9MrB,eAA+B;EAAE,OAAO,EN4P1B,GAAO;;AM3PrB,kBAAkC;EAAE,OAAO,ENuK1B,GAAO;;AMtKxB,qBAAqC;EAAE,OAAO,ENtP1B,GAAO;;AMuP3B,qBAAqC;EAAE,OAAO,ENiK1B,GAAO;;AMhK3B,mBAAmC;EAAE,OAAO,EN9P1B,GAAO;;AM+PzB,qBAAqC;EAAE,OAAO,EN/L1B,GAAO;;AMgM3B,sBAAsC;EAAE,OAAO,ENxL1B,GAAO;;AMyL5B,uBAAuC;EAAE,OAAO,ENrM1B,GAAO;;AMsM7B,4BAA4C;EAAE,OAAO,EN/L1B,GAAO;;AMgMlC;;uBAEuC;EAAE,OAAO,ENxM1B,GAAO;;AMyM7B;yBACyC;EAAE,OAAO,EN9M1B,GAAO;;AM+M/B;uBACuC;EAAE,OAAO,EN/M1B,GAAO;;AMgN7B;uBACuC;EAAE,OAAO,ENpM1B,GAAO;;AMqM7B,sBAAsC;EAAE,OAAO,ENjN1B,GAAO;;AMkN5B,eAA+B;EAAE,OAAO,ENuR1B,GAAO;;AMtRrB,kBAAkC;EAAE,OAAO,EN5S1B,GAAO;;AM6SxB,mBAAmC;EAAE,OAAO,EN9E1B,GAAO;;AM+EzB;;;;oBAIoC;EAAE,OAAO,ENnE1B,GAAO;;AMoE1B,yBAAyC;EAAE,OAAO,EN/T1B,GAAO;;AMgU/B;;gBAEgC;EAAE,OAAO,ENqD1B,GAAO;;AMpDtB;iBACiC;EAAE,OAAO,ENnQ1B,GAAO;;AMoQvB,qBAAqC;EAAE,OAAO,ENzK1B,GAAO;;AM0K3B,cAA8B;EAAE,OAAO,EN3K1B,GAAO;;AM4KpB;;sBAEsC;EAAE,OAAO,ENxJ1B,GAAO;;AMyJ5B,wBAAwC;EAAE,OAAO,EN2K1B,GAAO;;AM1K9B,aAA6B;EAAE,OAAO,ENiC1B,GAAO;;AMhCnB;iBACiC;EAAE,OAAO,EN0Q1B,GAAO;;AMzQvB;sBACsC;EAAE,OAAO,ENV1B,GAAO;;AMW5B;wBACwC;EAAE,OAAO,ENX1B,GAAO;;AMY9B,kBAAkC;EAAE,OAAO,EN1I1B,GAAO;;AM2IxB,sBAAsC;EAAE,OAAO,ENlV1B,GAAO;;AMmV5B,iBAAiC;EAAE,OAAO,ENjJ1B,GAAO;;AMkJvB,oBAAoC;EAAE,OAAO,ENb1B,GAAO;;AMc1B,kBAAkC;EAAE,OAAO,EN+F1B,GAAO;;AM9FxB,oBAAoC;EAAE,OAAO,ENuE1B,GAAO;;AMtE1B,2BAA2C;EAAE,OAAO,ENuE1B,GAAO;;AMtEjC,eAA+B;EAAE,OAAO,ENzZ1B,GAAO;;AM0ZrB;mBACmC;EAAE,OAAO,EN5M1B,GAAO;;AM6MzB,cAA8B;EAAE,OAAO,EN0M1B,GAAO;;AMzMpB,qBAAqC;EAAE,OAAO,ENxa1B,GAAO;;AMya3B,eAA+B;EAAE,OAAO,ENI1B,GAAO;;AMHrB,qBAAqC;EAAE,OAAO,ENuF1B,GAAO;;AMtF3B,iBAAiC;EAAE,OAAO,EN2M1B,GAAO;;AM1MvB,eAA+B;EAAE,OAAO,EN+Q1B,GAAO;;AM9QrB,sBAAsC;EAAE,OAAO,ENzC1B,GAAO;;AM0C5B,eAA+B;EAAE,OAAO,ENwP1B,GAAO;;AMvPrB,qBAAqC;EAAE,OAAO,ENrZ1B,GAAO;;AMsZ3B,iBAAiC;EAAE,OAAO,ENvB1B,GAAO;;AMwBvB,wBAAwC;EAAE,OAAO,EN3L1B,GAAO;;AM4L9B,kBAAkC;EAAE,OAAO,EN5X1B,GAAO;;AM6XxB,wBAAwC;EAAE,OAAO,ENhY1B,GAAO;;AMiY9B,sBAAsC;EAAE,OAAO,ENnY1B,GAAO;;AMoY5B,kBAAkC;EAAE,OAAO,ENtY1B,GAAO;;AMuYxB,oBAAoC;EAAE,OAAO,ENlY1B,GAAO;;AMmY1B,oBAAoC;EAAE,OAAO,ENlY1B,GAAO;;AMmY1B,qBAAqC;EAAE,OAAO,EN3b1B,GAAO;;AM4b3B,uBAAuC;EAAE,OAAO,EN3b1B,GAAO;;AM4b7B,gBAAgC;EAAE,OAAO,EN+K1B,GAAO;;AM9KtB,oBAAoC;EAAE,OAAO,ENnV1B,GAAO;;AMoV1B,aAA6B;EAAE,OAAO,EN9d1B,GAAO;;AM+dnB,qBAAqC;EAAE,OAAO,EN5R1B,GAAO;;AM6R3B,sBAAsC;EAAE,OAAO,EN/C1B,GAAO;;AMgD5B,wBAAwC;EAAE,OAAO,EN9b1B,GAAO;;AM+b9B,qBAAqC;EAAE,OAAO,ENtf1B,GAAO;;AMuf3B,oBAAoC;EAAE,OAAO,EN/B1B,GAAO;;AMgC1B,qBAAqC;EAAE,OAAO,ENzH1B,GAAO;;AM0H3B,iBAAiC;EAAE,OAAO,ENvI1B,GAAO;;AMwIvB,wBAAwC;EAAE,OAAO,ENvI1B,GAAO;;AMwI9B,qBAAqC;EAAE,OAAO,EN4J1B,GAAO;;AM3J3B,oBAAoC;EAAE,OAAO,EN4J1B,GAAO;;AM3J1B,kBAAkC;EAAE,OAAO,ENxc1B,GAAO;;AMycxB,cAA8B;EAAE,OAAO,ENjb1B,GAAO;;AMkbpB,kBAAkC;EAAE,OAAO,ENvJ1B,GAAO;;AMwJxB,oBAAoC;EAAE,OAAO,EN3gB1B,GAAO;;AM4gB1B,aAA6B;EAAE,OAAO,EN7Z1B,GAAO;;AM8ZnB;;cAE8B;EAAE,OAAO,ENzK1B,GAAO;;AM0KpB,mBAAmC;EAAE,OAAO,ENpG1B,GAAO;;AMqGzB,qBAAqC;EAAE,OAAO,ENxb1B,GAAO;;AMyb3B,yBAAyC;EAAE,OAAO,EN5W1B,GAAO;;AM6W/B,mBAAmC;EAAE,OAAO,EN9V1B,GAAO;;AM+VzB,mBAAmC;EAAE,OAAO,EN9P1B,GAAO;;AM+PzB,kBAAkC;EAAE,OAAO,ENrJ1B,GAAO;;AMsJxB,iBAAiC;EAAE,OAAO,ENe1B,GAAO;;AMdvB,uBAAuC;EAAE,OAAO,EN2B1B,GAAO;;AM1B7B,sBAAsC;EAAE,OAAO,ENoC1B,GAAO;;AMnC5B,mBAAmC;EAAE,OAAO,ENqC1B,GAAO;;AMpCzB,oBAAoC;EAAE,OAAO,EN5a1B,GAAO;;AM6a1B,0BAA0C;EAAE,OAAO,EN9a1B,GAAO;;AM+ahC,kBAAkC;EAAE,OAAO,EN/V1B,GAAO;;AMgWxB,eAA+B;EAAE,OAAO,ENoB1B,GAAO;;AMnBrB,sBAAsC;EAAE,OAAO,EN8K1B,GAAO;;AM7K5B,qBAAqC;EAAE,OAAO,EN/F1B,GAAO;;AMgG3B,sBAAsC;EAAE,OAAO,EN6E1B,GAAO;;AM5E5B,oBAAoC;EAAE,OAAO,EN9M1B,GAAO;;AM+M1B,gBAAgC;EAAE,OAAO,EN+K1B,GAAO;;AM9KtB,eAA+B;EAAE,OAAO,EN7H1B,GAAO;;AM8HrB,kBAAkC;EAAE,OAAO,ENnH1B,GAAO;;AMoHxB;sBACsC;EAAE,OAAO,ENkI1B,GAAO;;AMjI5B,0BAA0C;EAAE,OAAO,ENkI1B,GAAO;;AMjIhC,uBAAuC;EAAE,OAAO,EN0K1B,GAAO;;AMzK7B,sBAAsC;EAAE,OAAO,ENlI1B,GAAO;;AMmI5B,qBAAqC;EAAE,OAAO,ENyK1B,GAAO;;AMxK3B,sBAAsC;EAAE,OAAO,ENnI1B,GAAO;;AMoI5B,wBAAwC;EAAE,OAAO,ENlI1B,GAAO;;AMmI9B,wBAAwC;EAAE,OAAO,ENpI1B,GAAO;;AMqI9B,iBAAiC;EAAE,OAAO,EN1G1B,GAAO;;AM2GvB,qBAAqC;EAAE,OAAO,EN7Q1B,GAAO;;AM8Q3B,4BAA4C;EAAE,OAAO,EN1U1B,GAAO;;AM2UlC,sBAAsC;EAAE,OAAO,ENzE1B,GAAO;;AM0E5B,mBAAmC;EAAE,OAAO,ENkL1B,GAAO;;AMjLzB,iBAAiC;EAAE,OAAO,ENX1B,GAAO;;AMYvB,oBAAoC;EAAE,OAAO,ENuJ1B,GAAO;;AMtJ1B,qBAAqC;EAAE,OAAO,ENwJ1B,GAAO;;AMvJ3B;cAC8B;EAAE,OAAO,EN/f1B,GAAO;;AMggBpB,kBAAkC;EAAE,OAAO,EN4J1B,GAAO;;AM3JxB,gBAAgC;EAAE,OAAO,EN8G1B,GAAO;;AM7GtB,iBAAiC;EAAE,OAAO,ENwD1B,GAAO;;AMvDvB,iBAAiC;EAAE,OAAO,EN9I1B,GAAO;;AM+IvB;uBACuC;EAAE,OAAO,EN0L1B,GAAO;;AMzL7B,wBAAwC;EAAE,OAAO,ENjH1B,GAAO;;AMkH9B,mBAAmC;EAAE,OAAO,ENrH1B,GAAO;;AMsHzB,uBAAuC;EAAE,OAAO,ENnW1B,GAAO;;AMoW7B;;uBAEuC;EAAE,OAAO,EN/gB1B,GAAO;;AMghB7B;iCACiD;EAAE,OAAO,EN9gB1B,GAAO;;AM+gBvC;uBACuC;EAAE,OAAO,ENlhB1B,GAAO;;AMmhB7B;0BAC0C;EAAE,OAAO,ENnhB1B,GAAO;;AMohBhC;wBACwC;EAAE,OAAO,ENxhB1B,GAAO;;AMyhB9B,wBAAwC;EAAE,OAAO,EN3I1B,GAAO;;AM4I9B,mBAAmC;EAAE,OAAO,EN3O1B,GAAO;;AM4OzB,uBAAuC;EAAE,OAAO,ENxI1B,GAAO;;AMyI7B,yBAAyC;EAAE,OAAO,ENxI1B,GAAO;;AMyI/B,sBAAsC;EAAE,OAAO,ENwB1B,GAAO;;AMvB5B,wBAAwC;EAAE,OAAO,ENwB1B,GAAO;;AMvB9B,iBAAiC;EAAE,OAAO,EN/d1B,GAAO;;AMgevB,yBAAyC;EAAE,OAAO,ENle1B,GAAO;;AMme/B,gBAAgC;EAAE,OAAO,ENpc1B,GAAO;;AMqctB,wBAAwC;EAAE,OAAO,ENljB1B,GAAO;;AMmjB9B,sBAAsC;EAAE,OAAO,ENxP1B,GAAO;;AMyP5B;0BAC0C;EAAE,OAAO,ENzP1B,GAAO;;AM0PhC;yBACyC;EAAE,OAAO,EN7P1B,GAAO;;AM8P/B;wBACwC;EAAE,OAAO,ENhQ1B,GAAO;;AMiQ9B,oBAAoC;EAAE,OAAO,ENrQ1B,GAAO;;AMsQ1B;sBACsC;EAAE,OAAO,ENxR1B,GAAO;;AMyR5B;uBACuC;EAAE,OAAO,EN7R1B,GAAO;;AM8R7B,0BAA0C;EAAE,OAAO,EN1R1B,GAAO;;AM2RhC,wBAAwC;EAAE,OAAO,ENpS1B,GAAO;;AMqS9B,uBAAuC;EAAE,OAAO,EN3R1B,GAAO;;AM4R7B,yBAAyC;EAAE,OAAO,EN/R1B,GAAO;;AMgS/B,uBAAuC;EAAE,OAAO,ENjS1B,GAAO;;AMkS7B,oBAAoC;EAAE,OAAO,EN+D1B,GAAO;;AM9D1B,qBAAqC;EAAE,OAAO,EN/F1B,GAAO;;AMgG3B,2BAA2C;EAAE,OAAO,EN/b1B,GAAO;;AMgcjC,aAA6B;EAAE,OAAO,ENtU1B,GAAO;;AMuUnB,oBAAoC;EAAE,OAAO,ENtU1B,GAAO;;AMuU1B,sBAAsC;EAAE,OAAO,ENkE1B,GAAO;;AMjE5B,wBAAwC;EAAE,OAAO,ENrK1B,GAAO;;AMsK9B,+BAA+C;EAAE,OAAO,ENrK1B,GAAO;;AMsKrC,qBAAqC;EAAE,OAAO,EN5U1B,GAAO;;AM6U3B,sBAAsC;EAAE,OAAO,ENwH1B,GAAO;;AMvH5B,iBAAiC;EAAE,OAAO,ENnF1B,GAAO;;AMoFvB,iBAAiC;EAAE,OAAO,ENze1B,GAAO;;AM0evB,kBAAkC;EAAE,OAAO,EN9W1B,GAAO;;AM+WxB,gBAAgC;EAAE,OAAO,ENxK1B,GAAO;;AMyKtB,4BAA4C;EAAE,OAAO,ENpQ1B,GAAO;;AMqQlC;qBACqC;EAAE,OAAO,ENS1B,GAAO;;AMR3B,iBAAiC;EAAE,OAAO,ENjd1B,GAAO;;AMkdvB,gBAAgC;EAAE,OAAO,ENzoB1B,GAAO;;AM0oBtB,iBAAiC;EAAE,OAAO,EN/nB1B,GAAO;;AMgoBvB,0BAA0C;EAAE,OAAO,EN3hB1B,GAAO;;AM4hBhC,2BAA2C;EAAE,OAAO,EN9hB1B,GAAO;;AM+hBjC,2BAA2C;EAAE,OAAO,EN5hB1B,GAAO;;AM6hBjC,2BAA2C;EAAE,OAAO,ENjiB1B,GAAO;;AMkiBjC,mBAAmC;EAAE,OAAO,ENpR1B,GAAO;;AMqRzB,kBAAkC;EAAE,OAAO,EN5N1B,GAAO;;AM6NxB,oBAAoC;EAAE,OAAO,EN5N1B,GAAO;;AM6N1B,gBAAgC;EAAE,OAAO,EN/N1B,GAAO;;AMgOtB,cAA8B;EAAE,OAAO,ENlO1B,GAAO;;AMmOpB,qBAAqC;EAAE,OAAO,ENpe1B,GAAO;;AMqe3B,uBAAuC;EAAE,OAAO,ENpe1B,GAAO;;AMqe7B,gBAAgC;EAAE,OAAO,ENtS1B,GAAO;;AMuStB,gBAAgC;EAAE,OAAO,ENiF1B,GAAO;;AMhFtB,oBAAoC;EAAE,OAAO,ENlkB1B,GAAO;;AMmkB1B,oBAAoC;EAAE,OAAO,ENrX1B,GAAO;;AMsX1B,uBAAuC;EAAE,OAAO,ENpI1B,GAAO;;AMqI7B,eAA+B;EAAE,OAAO,ENpc1B,GAAO;;AMqcrB,0BAA0C;EAAE,OAAO,ENhe1B,GAAO;;AMiehC,mBAAmC;EAAE,OAAO,ENpf1B,GAAO;;AMqfzB,eAA+B;EAAE,OAAO,ENlN1B,GAAO;;AMmNrB,uBAAuC;EAAE,OAAO,EN1X1B,GAAO;;AM2X7B,cAA8B;EAAE,OAAO,ENoD1B,GAAO;;AMnDpB,uBAAuC;EAAE,OAAO,EN3J1B,GAAO;;AM4J7B,mBAAmC;EAAE,OAAO,ENzN1B,GAAO;;AM0NzB,iBAAiC;EAAE,OAAO,ENlH1B,GAAO;;AMmHvB,uBAAuC;EAAE,OAAO,EN7L1B,GAAO;;AM8L7B,yBAAyC;EAAE,OAAO,EN7L1B,GAAO;;AM8L/B,sBAAsC;EAAE,OAAO,EN3C1B,GAAO;;AM4C5B,wBAAwC;EAAE,OAAO,EN3C1B,GAAO;;AM4C9B,uBAAuC;EAAE,OAAO,ENrG1B,GAAO;;AMsG7B,0BAA0C;EAAE,OAAO,ENrG1B,GAAO;;AMsGhC,kBAAkC;EAAE,OAAO,EN7U1B,GAAO;;AM8UxB,oBAAoC;EAAE,OAAO,ENnlB1B,GAAO;;AMolB1B,sBAAsC;EAAE,OAAO,ENnlB1B,GAAO;;AMolB5B,kBAAkC;EAAE,OAAO,EN/L1B,GAAO;;AMgMxB,iBAAiC;EAAE,OAAO,ENlX1B,GAAO;;AMmXvB,qBAAqC;EAAE,OAAO,ENkF1B,GAAO;;AMjF3B,kBAAkC;EAAE,OAAO,ENmF1B,GAAO;;AMlFxB,iBAAiC;EAAE,OAAO,EN9c1B,GAAO;;AM+cvB,2BAA2C;EAAE,OAAO,EN2B1B,GAAO;;AM1BjC,yBAAyC;EAAE,OAAO,ENmE1B,GAAO;;AMlE/B,4BAA4C;EAAE,OAAO,ENxK1B,GAAO;;AMyKlC,gBAAgC;EAAE,OAAO,EN9lB1B,GAAO;;AM+lBtB,4BAA4C;EAAE,OAAO,ENtoB1B,GAAO;;AMuoBlC,+BAA+C;EAAE,OAAO,ENqD1B,GAAO;;AMpDrC,kBAAkC;EAAE,OAAO,ENxlB1B,GAAO;;AMylBxB,sCAAsD;EAAE,OAAO,EN5oB1B,GAAO;;AM6oB5C;8CAC8D;EAAE,OAAO,EN9qB1B,GAAO;;AM+qBpD;;eAE+B;EAAE,OAAO,ENvf1B,GAAO;;AMwfrB,gBAAgC;EAAE,OAAO,ENhY1B,GAAO;;AMiYtB,kBAAkC;EAAE,OAAO,ENhY1B,GAAO;;AMiYxB;wBACwC;EAAE,OAAO,EN1H1B,GAAO;;AM2H9B,qBAAqC;EAAE,OAAO,ENzR1B,GAAO;;AM0R3B,iBAAiC;EAAE,OAAO,ENiC1B,GAAO;;AMhCvB,wBAAwC;EAAE,OAAO,ENiC1B,GAAO;;AMhC9B,mBAAmC;EAAE,OAAO,ENlH1B,GAAO;;AMmHzB,yBAAyC;EAAE,OAAO,ENlH1B,GAAO;;AMmH/B,0BAA0C;EAAE,OAAO,ENlH1B,GAAO;;AMmHhC,qBAAqC;EAAE,OAAO,ENrN1B,GAAO;;AMsN3B,sBAAsC;EAAE,OAAO,ENpb1B,GAAO;;AMqb5B,gBAAgC;EAAE,OAAO,ENmE1B,GAAO;;AMlEtB,oBAAoC;EAAE,OAAO,ENpD1B,GAAO;;AMqD1B;+BAC+C;EAAE,OAAO,ENzY1B,GAAO;;AM0YrC;uBACuC;EAAE,OAAO,EN7a1B,GAAO;;AM8a7B,sBAAsC;EAAE,OAAO,ENtX1B,GAAO;;AMuX5B,wBAAwC;EAAE,OAAO,ENlf1B,GAAO;;AMmf9B,0BAA0C;EAAE,OAAO,ENlf1B,GAAO;;AMmfhC,iBAAiC;EAAE,OAAO,ENtT1B,GAAO;;AMuTvB,uBAAuC;EAAE,OAAO,ENptB1B,GAAO;;AMqtB7B,yBAAyC;EAAE,OAAO,ENptB1B,GAAO;;AMqtB/B;uBACuC;EAAE,OAAO,ENrtB1B,GAAO;;AMstB7B;yBACyC;EAAE,OAAO,ENttB1B,GAAO;;AMutB/B,sBAAsC;EAAE,OAAO,ENJ1B,GAAO;;AMK5B,wBAAwC;EAAE,OAAO,ENJ1B,GAAO;;AMK9B,iBAAiC;EAAE,OAAO,ENH1B,GAAO;;AMIvB,mBAAmC;EAAE,OAAO,EN3W1B,GAAO;;AM4WzB;kBACkC;EAAE,OAAO,EN5W1B,GAAO;;AM6WxB;oBACoC;EAAE,OAAO,EN7W1B,GAAO;;AM8W1B,gBAAgC;EAAE,OAAO,ENtN1B,GAAO;;AMuNtB,yBAAyC;EAAE,OAAO,EN3b1B,GAAO;;AM4b/B,mBAAmC;EAAE,OAAO,ENtF1B,GAAO;;AMuFzB;;2BAE2C;EAAE,OAAO,ENxE1B,GAAO;;AMyEjC;qCACqD;EAAE,OAAO,ENvE1B,GAAO;;AMwE3C;2BAC2C;EAAE,OAAO,EN3E1B,GAAO;;AM4EjC;8BAC8C;EAAE,OAAO,EN5E1B,GAAO;;AM6EpC;4BAC4C;EAAE,OAAO,ENjF1B,GAAO;;AMkFlC,iBAAiC;EAAE,OAAO,EN3K1B,GAAO;;AM4KvB;;eAE+B;EAAE,OAAO,ENzrB1B,GAAO;;AM0rBrB,kBAAkC;EAAE,OAAO,ENlP1B,GAAO;;AMmPxB,0BAA0C;EAAE,OAAO,ENK1B,GAAO;;AMJhC,0BAA0C;EAAE,OAAO,ENK1B,GAAO;;AMJhC,yBAAyC;EAAE,OAAO,ENK1B,GAAO;;AMJ/B;uBACuC;EAAE,OAAO,END1B,GAAO;;AME7B;yBACyC;EAAE,OAAO,ENF1B,GAAO;;AMG/B,mBAAmC;EAAE,OAAO,ENxsB1B,GAAO;;AMysBzB,eAA+B;EAAE,OAAO,ENpb1B,GAAO;;AMqbrB,eAA+B;EAAE,OAAO,EN1hB1B,GAAO;;AM2hBrB,eAA+B;EAAE,OAAO,ENxY1B,GAAO;;AMyYrB,kBAAkC;EAAE,OAAO,EN/O1B,GAAO;;AMgPxB,kBAAkC;EAAE,OAAO,ENziB1B,GAAO;;AM0iBxB,oBAAoC;EAAE,OAAO,ENjU1B,GAAO;;AMkU1B,sBAAsC;EAAE,OAAO,EN7K1B,GAAO;;AM8K5B,sBAAsC;EAAE,OAAO,ENhI1B,GAAO;;AMiI5B,qBAAqC;EAAE,OAAO,ENJ1B,GAAO;;AMK3B,iBAAiC;EAAE,OAAO,ENxU1B,GAAO;;AOzcvB,QAAS;EH8BP,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,IAAI;EACZ,QAAQ,EAAE,MAAM;EAChB,IAAI,EAAE,gBAAa;EACnB,MAAM,EAAE,CAAC;;AAUT,mDACQ;EACN,QAAQ,EAAE,MAAM;EAChB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,MAAM,EAAE,CAAC;EACT,QAAQ,EAAE,OAAO;EACjB,IAAI,EAAE,IAAI;;AThDd,QAAU;EACT,SAAS,EAAE,GAAG;EACd,WAAW,EAAE,CAAC;EACd,QAAQ,EAAE,QAAQ;EAClB,cAAc,EAAE,QAAQ;;AAKzB,yCAAU;EACT,KAAK,EAAE,OAAO;EACd,IAAI,EAAE,OAAO;EACb,MAAM,EAAE,CAAC;;AAIV,IAAK;EACJ,WAAW,EaiBH,UAAU;EbhBlB,oBAAoB,EAAE,IAAI;EAC1B,wBAAwB,EAAE,IAAI;EAC9B,WAAW,EAAE,oDAA2C;EACxD,WAAW,EAAE,GAAG;EAChB,KAAK,Ea3BI,IAAI;Eb4Bb,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,MAAM;EACnB,uBAAmB;IAClB,kBAAkB,EAAE,MAAM;IAC1B,MAAM,EAAE,OAAO;EAEhB,oBAAgB;IACf,MAAM,EAAE,OAAO;;AAGjB,IAAK;EACJ,MAAM,EAAE,CAAC;EACT,cAAc,EAAE,kBAAkB;EAClC,sBAAsB,EAAE,WAAW;EACnC,uBAAuB,EAAE,SAAS;EAClC,0BAA0B,EAAE,SAAS;;AAEtC,OAAQ;EACP,OAAO,EAAE,KAAK;;AAEf,KAAM;EACL,OAAO,EAAE,KAAK;;AAEf,OAAQ;EACP,OAAO,EAAE,KAAK;;AAEf,UAAW;EACV,OAAO,EAAE,KAAK;;AAEf,MAAO;EACN,OAAO,EAAE,KAAK;EACd,MAAM,EAAE,QAAQ;;AAEjB,MAAO;EACN,OAAO,EAAE,KAAK;;AAEf,MAAO;EACN,OAAO,EAAE,KAAK;;AAEf,MAAO;EACN,OAAO,EAAE,KAAK;;AAEf,IAAK;EACJ,OAAO,EAAE,KAAK;;AAEf,IAAK;EACJ,OAAO,EAAE,KAAK;;AAEf,GAAI;EACH,OAAO,EAAE,KAAK;;AAEf,OAAQ;EACP,OAAO,EAAE,KAAK;;AAEf,OAAQ;EACP,OAAO,EAAE,KAAK;;AAEf,KAAM;EACL,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,QAAQ;EACxB,qBAAkB;IACjB,OAAO,EAAE,IAAI;IACb,MAAM,EAAE,CAAC;;AAGX,MAAO;EACN,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,QAAQ;;AAEzB,QAAS;EACR,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,QAAQ;;AAEzB,KAAM;EACL,OAAO,EAAE,YAAY;EACrB,cAAc,EAAE,QAAQ;;AAEzB,QAAS;EACR,OAAO,EAAE,IAAI;;AAEd,QAAS;EACR,OAAO,EAAE,IAAI;;AAEd,CAAE;EACD,gBAAgB,EAAE,WAAW;EAC7B,eAAe,EAAE,IAAI;EACrB,KAAK,EahHI,OAAO;EbkHhB,UAAU,EAAE,OAAO;EACnB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,OAAO;EACf,QAAS;IACR,OAAO,EAAE,CAAC;EAEX,OAAQ;IACP,OAAO,EAAE,CAAC;IACV,KAAK,Ea1HG,OAAO;;Ab6HjB,WAAY;EACX,aAAa,EAAE,UAAU;;AAE1B,CAAE;EACD,WAAW,EAAE,GAAG;EAChB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,MAAO;EACN,WAAW,EAAE,GAAG;EAChB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,GAAI;EACH,UAAU,EAAE,MAAM;EAClB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EACF,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,OAAO;EACf,UAAU,EAAE,kBAAkB;EAC9B,WAAW,EAAE,mBAAmB;EAChC,aAAa,EAAE,kBAAkB;EACjC,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,oDAA2C;EACxD,WAAW,EAAE,GAAG;EAChB,KAAK,Ea3JI,IAAI;Eb4Jb,KAAK,EAAE,IAAI;;AAEZ,IAAK;EACJ,UAAU,EanKD,IAAI;EboKb,KAAK,EanKI,IAAI;;AbqKd,KAAM;EACL,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,CAAC;;AAEf,GAAI;EAEH,MAAM,EAAE,MAAM;EACd,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,CAAC;;AAEf,GAAI;EAEH,GAAG,EAAE,KAAK;EACV,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,CAAC;EACd,SAAM;IACL,SAAS,EAAE,GAAG;;AAGhB,GAAI;EACH,MAAM,EAAE,CAAC;EACT,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EAEF,UAAU,EAAE,WAAW;EACvB,MAAM,EAAE,CAAC;;AAEV,GAAI;EACH,QAAQ,EAAE,IAAI;EACd,OAAO,EAAE,MAAM;EACf,aAAa,EAAE,MAAM;EACrB,UAAU,EazLA,IAAI;Eb0Ld,WAAW,EAAE,CAAC;EACd,KAAK,Ea/KK,OAAO;EbiLjB,aAAa,EAAE,GAAG;EAClB,SAAS,EAAE,IAAI;EACf,WAAW,EatKH,SAAS;EbuKjB,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,MAAM;EACrB,QAAK;IACJ,OAAO,EAAE,CAAC;;AAGZ,IAAK;EACJ,WAAW,EahLH,SAAS;EbiLjB,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,0EAA2C;EACxD,OAAO,EAAE,eAAe;EACxB,WAAW,EAAE,CAAC;;AAEf,GAAI;EACH,WAAW,EazLH,SAAS;Eb0LjB,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,IAAK;EACJ,WAAW,Ea/LH,SAAS;EbgMjB,SAAS,EAAE,GAAG;EACd,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,MAAO;EAEN,QAAQ,EAAE,OAAO;EACjB,cAAc,EAAE,IAAI;EACpB,kBAAkB,EAAE,MAAM;EAC1B,MAAM,EAAE,OAAO;EACf,OAAO,EAAE,KAAK;EACd,MAAM,EAAE,OAAO;EACf,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,cAAc;EACvB,aAAa,EAAE,SAAS;;AAEzB,KAAM;EAEL,WAAW,EAAE,MAAM;EACnB,WAAQ;IACP,UAAU,EajOD,OAAO;;AboOlB,QAAS;EAER,WAAW,EAAE,GAAG;;AAEjB,MAAO;EAEN,cAAc,EAAE,IAAI;EACpB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,iBAAmB;EAE3B,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,SAAS;EAClB,KAAK,EAAE,iBAAiB;EACxB,MAAM,EAAE,UAAU;EAClB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;;AAEb,QAAS;EAER,QAAQ,EAAE,IAAI;EACd,OAAO,EAAE,KAAK;EACd,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,OAAO;EAChB,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,SAAS;EACxB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,iBAAmB;EAE3B,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,SAAS;EAClB,KAAK,EAAE,iBAAiB;EACxB,MAAM,EAAE,UAAU;;AAEnB,iBAAkB;EACjB,kBAAkB,EAAE,MAAM;EAC1B,MAAM,EAAE,OAAO;;AAEhB,kBAAmB;EAClB,kBAAkB,EAAE,MAAM;EAC1B,MAAM,EAAE,OAAO;EACf,OAAO,EAAE,KAAK;EACd,MAAM,EAAE,OAAO;EACf,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,cAAc;EACvB,aAAa,EAAE,SAAS;;AAEzB,gBAAiB;EAChB,MAAM,EAAE,OAAO;;AAEhB,wBAAyB;EACxB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,uBAAwB;EACvB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,oBAAqB;EAEpB,UAAU,EAAE,UAAU;EACtB,OAAO,EAAE,CAAC;;AAEX,iBAAkB;EAEjB,UAAU,EAAE,UAAU;EACtB,OAAO,EAAE,CAAC;;AAEX,6CAA8C;EAC7C,MAAM,EAAE,IAAI;;AAEb,6CAA8C;EAC7C,MAAM,EAAE,IAAI;;AAEb,kBAAmB;EAClB,kBAAkB,EAAE,SAAS;EAE7B,UAAU,EAAE,WAAW;;AAExB,gDAAiD;EAChD,kBAAkB,EAAE,IAAI;;AAEzB,6CAA8C;EAC7C,kBAAkB,EAAE,IAAI;;AAEzB,QAAS;EACR,MAAM,EAAE,gBAAkB;EAC1B,MAAM,EAAE,KAAK;EACb,OAAO,EAAE,kBAAkB;EAC3B,OAAO,EAAE,oBAAoB;EAC7B,YAAY,EAAE,GAAG;EACjB,YAAY,EAAE,KAAK;EACnB,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,QAAQ;EACvB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,eAAO;IACN,aAAa,EAAE,CAAC;EAEjB,2BAAmB;IAClB,aAAa,EAAE,CAAC;;AAGlB,MAAO;EACN,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,KAAK,EavWI,IAAI;EbwWb,WAAW,EAAE,GAAG;EAChB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,KAAM;EACL,KAAK,EAAE,IAAI;EACX,cAAc,EAAE,CAAC;EACjB,eAAe,EAAE,QAAQ;EACzB,aAAa,EAAE,QAAQ;EACvB,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,MAAM;;AAEtB,EAAG;EACF,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,OAAO,EAAE,eAAe;;AAEzB,EAAG;EACF,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,UAAU,EAAE,IAAI;EAChB,KAAK,EahYI,IAAI;EbiYb,OAAO,EAAE,eAAe;;AAEzB,yBAAwB;EACvB,IAAK;IACJ,SAAS,EAAE,IAAI;;EAEhB,EAAG;IACF,SAAS,EAAE,4DAA4D;;EAExE,EAAG;IACF,SAAS,EAAE,4DAA4D;;EAExE,EAAG;IACF,SAAS,EAAE,4DAA4D;;EAExE,EAAG;IACF,SAAS,EAAE,6DAA6D;;EAEzE,EAAG;IACF,SAAS,EAAE,4DAA4D;;EAExE,EAAG;IACF,SAAS,EAAE,sCAAsC;AAGnD,IAAK;EACJ,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,uBAAuB;EACtC,MAAM,EAAE,IAAI;;AAEb,OAAQ;EACP,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,uBAAuB;EACtC,MAAM,EAAE,IAAI;;AAEb,OAAQ;EACP,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,MAAM;EACrB,UAAU,EAAE,MAAM;;AAEnB,GAAI;EACH,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,CAAC;;AAEf,UAAW;EACV,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,MAAM;EACrB,UAAU,EAAE,MAAM;EAClB,eAAK;IACJ,OAAO,EAAE,KAAK;IACd,UAAU,EAAE,MAAM;;AAGpB,OAAQ;EACP,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,MAAO;EACN,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,IAAK;EACJ,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,GAAI;EACH,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,MAAM;;AAEtB,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,KAAK,EavdI,IAAI;Ebwdb,WAAW,EAAE,GAAG;;AAEjB,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,IAAK;EACJ,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,oDAA2C;EACxD,WAAW,EAAE,GAAG;EAChB,KAAK,EaveI,IAAI;Ebweb,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,mBAAmB;EAC9B,UAAU,EAAE,mBAAmB;EAC/B,WAAW,EAAE,mBAAmB;EAChC,aAAa,EAAE,kBAAkB;;AAElC,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,oDAA2C;EACxD,WAAW,EAAE,GAAG;EAChB,KAAK,EanfI,IAAI;Ebofb,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,oBAAoB;EAC/B,UAAU,EAAE,mBAAmB;EAC/B,WAAW,EAAE,mBAAmB;EAChC,aAAa,EAAE,kBAAkB;;AAElC,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,oDAA2C;EACxD,WAAW,EAAE,GAAG;EAChB,KAAK,Ea/fI,IAAI;EbggBb,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,oBAAoB;EAC/B,UAAU,EAAE,mBAAmB;EAC/B,WAAW,EAAE,kBAAkB;EAC/B,aAAa,EAAE,kBAAkB;;AAElC,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,oDAA2C;EACxD,WAAW,EAAE,GAAG;EAChB,KAAK,Ea3gBI,IAAI;Eb4gBb,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,oBAAoB;EAC/B,UAAU,EAAE,kBAAkB;EAC9B,WAAW,EAAE,mBAAmB;EAChC,aAAa,EAAE,kBAAkB;;AAElC,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,WAAW,EAAE,oDAA2C;EACxD,WAAW,EAAE,GAAG;EAChB,KAAK,EavhBI,IAAI;EbwhBb,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,QAAQ;EACpB,WAAW,EAAE,OAAO;EACpB,aAAa,EAAE,kBAAkB;;AAElC,CAAE;EACD,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,GAAI;EACH,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,KAAM;EACL,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,OAAO,EAAE,KAAK;EACd,cAAc,EAAE,QAAQ;EACxB,aAAa,EAAE,SAAS;EACxB,MAAM,EAAE,OAAO;;AAEhB,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,MAAM;EACrB,YAAY,EAAE,KAAK;;AAEpB,CAAE;EACD,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,MAAM;EACrB,MAAM,EAAE,UAAU;;AAEnB,CAAE;EACD,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,CAAE;EACD,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,MAAO;EACN,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,KAAM;EACL,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,KAAM;EACL,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,KAAM;EACL,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,CAAE;EACD,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,EAAG;EACF,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;EACV,aAAa,EAAE,MAAM;EACrB,YAAY,EAAE,KAAK;;AAEpB,GAAI;EACH,MAAM,EAAE,CAAC;EACT,OAAO,EAAE,CAAC;;AAEX,0BAAyB;EACxB,EAAG;IACF,SAAS,EAAE,oBAAoB;IAC/B,UAAU,EAAE,kBAAkB;IAC9B,WAAW,EAAE,mBAAmB;IAChC,aAAa,EAAE,kBAAkB;;EAElC,EAAG;IACF,SAAS,EAAE,oBAAoB;IAC/B,UAAU,EAAE,mBAAmB;IAC/B,WAAW,EAAE,mBAAmB;IAChC,aAAa,EAAE,kBAAkB;;EAElC,EAAG;IACF,SAAS,EAAE,mBAAmB;IAC9B,UAAU,EAAE,mBAAmB;IAC/B,WAAW,EAAE,mBAAmB;IAChC,aAAa,EAAE,kBAAkB;;EAElC,EAAG;IACF,SAAS,EAAE,mBAAmB;IAC9B,UAAU,EAAE,mBAAmB;IAC/B,WAAW,EAAE,kBAAkB;IAC/B,aAAa,EAAE,kBAAkB;;EAElC,EAAG;IACF,SAAS,EAAE,oBAAoB;IAC/B,UAAU,EAAE,kBAAkB;IAC9B,WAAW,EAAE,mBAAmB;IAChC,aAAa,EAAE,kBAAkB;;EAElC,EAAG;IACF,SAAS,EAAE,IAAI;IACf,UAAU,EAAE,QAAQ;IACpB,WAAW,EAAE,OAAO;IACpB,aAAa,EAAE,kBAAkB;;EAElC,QAAS;IACR,aAAa,EAAE,UAAU;;EAE1B,iBAAkB;IACjB,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,UAAU;;EAE1B,oBAAqB;IACpB,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,UAAU;;EAE1B,gBAAiB;IAChB,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,UAAU;;EAE1B,QAAS;IACR,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,UAAU;;EAE1B,MAAO;IACN,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,QAAQ;;EAExB,kBAAmB;IAClB,SAAS,EAAE,IAAI;IACf,aAAa,EAAE,QAAQ;;EAExB,KAAM;IACL,aAAa,EAAE,SAAS;;EAEzB,EAAG;IACF,OAAO,EAAE,cAAc;;EAExB,EAAG;IACF,OAAO,EAAE,cAAc;AAGzB,iBAAkB;EACjB,OAAO,EAAE,KAAK;EACd,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,OAAO;EAChB,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,SAAS;;AAEzB,oBAAqB;EACpB,OAAO,EAAE,KAAK;EACd,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,OAAO;EAChB,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,SAAS;;AAEzB,gBAAiB;EAChB,OAAO,EAAE,KAAK;EACd,SAAS,EAAE,IAAI;EACf,OAAO,EAAE,OAAO;EAChB,SAAS,EAAE,IAAI;EACf,aAAa,EAAE,SAAS;;AAEzB,OAAQ;EACP,gBAAgB,EAAE,4BAAW;EAC7B,eAAe,EAAE,KAAK;EACtB,mBAAmB,EAAE,GAAG;EACxB,UAAU,EAAE,KAAK;EACjB,OAAO,EAAE,YAAY;EACrB,OAAO,EAAE,WAAW;EACpB,OAAO,EAAE,IAAI;EACb,uBAAuB,EAAE,MAAM;EAC/B,aAAa,EAAE,MAAM;EACrB,eAAe,EAAE,MAAM;EACvB,mBAAmB,EAAE,MAAM;EAC3B,cAAc,EAAE,MAAM;EACtB,WAAW,EAAE,MAAM;;AAEpB,IAAK;EACJ,KAAK,EAAE,KAAK;EAEZ,aAAa,EAAE,WAAW;EAC1B,QAAQ,EAAE,MAAM;EAEhB,UAAU,EAAE,UAAU;EAEtB,UAAU,EAAE,8DAAwC;;AAErD,OAAQ;EACP,gBAAgB,EahuBP,OAAO;EbiuBhB,OAAO,EAAE,cAAc;EAEvB,aAAa,EAAE,WAAW;EAC1B,UAAU,EAAE,MAAM;;AAEnB,aAAc;EACb,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,SAAS;EACzB,SAAS,EAAE,IAAI;EACf,cAAc,EAAE,KAAK;EACrB,MAAM,EAAE,QAAQ;EAChB,mBAAmB,EAAE,IAAI;EACzB,gBAAgB,EAAE,IAAI;EACtB,eAAe,EAAE,IAAI;EAErB,WAAW,EAAE,IAAI;EACjB,KAAK,EahvBK,IAAI;;AbkvBf,cAAe;EACd,mBAAmB,EAAE,IAAI;EACzB,gBAAgB,EAAE,IAAI;EACtB,eAAe,EAAE,IAAI;EAErB,WAAW,EAAE,IAAI;EACjB,KAAK,EaxvBK,IAAI;EbyvBd,WAAW,EAAE,GAAG;EAChB,SAAS,EAAE,IAAI;EACf,MAAM,EAAE,QAAQ;;AAEjB,KAAM;EACL,QAAQ,EAAE,QAAQ;EAClB,OAAO,EAAE,CAAC;EACV,YAAY,EAAE,CAAC;EACf,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,CAAC;EAChB,OAAO,EAAE,YAAY;EACrB,OAAO,EAAE,WAAW;EACpB,OAAO,EAAE,IAAI;EACb,sBAAsB,EAAE,WAAW;EACnC,kBAAkB,EAAE,WAAW;EAC/B,cAAc,EAAE,WAAW;EAC3B,uBAAuB,EAAE,MAAM;EAC/B,aAAa,EAAE,MAAM;EACrB,eAAe,EAAE,MAAM;EACvB,mBAAmB,EAAE,MAAM;EAC3B,cAAc,EAAE,MAAM;EACtB,WAAW,EAAE,MAAM;EACnB,UAAU,EAAE,KAAK;;AAElB,cAAe;EACd,gBAAgB,EajxBN,OAAO;EbkxBjB,mBAAmB,EAAE,IAAI;EACzB,gBAAgB,EAAE,IAAI;EACtB,eAAe,EAAE,IAAI;EAErB,WAAW,EAAE,IAAI;EACjB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,GAAG;EACX,0BAAc;IACb,YAAY,EAAE,QAAQ;IACtB,QAAQ,EAAE,QAAQ;IAClB,IAAI,EAAE,QAAQ;EAEf,yBAAa;IACZ,YAAY,EAAE,QAAQ;IACtB,QAAQ,EAAE,QAAQ;IAClB,IAAI,EAAE,QAAQ;;AAGhB,WAAY;EACX,gBAAgB,EaryBN,OAAO;EbsyBjB,UAAU,EAAE,MAAM;EAClB,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,IAAI;EACZ,OAAO,EAAE,YAAY;EACrB,OAAO,EAAE,WAAW;EACpB,OAAO,EAAE,IAAI;EACb,uBAAuB,EAAE,MAAM;EAC/B,aAAa,EAAE,MAAM;EACrB,eAAe,EAAE,MAAM;EACvB,mBAAmB,EAAE,MAAM;EAC3B,cAAc,EAAE,MAAM;EACtB,WAAW,EAAE,MAAM;EAEnB,aAAa,EAAE,GAAG;EAClB,KAAK,EarzBK,IAAI;EbszBd,0BAAiB;IAChB,OAAO,EAAE,OAAO;IAChB,WAAW,Ea9wBH,QAAQ;EbgxBjB,+BAAsB;IACrB,OAAO,EAAE,OAAO;IAChB,WAAW,EalxBH,QAAQ;EboxBjB,8BAAqB;IACpB,OAAO,EAAE,OAAO;IAChB,WAAW,EatxBH,QAAQ;EbwxBjB,2BAAkB;IACjB,OAAO,EAAE,OAAO;IAChB,WAAW,Ea1xBH,QAAQ;Eb4xBjB,yBAAgB;IACf,OAAO,EAAE,OAAO;IAChB,WAAW,Ea9xBH,QAAQ;;AbiyBlB,KAAM;EACL,UAAU,EAAE,KAAK;EACjB,gBAAgB,Ea70BN,IAAI;Eb+0Bd,aAAa,EAAE,WAAW;EAC1B,OAAO,EAAE,cAAc;;AAExB,QAAS;EACR,UAAU,EAAE,MAAM;EAClB,gBAAQ;IACP,MAAM,EAAE,IAAI;;AAGd,eAAgB;EACf,UAAU,EAAE,KAAK;;AAElB,OAAQ;EACP,OAAO,EAAE,YAAY;EACrB,gBAAgB,Ea31BN,OAAO;Eb61BjB,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,QAAQ;EACjB,KAAK,Eaj2BK,IAAI;Ebm2Bd,UAAU,EAAE,8DAA0C;EACtD,eAAe,EAAE,IAAI;EACrB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,IAAI;EAEZ,UAAU,EAAE,8CAA8C;EAC1D,MAAM,EAAE,OAAO;EACf,aAAQ;IACP,KAAK,Ea32BI,IAAI;Ib62Bb,UAAU,EAAE,8DAAwC;IACpD,gBAAgB,Eax2BP,OAAO;;Ab22BlB,cAAe;EACd,OAAO,EAAE,QAAQ;EACjB,SAAS,EAAE,IAAI;EACf,UAAU,EAAE,cAAmB;EAC/B,KAAK,Ea72BK,IAAI;Eb82Bd,UAAU,Eat3BA,IAAI;Ebu3Bd,oBAAQ;IACP,KAAK,Eah3BI,IAAI;Ibi3Bb,UAAU,Eaz3BD,IAAI;Ib23Bb,UAAU,EAAE,4DAAuC;;AAGrD,KAAM;EACL,YAAY,EAAE,CAAC;EACf,UAAU,EAAE,IAAI;EAChB,aAAa,EAAE,CAAC;EAChB,MAAM,EAAE,WAAW;EACnB,MAAM,EAAE,6BAAmB;EAE3B,aAAa,EAAE,GAAG;EAClB,6BAAwB;IACvB,UAAU,Ean4BD,mBAAkB;Ibq4B1B,0CAAK;MACJ,KAAK,Ear3BE,OAAO;Ibu3Bf,gDAAW;MACV,KAAK,Eax3BE,OAAO;Ib43Bf,wCAAK;MACJ,KAAK,Eap4BE,OAAO;Ibs4Bf,8CAAW;MACV,KAAK,Eav4BE,OAAO;;Ab44BlB,WAAY;EACX,QAAQ,EAAE,QAAQ;EAClB,QAAQ,EAAE,MAAM;EAChB,OAAO,EAAE,QAAQ;EACjB,aAAa,EAAE,6BAAmB;EAClC,wBAAe;IACd,cAAc,EAAE,SAAS;EAE1B,sBAAa;IACZ,aAAa,EAAE,IAAI;EAEpB,+BAAoB;IACnB,OAAO,EAAE,YAAY;IACrB,OAAO,EAAE,WAAW;IACpB,OAAO,EAAE,IAAI;IACb,uBAAuB,EAAE,MAAM;IAC/B,aAAa,EAAE,MAAM;IACrB,eAAe,EAAE,MAAM;IACvB,mBAAmB,EAAE,MAAM;IAC3B,cAAc,EAAE,MAAM;IACtB,WAAW,EAAE,MAAM;IACnB,OAAO,EAAE,QAAQ;IACjB,QAAQ,EAAE,QAAQ;IAClB,GAAG,EAAE,CAAC;IACN,KAAK,EAAE,CAAC;IACR,MAAM,EAAE,CAAC;EAEV,8BAAqB;IACpB,KAAK,Ea56BI,OAAO;Eb86BjB,4BAAmB;IAClB,KAAK,Ea96BI,OAAO;;Abk7BjB,+BAAS;EACR,OAAO,EAAE,aAAa;AAEvB,6BAAK;EACJ,OAAO,EAAE,YAAY;EACrB,OAAO,EAAE,WAAW;EACpB,OAAO,EAAE,IAAI;EACb,uBAAuB,EAAE,MAAM;EAC/B,aAAa,EAAE,MAAM;EACrB,eAAe,EAAE,MAAM;EACvB,mBAAmB,EAAE,MAAM;EAC3B,cAAc,EAAE,MAAM;EACtB,WAAW,EAAE,MAAM;EACnB,OAAO,EAAE,QAAQ;EACjB,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;EACT,gBAAgB,Ean8BP,OAAO;Ebo8BhB,WAAW,EAAE,GAAG;EAChB,SAAS,EAAE,IAAI;EACf,oCAAS;IACR,YAAY,EAAE,GAAG;IACjB,WAAW,EAAE,GAAG;AAGlB,yCAAmB;EAClB,KAAK,Ea98BI,OAAO;Eb+8BhB,cAAc,EAAE,GAAG;AAEpB,uCAAiB;EAChB,KAAK,Eaj9BI,OAAO;Ebk9BhB,cAAc,EAAE,GAAG;;AAGrB,SAAU;EAET,UAAU,EAAE,UAAU;EACtB,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,IAAI;EACf,WAAW,EAAE,IAAI;EACjB,MAAM,EAAE,KAAK;EACb,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,4BAAmB;EAC3B,mBAAU;IACT,aAAa,EAAE,IAAI;;AAGrB,YAAa;EACZ,UAAU,EAAE,MAAM;;AAEnB,aAAc;EACb,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,IAAI;;AAEZ,UAAW;EACV,KAAK,Eav+BK,OAAO;Ebw+BjB,gBAAM;IACL,KAAK,Eax+BI,OAAO;Iby+BhB,MAAM,EAAE,aAAmB;;AAG7B,QAAS;EACR,QAAQ,EAAE,QAAQ;EAClB,KAAK,EAAE,GAAG;EACV,MAAM,EAAE,GAAG;EACX,OAAO,EAAE,CAAC;EACV,MAAM,EAAE,IAAI;EACZ,QAAQ,EAAE,MAAM;EAChB,IAAI,EAAE,gBAAgB;EACtB,MAAM,EAAE,CAAC;;AAEV,kBAAmB;EAClB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,iBAAmB;EAE3B,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,SAAS;EAClB,KAAK,EAAE,iBAAiB;EACxB,MAAM,EAAE,UAAU;;AAEnB,sBAAuB;EACtB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,iBAAmB;EAE3B,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,SAAS;EAClB,KAAK,EAAE,iBAAiB;EACxB,MAAM,EAAE,UAAU;;AAEnB,iBAAkB;EACjB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,iBAAmB;EAE3B,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,SAAS;EAClB,KAAK,EAAE,iBAAiB;EACxB,MAAM,EAAE,UAAU;;AAEnB,oBAAqB;EACpB,OAAO,EAAE,IAAI;EACb,MAAM,EAAE,iBAAmB;EAE3B,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,SAAS;EAClB,KAAK,EAAE,iBAAiB;EACxB,MAAM,EAAE,UAAU;;AAEnB,KAAM;EACL,OAAO,EAAE,CAAC;EACV,gBAAW;IACV,OAAO,EAAE,IAAI;IACb,qCAAuB;MACtB,gBAAgB,Ea9iCR,IAAI;Mb+iCZ,KAAK,Ea5hCG,IAAI;Eb+hCd,gBAAW;IACV,KAAK,EajiCI,IAAI;IbkiCb,MAAM,EAAE,OAAO;IACf,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,GAAG;IACZ,UAAU,EAAE,MAAM;IAClB,kBAAkB,EAAE,oBAAoB;IACxC,eAAe,EAAE,oBAAoB;IACrC,aAAa,EAAE,oBAAoB;IAEnC,UAAU,EAAE,oBAAoB;IAChC,sBAAQ;MACP,KAAK,Ea3iCG,IAAI;Eb8iCd,gBAAW;IACV,KAAK,EAAE,IAAI;EAEZ,UAAK;IACJ,OAAO,EAAE,IAAI;IACb,yBAAe;MACd,aAAa,EAAE,CAAC;;AAInB,WAAY;EACX,KAAK,EAAE,IAAI;;AAEZ,YAAa;EACZ,KAAK,EAAE,KAAK;;AAEb,kBAAmB;EAClB,UAAU,EAAE,IAAI;EAChB,MAAM,EAAE,OAAO;;AAEhB,MAAO;EAEN,UAAU,EAAE,kBAAmB;EAC/B,0BAAoB;IACnB,KAAK,EAAE,IAAI;IACX,OAAO,EAAE,IAAI;IACb,kCAAQ;MACP,UAAU,EarkCF,OAAO;MbskCf,KAAK,Ea7lCG,IAAI;Mb8lCZ,WAAW,EAAE,GAAG;MAChB,kBAAkB,EAAE,oBAAoB;MACxC,eAAe,EAAE,oBAAoB;MACrC,aAAa,EAAE,oBAAoB;MAEnC,UAAU,EAAE,oBAAoB;MAChC,wCAAQ;QACP,UAAU,Ea7kCH,OAAO;Qb8kCd,KAAK,EatmCE,IAAI;QbumCX,WAAW,EAAE,GAAG;IAIjB,0CAAQ;MACP,gBAAgB,EanlCT,OAAO;IbqlCf,0CAAQ;MACP,MAAM,EAAE,KAAK;MACb,UAAU,EAAE,IAAI;MAChB,kBAAkB,EAAE,oBAAoB;MACxC,eAAe,EAAE,oBAAoB;MACrC,aAAa,EAAE,oBAAoB;MAEnC,UAAU,EAAE,oBAAoB;IAGlC,wCAAc;MACb,WAAW,EAAE,IAAI;EAGnB,YAAM;IACL,KAAK,EAAE,KAAK;IACZ,SAAS,EAAE,IAAI;IACf,MAAM,EAAE,OAAO;EAEhB,WAAK;IACJ,WAAW,Ea7lCJ,KAAK;Ib8lCZ,WAAW,EAAE,GAAG;IAChB,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,mBAAmB;IAC5B,MAAM,EAAE,CAAC;IACT,MAAM,EAAE,OAAO;;AAGjB,KAAM;EACL,UAAU,Ea3oCA,IAAI;Eb4oCd,KAAK,EapoCK,IAAI;EbqoCd,KAAK,EAAE,IAAI;EACX,MAAM,EAAE,CAAC;EACT,WAAW,EAAE,CAAC;EACd,YAAY,EAAE,IAAI;EAClB,aAAa,EAAE,IAAI;EACnB,OAAO,EAAE,KAAK;EACd,QAAQ,EAAE,MAAM;EAEhB,UAAU,EAAE,UAAU;EAEtB,UAAU,EAAE,YAAY;;AAEzB,WAAY;EACX,UAAU,EalpCA,IAAI;EbmpCd,KAAK,Ea3pCK,IAAI;;Ab6pCf,mBAAoB;EACnB,KAAK,EAAE,IAAI;;AAEZ,yBAA0B;EACzB,kBAAkB,EAAE,gCAAuB;EAC3C,qBAAqB,EAAE,CAAC;EAExB,aAAa,EAAE,CAAC;;AAEjB,yBAA0B;EACzB,qBAAqB,EAAE,CAAC;EAExB,aAAa,EAAE,CAAC;EAChB,UAAU,Ea/oCA,IAAI;EbgpCd,kBAAkB,EAAE,gCAAuB;;AAE5C,gBAAiB;EAChB,aAAa,EAAE,GAAG;;AAEnB,gBAAiB;EAChB,aAAa,EAAE,GAAG;;AAEnB,aAAc;EACb,UAAU,EAAE,GAAG;;AAEhB,aAAc;EACb,UAAU,EAAE,GAAG;;AAEhB,MAAO;EACN,MAAM,EAAE,QAAQ;EAChB,SAAS,EAAE,KAAK;EAChB,gBAAgB,EahrCN,OAAO;EbkrCjB,aAAa,EAAE,GAAG;EAClB,OAAO,EAAE,IAAI;EACb,QAAQ,EAAE,QAAQ;EAClB,mBAAe;IACd,UAAU,EaprCD,OAAO;IbqrChB,KAAK,EatqCI,OAAO;IbuqChB,OAAO,EAAE,cAAc;IACvB,sBAAG;MACF,KAAK,EazqCG,OAAO;Mb0qCf,MAAM,EAAE,CAAC;IAEV,sBAAG;MACF,MAAM,EAAE,CAAC;EAGX,aAAO;IACN,KAAK,EAAE,IAAI;IACX,MAAM,EAAE,IAAI;IACZ,OAAO,EAAE,CAAC;IACV,MAAM,EAAE,CAAC;IAET,UAAU,EAAE,IAAI;IAChB,MAAM,EAAE,aAAmB;IAC3B,OAAO,EAAE,IAAI;IACb,KAAK,EAAE,KAAK;IAEZ,aAAa,EAAE,GAAG;IAClB,QAAQ,EAAE,QAAQ;IAClB,KAAK,EAAE,CAAC;IACR,GAAG,EAAE,CAAC;IACN,gBAAgB,EAAE,WAAW;IAC7B,MAAM,EAAE,OAAO;IACf,kBAAkB,EAAE,oBAAoB;IACxC,eAAe,EAAE,oBAAoB;IACrC,aAAa,EAAE,oBAAoB;IAEnC,UAAU,EAAE,oBAAoB;IAChC,mBAAQ;MACP,gBAAgB,EavsCR,OAAO;MbwsCf,KAAK,EavtCG,OAAO;;Ab2tClB,cAAe;EACd,QAAQ,EAAE,MAAM;;AAEjB;;4CAE2C;EACvC,gBAAgB,Ea7uCT,OAAO;Eb8uCd,kBAAkB,EAAE,oBAAoB;EACxC,eAAe,EAAE,oBAAoB;EACrC,aAAa,EAAE,oBAAoB;EACnC,UAAU,EAAE,oBAAoB;;AAEpC;kDACiD;EAC7C,gBAAgB,EapvCT,OAAO;;AbuvCjB,4BAAO;EACN,YAAY,Ea7uCH,OAAO;Ab+uCjB,8BAAS;EACR,YAAY,EahvCH,OAAO;AbkvCjB,wCAAmB;EAClB,YAAY,EanvCH,OAAO;AbqvCjB,4CAAuB;EACtB,YAAY,EatvCH,OAAO;AbwvCjB,uCAAkB;EACjB,YAAY,EazvCH,OAAO;Ab2vCjB,0CAAqB;EACpB,YAAY,Ea5vCH,OAAO;Ab8vCjB,kCAAa;EACZ,MAAM,EAAE,SAAS;EACjB,OAAO,EAAE,KAAK;EACd,KAAK,EAAE,IAAI;EACX,SAAS,EAAE,IAAI;EACf,KAAK,EanwCI,OAAO;EbowChB,WAAW,EAAE,GAAG;;AAGlB,qBAAsB;EACrB,OAAO,EAAE,UAAU;EACnB,KAAK,EAAE,IAAI;EACX,KAAK,EAAE,EAAE;;AAEV,uCAAwC;EACvC,OAAO,EAAE,KAAK;;AAEf,uCAAwC;EACvC,OAAO,EAAE,KAAK;;AAEf,uCAAwC;EACvC,OAAO,EAAE,KAAK;;AAEf,uCAAwC;EACvC,OAAO,EAAE,KAAK;;AAEf,uCAAwC;EACvC,OAAO,EAAE,KAAK;;AAEf,WAAY;EACX,QAAQ,EAAE,QAAQ;EAClB,GAAG,EAAE,CAAC;EACN,KAAK,EAAE,CAAC;EACR,MAAM,EAAE,CAAC;;AAGT,+BAAc;EAEb,aAAa,EAAE,WAAW;AAE3B,8BAAa;EAEZ,aAAa,EAAE,WAAW", +"sources": ["scss/font-awesome/font-awesome.scss","scss/font-awesome/_path.scss","scss/style.scss","scss/font-awesome/_core.scss","scss/font-awesome/_larger.scss","scss/font-awesome/_fixed-width.scss","scss/font-awesome/_list.scss","scss/font-awesome/_variables.scss","scss/font-awesome/_bordered-pulled.scss","scss/font-awesome/_animated.scss","scss/font-awesome/_rotated-flipped.scss","scss/font-awesome/_mixins.scss","scss/font-awesome/_stacked.scss","scss/font-awesome/_icons.scss","scss/font-awesome/_screen-reader.scss","scss/_variables.scss"], +"names": [], +"file": "style.css" +} diff --git a/public/installer/css/style.min.css b/public/installer/css/style.min.css new file mode 100755 index 000000000..bd930ef97 --- /dev/null +++ b/public/installer/css/style.min.css @@ -0,0 +1,6 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@import url("https://fonts.googleapis.com/css?family=Roboto:400,300,500,700,900");@font-face{font-family:'FontAwesome';src:url("../fonts/fontawesome-webfont.eot?v=4.7.0");src:url("../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0") format("embedded-opentype"),url("../fonts/fontawesome-webfont.woff2?v=4.7.0") format("woff2"),url("../fonts/fontawesome-webfont.woff?v=4.7.0") format("woff"),url("../fonts/fontawesome-webfont.ttf?v=4.7.0") format("truetype"),url("../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular") format("svg");font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.3333333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.2857142857em;text-align:center}.fa-ul{padding-left:0;margin-left:2.1428571429em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.1428571429em;width:2.1428571429em;top:.1428571429em;text-align:center}.fa-li.fa-lg{left:-1.8571428571em}.fa-border{padding:.2em .25em .15em;border:solid 0.08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{-webkit-filter:none;filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-remove:before,.fa-close:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-gear:before,.fa-cog:before{content:""}.fa-trash-o:before{content:""}.fa-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-rotate-right:before,.fa-repeat:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before{content:""}.fa-check-circle:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-warning:before,.fa-exclamation-triangle:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-gears:before,.fa-cogs:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before{content:""}.fa-arrow-circle-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-save:before,.fa-floppy-o:before{content:""}.fa-square:before{content:""}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-unsorted:before,.fa-sort:before{content:""}.fa-sort-down:before,.fa-sort-desc:before{content:""}.fa-sort-up:before,.fa-sort-asc:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-legal:before,.fa-gavel:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-flash:before,.fa-bolt:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-paste:before,.fa-clipboard:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-unlink:before,.fa-chain-broken:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:""}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:""}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:""}.fa-euro:before,.fa-eur:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-rupee:before,.fa-inr:before{content:""}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:""}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:""}.fa-won:before,.fa-krw:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-turkish-lira:before,.fa-try:before{content:""}.fa-plus-square-o:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-institution:before,.fa-bank:before,.fa-university:before{content:""}.fa-mortar-board:before,.fa-graduation-cap:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:""}.fa-file-zip-o:before,.fa-file-archive-o:before{content:""}.fa-file-sound-o:before,.fa-file-audio-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:""}.fa-ge:before,.fa-empire:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-send:before,.fa-paper-plane:before{content:""}.fa-send-o:before,.fa-paper-plane-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-hotel:before,.fa-bed:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-yc:before,.fa-y-combinator:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-tv:before,.fa-television:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:""}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-signing:before,.fa-sign-language:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-vcard:before,.fa-address-card:before{content:""}.fa-vcard-o:before,.fa-address-card-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-family:Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:300;color:#666;font-size:12px;line-height:1.75em}html input[type=button]{-webkit-appearance:button;cursor:pointer}html input[disabled]{cursor:default}body{margin:0;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;-moz-font-feature-settings:"liga" on}article{display:block}aside{display:block}details{display:block}figcaption{display:block}figure{display:block;margin:1em 40px}footer{display:block}header{display:block}hgroup{display:block}main{display:block}menu{display:block}nav{display:block}section{display:block}summary{display:block}audio{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}canvas{display:inline-block;vertical-align:baseline}progress{display:inline-block;vertical-align:baseline}video{display:inline-block;vertical-align:baseline}[hidden]{display:none}template{display:none}a{background-color:transparent;text-decoration:none;color:#1d73a2;-webkit-transition:all .2s;transition:all .2s;margin:0;padding:0;cursor:pointer}a:active{outline:0}a:hover{outline:0;color:#175c82}abbr[title]{border-bottom:1px dotted}b{font-weight:700;margin:0;padding:0}strong{font-weight:700;margin:0;padding:0}dfn{font-style:italic;margin:0;padding:0}h1{font-size:2em;margin:.67em 0;margin-top:.942400822452556em;line-height:1.130880986943067em;margin-bottom:.188480164490511em;margin:0;padding:0;font-family:Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;color:#111;clear:both}mark{background:#ff0;color:#000}small{font-size:80%;margin:0;padding:0;line-height:0}sub{bottom:-.25em;margin:0;padding:0;line-height:0}sup{top:-.5em;margin:0;padding:0;line-height:0}sup a .fa{font-size:1em}img{border:0;margin:0;padding:0}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto;padding:.875em;margin-bottom:1.75em;background:#222;line-height:1;color:#ff0;border-radius:3px;font-size:10px;font-family:monospace;font-size:1em;margin:0;padding:0;margin-bottom:1.75em}pre code{padding:0}code{font-family:monospace;font-size:1em;margin:0;padding:0;font-family:Courier New,Courier,Lucida Sans Typewriter,Lucida Typewriter,monospace;padding:.0875em .2625em;line-height:0}kbd{font-family:monospace;font-size:1em;margin:0;padding:0}samp{font-family:monospace;font-size:1em;margin:0;padding:0}button{overflow:visible;text-transform:none;-webkit-appearance:button;cursor:pointer;display:block;cursor:pointer;font-size:12px;padding:.4375em 1.75em;margin-bottom:1.18125em}input{line-height:normal}input:focus{background:#ff0}optgroup{font-weight:700}select{text-transform:none;outline:none;border:1px solid #ddd;border-radius:3px;padding:10px 12px;width:calc(100% - 24px);margin:0 auto 1em;width:100%;height:35px}textarea{overflow:auto;display:block;max-width:100%;padding:.4375em;font-size:12px;margin-bottom:1.18125em;outline:none;border:1px solid #ddd;border-radius:3px;padding:10px 12px;width:calc(100% - 24px);margin:0 auto 1em}input[type=reset]{-webkit-appearance:button;cursor:pointer}input[type=submit]{-webkit-appearance:button;cursor:pointer;display:block;cursor:pointer;font-size:12px;padding:.4375em 1.75em;margin-bottom:1.18125em}button[disabled]{cursor:default}button::-moz-focus-inner{border:0;padding:0}input::-moz-focus-inner{border:0;padding:0}input[type=checkbox]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button{height:auto}input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button{-webkit-appearance:none}input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em;padding:.875em 1.75em 1.75em;border-width:1px;border-style:solid;max-width:100%;margin-bottom:1.8375em;margin:0;padding:0}fieldset button{margin-bottom:0}fieldset input[type=submit]{margin-bottom:0}legend{border:0;padding:0;color:#111;font-weight:700;margin:0;padding:0}table{width:100%;border-spacing:0;border-collapse:collapse;margin-bottom:2.1875em;margin:0;padding:0;margin-bottom:1.75em}td{padding:0;margin:0;padding:0;padding:.21875em .875em}th{padding:0;margin:0;padding:0;text-align:left;color:#111;padding:.21875em .875em}@media (min-width: 600px){html{font-size:12px}h1{font-size:calc(27.85438995234061px +18.56959 *((100vw - 600px) / 540))}h2{font-size:calc(23.53700340860508px +15.69134 *((100vw - 600px) / 540))}h3{font-size:calc(19.888804974891777px +13.2592 *((100vw - 600px) / 540))}h4{font-size:calc(16.806071548796314px +11.20405 *((100vw - 600px) / 540))}h5{font-size:calc(14.201156945318074px +9.46744 *((100vw - 600px) / 540))}h6{font-size:calc(12px +8 *((100vw - 600px) / 540))}}abbr{margin:0;padding:0;border-bottom:1px dotted currentColor;cursor:help}acronym{margin:0;padding:0;border-bottom:1px dotted currentColor;cursor:help}address{margin:0;padding:0;margin-bottom:1.75em;font-style:normal}big{margin:0;padding:0;line-height:0}blockquote{margin:0;padding:0;margin-bottom:1.75em;font-style:italic}blockquote cite{display:block;font-style:normal}caption{margin:0;padding:0}center{margin:0;padding:0}cite{margin:0;padding:0}dd{margin:0;padding:0}del{margin:0;padding:0}dl{margin:0;padding:0;margin-bottom:1.75em}dt{margin:0;padding:0;color:#111;font-weight:700}em{margin:0;padding:0}form{margin:0;padding:0}h2{margin:0;padding:0;font-family:Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;color:#111;clear:both;font-size:23.53700340860508px;margin-top:1.115265165420465em;line-height:1.338318198504558em;margin-bottom:.251483121980101em}h3{margin:0;padding:0;font-family:Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;color:#111;clear:both;font-size:19.888804974891777px;margin-top:1.319837970815179em;line-height:1.583805564978215em;margin-bottom:.303784103173448em}h4{margin:0;padding:0;font-family:Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;color:#111;clear:both;font-size:16.806071548796314px;margin-top:1.561935513828041em;line-height:1.87432261659365em;margin-bottom:.368150361036632em}h5{margin:0;padding:0;font-family:Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;color:#111;clear:both;font-size:14.201156945318074px;margin-top:1.84844094752817em;line-height:2.218129137033805em;margin-bottom:.369688189505634em}h6{margin:0;padding:0;font-family:Roboto,Helvetica Neue,Helvetica,Arial,sans-serif;font-weight:500;color:#111;clear:both;font-size:12px;margin-top:2.1875em;line-height:2.625em;margin-bottom:.619791666666667em}i{margin:0;padding:0}ins{margin:0;padding:0}label{margin:0;padding:0;display:block;padding-bottom:.21875em;margin-bottom:-.21875em;cursor:pointer}li{margin:0;padding:0}ol{margin:0;padding:0;margin-bottom:1.75em;padding-left:1.4em}p{margin:0;padding:0;margin-bottom:1.75em;margin:0 0 1rem 0}q{margin:0;padding:0}s{margin:0;padding:0}strike{margin:0;padding:0}tbody{margin:0;padding:0}tfoot{margin:0;padding:0}thead{margin:0;padding:0}tr{margin:0;padding:0}tt{margin:0;padding:0}u{margin:0;padding:0}ul{margin:0;padding:0;margin-bottom:1.75em;padding-left:1.1em}var{margin:0;padding:0}@media (min-width: 1140px){h1{font-size:46.423983253901014px;margin-top:.942400822452556em;line-height:1.130880986943067em;margin-bottom:.188480164490511em}h2{font-size:39.228339014341806px;margin-top:1.115265165420465em;line-height:1.338318198504558em;margin-bottom:.240111086421698em}h3{font-size:33.14800829148629px;margin-top:1.319837970815179em;line-height:1.583805564978215em;margin-bottom:.287857499569283em}h4{font-size:28.01011924799386px;margin-top:1.561935513828041em;line-height:1.87432261659365em;margin-bottom:.345845057728222em}h5{font-size:23.668594908863454px;margin-top:1.84844094752817em;line-height:2.218129137033805em;margin-bottom:.369688189505634em}h6{font-size:20px;margin-top:2.1875em;line-height:2.625em;margin-bottom:.473958333333333em}fieldset{margin-bottom:2.078125em}input[type=email]{font-size:20px;margin-bottom:.5140625em}input[type=password]{font-size:20px;margin-bottom:.5140625em}input[type=text]{font-size:20px;margin-bottom:.5140625em}textarea{font-size:20px;margin-bottom:.5140625em}button{font-size:20px;margin-bottom:1.3125em}input[type=submit]{font-size:20px;margin-bottom:1.3125em}table{margin-bottom:2.05625em}th{padding:.4375em .875em}td{padding:.4375em .875em}}input[type=email]{display:block;max-width:100%;padding:.4375em;font-size:12px;margin-bottom:1.18125em}input[type=password]{display:block;max-width:100%;padding:.4375em;font-size:12px;margin-bottom:1.18125em}input[type=text]{display:block;max-width:100%;padding:.4375em;font-size:12px;margin-bottom:1.18125em}.master{background-image:url("../img/background.png");background-size:cover;background-position:top;min-height:100vh;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center;-ms-flex-align:center;-webkit-box-align:center;align-items:center}.box{width:450px;border-radius:0 0 3px 3px;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-box-shadow:0 10px 10px rgba(0,0,0,0.19),0 6px 3px rgba(0,0,0,0.23);box-shadow:0 10px 10px rgba(0,0,0,0.19),0 6px 3px rgba(0,0,0,0.23)}.header{background-color:#357295;padding:30px 30px 40px;border-radius:3px 3px 0 0;text-align:center}.header__step{font-weight:300;text-transform:uppercase;font-size:14px;letter-spacing:1.1px;margin:0 0 10px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#fff}.header__title{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:#fff;font-weight:400;font-size:20px;margin:0 0 15px}.step{position:relative;z-index:1;padding-left:0;list-style:none;margin-bottom:0;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-direction:row-reverse;-webkit-box-orient:horizontal;-webkit-box-direction:reverse;flex-direction:row-reverse;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center;-ms-flex-align:center;-webkit-box-align:center;align-items:center;margin-top:-20px}.step__divider{background-color:#cacfd2;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;width:60px;height:3px}.step__divider:first-child{-ms-flex:1 0 auto;-webkit-box-flex:1;flex:1 0 auto}.step__divider:last-child{-ms-flex:1 0 auto;-webkit-box-flex:1;flex:1 0 auto}.step__icon{background-color:#cacfd2;font-style:normal;width:40px;height:40px;display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center;-ms-flex-align:center;-webkit-box-align:center;align-items:center;border-radius:50%;color:#fff}.step__icon.welcome:before{content:'\f144';font-family:Ionicons}.step__icon.requirements:before{content:'\f127';font-family:Ionicons}.step__icon.permissions:before{content:'\f296';font-family:Ionicons}.step__icon.database:before{content:'\f454';font-family:Ionicons}.step__icon.update:before{content:'\f2bf';font-family:Ionicons}.main{margin-top:-20px;background-color:#fff;border-radius:0 0 3px 3px;padding:40px 40px 30px}.buttons{text-align:center}.buttons .button{margin:.5em}.buttons--right{text-align:right}.button{display:inline-block;background-color:#34a0db;border-radius:2px;padding:7px 20px;color:#fff;-webkit-box-shadow:0 1px 1.5px rgba(0,0,0,0.12),0 1px 1px rgba(0,0,0,0.24);box-shadow:0 1px 1.5px rgba(0,0,0,0.12),0 1px 1px rgba(0,0,0,0.24);text-decoration:none;outline:none;border:none;-webkit-transition:background-color .2s ease, -webkit-box-shadow .2s ease;transition:background-color .2s ease, -webkit-box-shadow .2s ease;transition:box-shadow .2s ease, background-color .2s ease;transition:box-shadow .2s ease, background-color .2s ease, -webkit-box-shadow .2s ease;cursor:pointer}.button:hover{color:#fff;-webkit-box-shadow:0 10px 10px rgba(0,0,0,0.19),0 6px 3px rgba(0,0,0,0.23);box-shadow:0 10px 10px rgba(0,0,0,0.19),0 6px 3px rgba(0,0,0,0.23);background-color:#2490cb}.button--light{padding:3px 16px;font-size:16px;border-top:1px solid #eee;color:#222;background:#fff}.button--light:hover{color:#222;background:#fff;-webkit-box-shadow:0 3px 3px rgba(0,0,0,0.16),0 3px 3px rgba(0,0,0,0.23);box-shadow:0 3px 3px rgba(0,0,0,0.16),0 3px 3px rgba(0,0,0,0.23)}.list{padding-left:0;list-style:none;margin-bottom:0;margin:20px 0 35px;border:1px solid rgba(0,0,0,0.12);border-radius:2px}.list .list__item.list__title{background:rgba(0,0,0,0.12)}.list .list__item.list__title.success span{color:green}.list .list__item.list__title.success .fa:before{color:green}.list .list__item.list__title.error span{color:red}.list .list__item.list__title.error .fa:before{color:red}.list__item{position:relative;overflow:hidden;padding:7px 20px;border-bottom:1px solid rgba(0,0,0,0.12)}.list__item:first-letter{text-transform:uppercase}.list__item:last-child{border-bottom:none}.list__item .fa.row-icon:before{display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center;-ms-flex-align:center;-webkit-box-align:center;align-items:center;padding:7px 20px;position:absolute;top:0;right:0;bottom:0}.list__item.success .fa:before{color:#2ecc71}.list__item.error .fa:before{color:#e74c3c}.list__item--permissions:before{content:'' !important}.list__item--permissions span{display:-ms-flexbox;display:-webkit-box;display:flex;-ms-flex-pack:center;-webkit-box-pack:center;justify-content:center;-ms-flex-align:center;-webkit-box-align:center;align-items:center;padding:7px 20px;position:absolute;top:0;right:0;bottom:0;background-color:#f5f5f5;font-weight:700;font-size:16px}.list__item--permissions span:before{margin-right:7px;font-weight:400}.list__item--permissions.success i:before{color:#2ecc71;vertical-align:1px}.list__item--permissions.error i:before{color:#e74c3c;vertical-align:1px}.textarea{-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;font-size:14px;line-height:25px;height:150px;outline:none;border:1px solid rgba(0,0,0,0.2)}.textarea ~ .button{margin-bottom:35px}.text-center{text-align:center}.form-control{height:14px;width:100%}.has-error{color:red}.has-error input{color:#000;border:1px solid red}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}input[type='text']{outline:none;border:1px solid #ddd;border-radius:3px;padding:10px 12px;width:calc(100% - 24px);margin:0 auto 1em}input[type='password']{outline:none;border:1px solid #ddd;border-radius:3px;padding:10px 12px;width:calc(100% - 24px);margin:0 auto 1em}input[type='url']{outline:none;border:1px solid #ddd;border-radius:3px;padding:10px 12px;width:calc(100% - 24px);margin:0 auto 1em}input[type='number']{outline:none;border:1px solid #ddd;border-radius:3px;padding:10px 12px;width:calc(100% - 24px);margin:0 auto 1em}.tabs{padding:0}.tabs .tab-input{display:none}.tabs .tab-input:checked+.tab-label{background-color:#fff;color:#333}.tabs .tab-label{color:#ddd;cursor:pointer;float:left;padding:1em;text-align:center;-webkit-transition:all 0.1s ease-in-out;transition:all 0.1s ease-in-out}.tabs .tab-label:hover{color:#333}.tabs .tabs-wrap{clear:both}.tabs .tab{display:none}.tabs .tab>*:last-child{margin-bottom:0}.float-left{float:left}.float-right{float:right}.buttons-container{min-height:37px;margin:1em 0 0}.block{-webkit-box-shadow:0 3px 1px #a9a9a9;box-shadow:0 3px 1px #a9a9a9}.block input[type='radio']{width:100%;display:none}.block input[type='radio']+label{background:teal;color:#fff;padding-top:5px;-webkit-transition:all 0.1s ease-in-out;transition:all 0.1s ease-in-out}.block input[type='radio']+label:hover{background:#144242;color:#fff;padding-top:5px}.block input[type='radio']:checked+label{background-color:#a9a9a9}.block input[type='radio']:checked ~ .info{height:200px;overflow-y:auto;-webkit-transition:all 0.3s ease-in-out;transition:all 0.3s ease-in-out}.block input[type='radio'] ~ .info>div{padding-top:15px}.block label{width:450px;max-width:100%;cursor:pointer}.block span{font-family:Arial;font-weight:700;display:block;padding:10px 12px 12px 15px;margin:0;cursor:pointer}.info{background:#fff;color:#222;width:100%;height:0;line-height:2;padding-left:15px;padding-right:15px;display:block;overflow:hidden;-webkit-box-sizing:border-box;box-sizing:border-box;-webkit-transition:.3s ease-out;transition:.3s ease-out}::-moz-selection{background:#222;color:#fff}::selection{background:#222;color:#fff}::-webkit-scrollbar{width:12px}::-webkit-scrollbar-track{-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,0.1);border-radius:0}::-webkit-scrollbar-thumb{border-radius:0;background:#ccc;-webkit-box-shadow:inset 0 0 6px rgba(0,0,0,0.3)}.margin-bottom-1{margin-bottom:1em}.margin-bottom-2{margin-bottom:1em}.margin-top-1{margin-top:1em}.margin-top-2{margin-top:1em}.alert{margin:0 0 10px;font-size:1.1em;background-color:#f5f5f5;border-radius:3px;padding:10px;position:relative}.alert.alert-danger{background:red;color:#fff;padding:10px 20px 15px}.alert.alert-danger h4{color:#fff;margin:0}.alert.alert-danger ul{margin:0}.alert .close{width:25px;height:25px;padding:0;margin:0;-webkit-box-shadow:none;box-shadow:none;border:2px solid red;outline:none;float:right;border-radius:50%;position:absolute;right:0;top:0;background-color:transparent;cursor:pointer;-webkit-transition:all 0.1s ease-in-out;transition:all 0.1s ease-in-out}.alert .close:hover{background-color:#fff;color:red}svg:not(:root){overflow:hidden}.step__item.active .step__icon,.step__item.active ~ .step__divider,.step__item.active ~ .step__item .step__icon{background-color:#34a0db;-webkit-transition:all 0.1s ease-in-out;transition:all 0.1s ease-in-out}.step__item.active .step__icon:hover,.step__item.active ~ .step__item .step__icon:hover{background-color:#3d657b}.form-group.has-error select{border-color:red}.form-group.has-error textarea{border-color:red}.form-group.has-error input[type='text']{border-color:red}.form-group.has-error input[type='password']{border-color:red}.form-group.has-error input[type='url']{border-color:red}.form-group.has-error input[type='number']{border-color:red}.form-group.has-error .error-block{margin:-12px 0 0;display:block;width:100%;font-size:.9em;color:red;font-weight:500}.tabs-full .tab-label{display:table-cell;float:none;width:1%}#tab1:checked ~ .tabs-wrap #tab1content{display:block}#tab2:checked ~ .tabs-wrap #tab2content{display:block}#tab3:checked ~ .tabs-wrap #tab3content{display:block}#tab4:checked ~ .tabs-wrap #tab4content{display:block}#tab5:checked ~ .tabs-wrap #tab5content{display:block}.github img{position:absolute;top:0;right:0;border:0}#tab3content .block:first-child{border-radius:3px 3px 0 0}#tab3content .block:last-child{border-radius:0 0 3px 3px} + +/*# sourceMappingURL=style.min.css.map */ \ No newline at end of file diff --git a/public/installer/css/style.min.css.map b/public/installer/css/style.min.css.map new file mode 100755 index 000000000..41aa17eea --- /dev/null +++ b/public/installer/css/style.min.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "CAAA;;;IAGG,DCqBK,iFAAW,CCrBnB,UAWC,CAVC,WAAW,CAAE,aAAa,CAC1B,GAAG,CAAE,+CAAgE,CACrE,GAAG,CAAE,wWAI8F,CAEnG,WAAW,CAAE,MAAM,CACnB,UAAU,CAAE,MAAM,CCVpB,GAAmB,CACjB,OAAO,CAAE,YAAY,CACrB,IAAI,CAAE,uCAA8E,CACpF,SAAS,CAAE,OAAO,CAClB,cAAc,CAAE,IAAI,CACpB,sBAAsB,CAAE,WAAW,CACnC,uBAAuB,CAAE,SAAS,CCLpC,MAAsB,CACpB,SAAS,CAAE,cAAS,CACpB,WAAW,CAAE,KAAS,CACtB,cAAc,CAAE,IAAI,CAEtB,MAAsB,CAAE,SAAS,CAAE,GAAG,CACtC,MAAsB,CAAE,SAAS,CAAE,GAAG,CACtC,MAAsB,CAAE,SAAS,CAAE,GAAG,CACtC,MAAsB,CAAE,SAAS,CAAE,GAAG,CCVtC,MAAsB,CACpB,KAAK,CAAE,cAAW,CAClB,UAAU,CAAE,MAAM,CCDpB,MAAsB,CACpB,YAAY,CAAE,CAAC,CACf,WAAW,CCMU,cAAS,CDL9B,eAAe,CAAE,IAAI,CACrB,SAAK,CAAE,QAAQ,CAAE,QAAQ,CAE3B,MAAsB,CACpB,QAAQ,CAAE,QAAQ,CAClB,IAAI,CAAE,eAAa,CACnB,KAAK,CCDgB,cAAS,CDE9B,GAAG,CAAE,aAAU,CACf,UAAU,CAAE,MAAM,CAClB,YAAuB,CACrB,IAAI,CAAE,eAA0B,CEbpC,UAA0B,CACxB,OAAO,CAAE,gBAAgB,CACzB,MAAM,CAAE,iBAA4B,CACpC,aAAa,CAAE,IAAI,CAGrB,aAA6B,CAAE,KAAK,CAAE,IAAI,CAC1C,cAA8B,CAAE,KAAK,CAAE,KAAK,CAG1C,gBAA8B,CAAE,YAAY,CAAE,IAAI,CAClD,iBAA+B,CAAE,WAAW,CAAE,IAAI,CAIpD,WAAY,CAAE,KAAK,CAAE,KAAK,CAC1B,UAAW,CAAE,KAAK,CAAE,IAAI,CAGtB,aAAY,CAAE,YAAY,CAAE,IAAI,CAChC,cAAa,CAAE,WAAW,CAAE,IAAI,CCpBlC,QAAwB,CACtB,iBAAiB,CAAE,0BAA0B,CACrC,SAAS,CAAE,0BAA0B,CAG/C,SAAyB,CACvB,iBAAiB,CAAE,4BAA4B,CACvC,SAAS,CAAE,4BAA4B,CAGjD,0BASC,CARC,EAAG,CACD,iBAAiB,CAAE,YAAY,CACvB,SAAS,CAAE,YAAY,CAEjC,IAAK,CACH,iBAAiB,CAAE,cAAc,CACzB,SAAS,CAAE,cAAc,EAIrC,kBASC,CARC,EAAG,CACD,iBAAiB,CAAE,YAAY,CACvB,SAAS,CAAE,YAAY,CAEjC,IAAK,CACH,iBAAiB,CAAE,cAAc,CACzB,SAAS,CAAE,cAAc,EC5BrC,aAA8B,CCW5B,UAAU,CAAE,0DAAqE,CACjF,iBAAiB,CAAE,aAAgB,CAC/B,aAAa,CAAE,aAAgB,CAC3B,SAAS,CAAE,aAAgB,CDbrC,cAA8B,CCU5B,UAAU,CAAE,0DAAqE,CACjF,iBAAiB,CAAE,cAAgB,CAC/B,aAAa,CAAE,cAAgB,CAC3B,SAAS,CAAE,cAAgB,CDZrC,cAA8B,CCS5B,UAAU,CAAE,0DAAqE,CACjF,iBAAiB,CAAE,cAAgB,CAC/B,aAAa,CAAE,cAAgB,CAC3B,SAAS,CAAE,cAAgB,CDVrC,mBAAmC,CCcjC,UAAU,CAAE,oEAA+E,CAC3F,iBAAiB,CAAE,YAAoB,CACnC,aAAa,CAAE,YAAoB,CAC/B,SAAS,CAAE,YAAoB,CDhBzC,iBAAmC,CCajC,UAAU,CAAE,oEAA+E,CAC3F,iBAAiB,CAAE,YAAoB,CACnC,aAAa,CAAE,YAAoB,CAC/B,SAAS,CAAE,YAAoB,CDXzC,+GAIuC,CACrC,MAAM,CAAE,IAAI,CEfd,SAAyB,CACvB,QAAQ,CAAE,QAAQ,CAClB,OAAO,CAAE,YAAY,CACrB,KAAK,CAAE,GAAG,CACV,MAAM,CAAE,GAAG,CACX,WAAW,CAAE,GAAG,CAChB,cAAc,CAAE,MAAM,CAExB,yBAAyD,CACvD,QAAQ,CAAE,QAAQ,CAClB,IAAI,CAAE,CAAC,CACP,KAAK,CAAE,IAAI,CACX,UAAU,CAAE,MAAM,CAEpB,YAA4B,CAAE,WAAW,CAAE,OAAO,CAClD,YAA4B,CAAE,SAAS,CAAE,GAAG,CAC5C,WAA2B,CAAE,KAAK,CLTZ,IAAI,CMP1B,gBAAgC,CAAE,OAAO,CNwU1B,GAAO,CMvUtB,gBAAgC,CAAE,OAAO,CN2d1B,GAAO,CM1dtB,iBAAiC,CAAE,OAAO,CN0jB1B,GAAO,CMzjBvB,qBAAqC,CAAE,OAAO,CNsO1B,GAAO,CMrO3B,gBAAgC,CAAE,OAAO,CNuW1B,GAAO,CMtWtB,eAA+B,CAAE,OAAO,CNknB1B,GAAO,CMjnBrB,iBAAiC,CAAE,OAAO,CNsnB1B,GAAO,CMrnBvB,eAA+B,CAAE,OAAO,CNytB1B,GAAO,CMxtBrB,eAA+B,CAAE,OAAO,CNmR1B,GAAO,CMlRrB,mBAAmC,CAAE,OAAO,CNupB1B,GAAO,CMtpBzB,aAA6B,CAAE,OAAO,CNqpB1B,GAAO,CMppBnB,kBAAkC,CAAE,OAAO,CNspB1B,GAAO,CMrpBxB,gBAAgC,CAAE,OAAO,CNyI1B,GAAO,CMxItB,mDAEgC,CAAE,OAAO,CNqqB1B,GAAO,CMpqBtB,sBAAsC,CAAE,OAAO,CN8iB1B,GAAO,CM7iB5B,uBAAuC,CAAE,OAAO,CN4iB1B,GAAO,CM3iB7B,oBAAoC,CAAE,OAAO,CN4f1B,GAAO,CM3f1B,iBAAiC,CAAE,OAAO,CNikB1B,GAAO,CMhkBvB,8BAC8B,CAAE,OAAO,CNgK1B,GAAO,CM/JpB,kBAAkC,CAAE,OAAO,CN+qB1B,GAAO,CM9qBxB,eAA+B,CAAE,OAAO,CNwV1B,GAAO,CMvVrB,iBAAiC,CAAE,OAAO,CNuP1B,GAAO,CMtPvB,kBAAkC,CAAE,OAAO,CNgJ1B,GAAO,CM/IxB,eAA+B,CAAE,OAAO,CNmhB1B,GAAO,CMlhBrB,mBAAmC,CAAE,OAAO,CNgM1B,GAAO,CM/LzB,8BAA8C,CAAE,OAAO,CNY1B,GAAO,CMXpC,4BAA4C,CAAE,OAAO,CNc1B,GAAO,CMblC,gBAAgC,CAAE,OAAO,CNqW1B,GAAO,CMpWtB,wBAAwC,CAAE,OAAO,CNwe1B,GAAO,CMve9B,yCACiC,CAAE,OAAO,CNsgB1B,GAAO,CMrgBvB,kBAAkC,CAAE,OAAO,CNggB1B,GAAO,CM/fxB,mBAAmC,CAAE,OAAO,CNwY1B,GAAO,CMvYzB,eAA+B,CAAE,OAAO,CN2Y1B,GAAO,CM1YrB,eAA+B,CAAE,OAAO,CN4P1B,GAAO,CM3PrB,qBAAqC,CAAE,OAAO,CNoU1B,GAAO,CMnU3B,qBAAqC,CAAE,OAAO,CNitB1B,GAAO,CMhtB3B,sBAAsC,CAAE,OAAO,CN+sB1B,GAAO,CM9sB5B,oBAAoC,CAAE,OAAO,CNgtB1B,GAAO,CM/sB1B,iBAAiC,CAAE,OAAO,CNye1B,GAAO,CMxevB,kBAAkC,CAAE,OAAO,CNwB1B,GAAO,CMvBxB,cAA8B,CAAE,OAAO,CNymB1B,GAAO,CMxmBpB,eAA+B,CAAE,OAAO,CNymB1B,GAAO,CMxmBrB,eAA+B,CAAE,OAAO,CNyD1B,GAAO,CMxDrB,mBAAmC,CAAE,OAAO,CNyD1B,GAAO,CMxDzB,gBAAgC,CAAE,OAAO,CN+d1B,GAAO,CM9dtB,iBAAiC,CAAE,OAAO,CN2E1B,GAAO,CM1EvB,eAA+B,CAAE,OAAO,CN0P1B,GAAO,CMzPrB,eAA+B,CAAE,OAAO,CNiD1B,GAAO,CMhDrB,iBAAiC,CAAE,OAAO,CN0V1B,GAAO,CMzVvB,sBAAsC,CAAE,OAAO,CNwmB1B,GAAO,CMvmB5B,qBAAqC,CAAE,OAAO,CNwmB1B,GAAO,CMvmB3B,qBAAqC,CAAE,OAAO,CNpC1B,GAAO,CMqC3B,uBAAuC,CAAE,OAAO,CNvC1B,GAAO,CMwC7B,sBAAsC,CAAE,OAAO,CNrC1B,GAAO,CMsC5B,wBAAwC,CAAE,OAAO,CNxC1B,GAAO,CMyC9B,eAA+B,CAAE,OAAO,CN+W1B,GAAO,CM9WrB,oCACkC,CAAE,OAAO,CN2a1B,GAAO,CM1axB,iBAAiC,CAAE,OAAO,CNsU1B,GAAO,CMrUvB,uBAAuC,CAAE,OAAO,CNkrB1B,GAAO,CMjrB7B,sDAEoC,CAAE,OAAO,CN0b1B,GAAO,CMzb1B,iBAAiC,CAAE,OAAO,CNkb1B,GAAO,CMjbvB,qBAAqC,CAAE,OAAO,CNwX1B,GAAO,CMvX3B,iBAAiC,CAAE,OAAO,CNtD1B,GAAO,CMuDvB,eAA+B,CAAE,OAAO,CNmnB1B,GAAO,CMlnBrB,0CAC0C,CAAE,OAAO,CN+a1B,GAAO,CM9ahC,yBAAyC,CAAE,OAAO,CN8f1B,GAAO,CM7f/B,yBAAyC,CAAE,OAAO,CN+E1B,GAAO,CM9E/B,iBAAiC,CAAE,OAAO,CNzB1B,GAAO,CM0BvB,wBAAwC,CAAE,OAAO,CNmjB1B,GAAO,CMljB9B,wBAAwC,CAAE,OAAO,CNqL1B,GAAO,CMpL9B,mBAAmC,CAAE,OAAO,CNlB1B,GAAO,CMmBzB,eAA+B,CAAE,OAAO,CNsb1B,GAAO,CMrbrB,gBAAgC,CAAE,OAAO,CNga1B,GAAO,CM/ZtB,eAA+B,CAAE,OAAO,CNmjB1B,GAAO,CMljBrB,kBAAkC,CAAE,OAAO,CN+N1B,GAAO,CM9NxB,uBAAuC,CAAE,OAAO,CNgL1B,GAAO,CM/K7B,uBAAuC,CAAE,OAAO,CN4iB1B,GAAO,CM3iB7B,gBAAgC,CAAE,OAAO,CN+I1B,GAAO,CM9ItB,uBAAuC,CAAE,OAAO,CNyE1B,GAAO,CMxE7B,wBAAwC,CAAE,OAAO,CNyE1B,GAAO,CMxE9B,sBAAsC,CAAE,OAAO,CNkb1B,GAAO,CMjb5B,uBAAuC,CAAE,OAAO,CNuX1B,GAAO,CMtX7B,uBAAuC,CAAE,OAAO,CN2lB1B,GAAO,CM1lB7B,uBAAuC,CAAE,OAAO,CN2D1B,GAAO,CM1D7B,0BAA0C,CAAE,OAAO,CNyb1B,GAAO,CMxbhC,sBAAsC,CAAE,OAAO,CN0S1B,GAAO,CMzS5B,qBAAqC,CAAE,OAAO,CN0G1B,GAAO,CMzG3B,yBAAyC,CAAE,OAAO,CNulB1B,GAAO,CMtlB/B,yBAAyC,CAAE,OAAO,CNuD1B,GAAO,CMtD/B,cAA8B,CAAE,OAAO,CNnC1B,GAAO,CMoCpB,qBAAqC,CAAE,OAAO,CNnD1B,GAAO,CMoD3B,sBAAsC,CAAE,OAAO,CNnD1B,GAAO,CMoD5B,mBAAmC,CAAE,OAAO,CNnD1B,GAAO,CMoDzB,qBAAqC,CAAE,OAAO,CNvD1B,GAAO,CMwD3B,wCACgC,CAAE,OAAO,CN4d1B,GAAO,CM3dtB,iBAAiC,CAAE,OAAO,CN8I1B,GAAO,CM7IvB,mBAAmC,CAAE,OAAO,CNsF1B,GAAO,CMrFzB,eAA+B,CAAE,OAAO,CN+Z1B,GAAO,CM9ZrB,gBAAgC,CAAE,OAAO,CNoW1B,GAAO,CMnWtB,mBAAmC,CAAE,OAAO,CNpD1B,GAAO,CMqDzB,6BAA6C,CAAE,OAAO,CNuI1B,GAAO,CMtInC,eAA+B,CAAE,OAAO,CNkN1B,GAAO,CMjNrB,eAA+B,CAAE,OAAO,CN0S1B,GAAO,CMzSrB,eAA+B,CAAE,OAAO,CN6K1B,GAAO,CM5KrB,cAA8B,CAAE,OAAO,CNyI1B,GAAO,CMxIpB,oBAAoC,CAAE,OAAO,CNyI1B,GAAO,CMxI1B,kDAC+C,CAAE,OAAO,CNiI1B,GAAO,CMhIrC,gBAAgC,CAAE,OAAO,CN+Y1B,GAAO,CM9YtB,mBAAmC,CAAE,OAAO,CNA1B,GAAO,CMCzB,iBAAiC,CAAE,OAAO,CNoa1B,GAAO,CMnavB,kBAAkC,CAAE,OAAO,CNgE1B,GAAO,CM/DxB,iBAAiC,CAAE,OAAO,CN6T1B,GAAO,CM5TvB,qBAAqC,CAAE,OAAO,CNuC1B,GAAO,CMtC3B,uBAAuC,CAAE,OAAO,CNmC1B,GAAO,CMlC7B,kBAAkC,CAAE,OAAO,CN+a1B,GAAO,CM9axB,wBAAwC,CAAE,OAAO,CNkd1B,GAAO,CMjd9B,iBAAiC,CAAE,OAAO,CN0K1B,GAAO,CMzKvB,sBAAsC,CAAE,OAAO,CN2K1B,GAAO,CM1K5B,mBAAmC,CAAE,OAAO,CN3E1B,GAAO,CM4EzB,mBAAmC,CAAE,OAAO,CN7E1B,GAAO,CM8EzB,2CACoC,CAAE,OAAO,CNlE1B,GAAO,CMmE1B,yBAAyC,CAAE,OAAO,CN+kB1B,GAAO,CM9kB/B,0BAA0C,CAAE,OAAO,CN4H1B,GAAO,CM3HhC,uBAAuC,CAAE,OAAO,CNT1B,GAAO,CMU7B,cAA8B,CAAE,OAAO,CN2Q1B,GAAO,CM1QpB,gCAC+B,CAAE,OAAO,CN6C1B,GAAO,CM5CrB,mBAAmC,CAAE,OAAO,CNkD1B,GAAO,CMjDzB,sBAAsC,CAAE,OAAO,CNsiB1B,GAAO,CMriB5B,wBAAwC,CAAE,OAAO,CNoiB1B,GAAO,CMniB9B,oBAAoC,CAAE,OAAO,CN2e1B,GAAO,CM1e1B,kBAAkC,CAAE,OAAO,CN8N1B,GAAO,CM7NxB,mBAAmC,CAAE,OAAO,CNoc1B,GAAO,CMnczB,0BAA0C,CAAE,OAAO,CNuR1B,GAAO,CMtRhC,qBAAqC,CAAE,OAAO,CN6hB1B,GAAO,CM5hB3B,wBAAwC,CAAE,OAAO,CNsG1B,GAAO,CMrG9B,kBAAkC,CAAE,OAAO,CN8b1B,GAAO,CM7bxB,iBAAiC,CAAE,OAAO,CNqjB1B,GAAO,CMpjBvB,wBAAwC,CAAE,OAAO,CNgL1B,GAAO,CM/K9B,iBAAiC,CAAE,OAAO,CNukB1B,GAAO,CMtkBvB,kBAAkC,CAAE,OAAO,CNqQ1B,GAAO,CMpQxB,gBAAgC,CAAE,OAAO,CNiW1B,GAAO,CMhWtB,mBAAmC,CAAE,OAAO,CN2d1B,GAAO,CM1dzB,qBAAqC,CAAE,OAAO,CNjD1B,GAAO,CMkD3B,uBAAuC,CAAE,OAAO,CN+V1B,GAAO,CM9V7B,kBAAkC,CAAE,OAAO,CNsjB1B,GAAO,CMrjBxB,yCACmC,CAAE,OAAO,CNgG1B,GAAO,CM/FzB,iBAAiC,CAAE,OAAO,CNoK1B,GAAO,CMnKvB,iBAAiC,CAAE,OAAO,CN0jB1B,GAAO,CMzjBvB,sBAAsC,CAAE,OAAO,CNoC1B,GAAO,CMnC5B,8BAC8B,CAAE,OAAO,CN+Y1B,GAAO,CM9YpB,gBAAgC,CAAE,OAAO,CNoM1B,GAAO,CMnMtB,mBAAmC,CAAE,OAAO,CNrD1B,GAAO,CMsDzB,eAA+B,CAAE,OAAO,CNhF1B,GAAO,CMiFrB,sBAAsC,CAAE,OAAO,CNrB1B,GAAO,CMsB5B,uBAAuC,CAAE,OAAO,CNoL1B,GAAO,CMnL7B,sBAAsC,CAAE,OAAO,CNkL1B,GAAO,CMjL5B,oBAAoC,CAAE,OAAO,CNmL1B,GAAO,CMlL1B,sBAAsC,CAAE,OAAO,CN+K1B,GAAO,CM9K5B,4BAA4C,CAAE,OAAO,CNrI1B,GAAO,CMsIlC,6BAA6C,CAAE,OAAO,CNjI1B,GAAO,CMkInC,0BAA0C,CAAE,OAAO,CNjI1B,GAAO,CMkIhC,4BAA4C,CAAE,OAAO,CNzI1B,GAAO,CM0IlC,gBAAgC,CAAE,OAAO,CN2J1B,GAAO,CM1JtB,iBAAiC,CAAE,OAAO,CN6lB1B,GAAO,CM5lBvB,gBAAgC,CAAE,OAAO,CNqe1B,GAAO,CMpetB,iBAAiC,CAAE,OAAO,CNyG1B,GAAO,CMxGvB,oBAAoC,CAAE,OAAO,CNzE1B,GAAO,CM0E1B,qBAAqC,CAAE,OAAO,CNlI1B,GAAO,CMmI3B,iCACgC,CAAE,OAAO,CNijB1B,GAAO,CMhjBtB,gCAC+B,CAAE,OAAO,CN4O1B,GAAO,CM3OrB,gBAAgC,CAAE,OAAO,CNd1B,GAAO,CMetB,gBAAgC,CAAE,OAAO,CN0G1B,GAAO,CMzGtB,kCACmC,CAAE,OAAO,CN6X1B,GAAO,CM5XzB,kCACkC,CAAE,OAAO,CN2F1B,GAAO,CM1FxB,oBAAoC,CAAE,OAAO,CN6S1B,GAAO,CM5S1B,mCACmC,CAAE,OAAO,CNqG1B,GAAO,CMpGzB,iBAAiC,CAAE,OAAO,CNgb1B,GAAO,CM/avB,qDAE+B,CAAE,OAAO,CNlI1B,GAAO,CMmIrB,kBAAkC,CAAE,OAAO,CNsO1B,GAAO,CMrOxB,kBAAkC,CAAE,OAAO,CNoO1B,GAAO,CMnOxB,wBAAwC,CAAE,OAAO,CN+b1B,GAAO,CM9b9B,oBAAoC,CAAE,OAAO,CN2gB1B,GAAO,CM1gB1B,gBAAgC,CAAE,OAAO,CNuc1B,GAAO,CMtctB,gBAAgC,CAAE,OAAO,CNyO1B,GAAO,CMxOtB,gBAAgC,CAAE,OAAO,CN6f1B,GAAO,CM5ftB,oBAAoC,CAAE,OAAO,CNmT1B,GAAO,CMlT1B,2BAA2C,CAAE,OAAO,CNoT1B,GAAO,CMnTjC,6BAA6C,CAAE,OAAO,CNgI1B,GAAO,CM/HnC,sBAAsC,CAAE,OAAO,CN4H1B,GAAO,CM3H5B,gBAAgC,CAAE,OAAO,CNqQ1B,GAAO,CMpQtB,qBAAqC,CAAE,OAAO,CNpF1B,GAAO,CMqF3B,mBAAmC,CAAE,OAAO,CN9E1B,GAAO,CM+EzB,qBAAqC,CAAE,OAAO,CNrF1B,GAAO,CMsF3B,sBAAsC,CAAE,OAAO,CNrF1B,GAAO,CMsF5B,kBAAkC,CAAE,OAAO,CNhC1B,GAAO,CMiCxB,mCAC+B,CAAE,OAAO,CN0Y1B,GAAO,CMzYrB,yCACoC,CAAE,OAAO,CN8Y1B,GAAO,CM7Y1B,sCACmC,CAAE,OAAO,CN2Y1B,GAAO,CM1YzB,mBAAmC,CAAE,OAAO,CNU1B,GAAO,CMTzB,mBAAmC,CAAE,OAAO,CNuM1B,GAAO,CMtMzB,sCAC+B,CAAE,OAAO,CNqf1B,GAAO,CMpfrB,iCACgC,CAAE,OAAO,CNoF1B,GAAO,CMnFtB,0CACqC,CAAE,OAAO,CN+a1B,GAAO,CM9a3B,oBAAoC,CAAE,OAAO,CN7C1B,GAAO,CM8C1B,qBAAqC,CAAE,OAAO,CN1C1B,GAAO,CM2C3B,gCAC+B,CAAE,OAAO,CNpI1B,GAAO,CMqIrB,kBAAkC,CAAE,OAAO,CN6W1B,GAAO,CM5WxB,mBAAmC,CAAE,OAAO,CNye1B,GAAO,CMxezB,qCACoC,CAAE,OAAO,CNrE1B,GAAO,CMsE1B,sBAAsC,CAAE,OAAO,CNqL1B,GAAO,CMpL5B,mBAAmC,CAAE,OAAO,CNG1B,GAAO,CMFzB,yBAAyC,CAAE,OAAO,CNnE1B,GAAO,CMoE/B,uBAAuC,CAAE,OAAO,CNnE1B,GAAO,CMoE7B,kBAAkC,CAAE,OAAO,CNif1B,GAAO,CMhfxB,sBAAsC,CAAE,OAAO,CN8Y1B,GAAO,CM7Y5B,mBAAmC,CAAE,OAAO,CNyZ1B,GAAO,CMxZzB,iBAAiC,CAAE,OAAO,CN9J1B,GAAO,CM+JvB,iBAAiC,CAAE,OAAO,CNlE1B,GAAO,CMmEvB,kBAAkC,CAAE,OAAO,CN1C1B,GAAO,CM2CxB,sBAAsC,CAAE,OAAO,CN8B1B,GAAO,CM7B5B,qBAAqC,CAAE,OAAO,CN1I1B,GAAO,CM2I3B,qBAAqC,CAAE,OAAO,CNsH1B,GAAO,CMrH3B,oBAAoC,CAAE,OAAO,CNrO1B,GAAO,CMsO1B,iBAAiC,CAAE,OAAO,CN4M1B,GAAO,CM3MvB,sBAAsC,CAAE,OAAO,CNU1B,GAAO,CMT5B,eAA+B,CAAE,OAAO,CN3K1B,GAAO,CM4KrB,mBAAmC,CAAE,OAAO,CNuF1B,GAAO,CMtFzB,sBAAsC,CAAE,OAAO,CN2Q1B,GAAO,CM1Q5B,4BAA4C,CAAE,OAAO,CNrO1B,GAAO,CMsOlC,6BAA6C,CAAE,OAAO,CNrO1B,GAAO,CMsOnC,0BAA0C,CAAE,OAAO,CNrO1B,GAAO,CMsOhC,4BAA4C,CAAE,OAAO,CNzO1B,GAAO,CM0OlC,qBAAqC,CAAE,OAAO,CNrO1B,GAAO,CMsO3B,sBAAsC,CAAE,OAAO,CNrO1B,GAAO,CMsO5B,mBAAmC,CAAE,OAAO,CNrO1B,GAAO,CMsOzB,qBAAqC,CAAE,OAAO,CNzO1B,GAAO,CM0O3B,kBAAkC,CAAE,OAAO,CNpD1B,GAAO,CMqDxB,iBAAiC,CAAE,OAAO,CN4I1B,GAAO,CM3IvB,iBAAiC,CAAE,OAAO,CNwY1B,GAAO,CMvYvB,yCACiC,CAAE,OAAO,CNuM1B,GAAO,CMtMvB,mBAAmC,CAAE,OAAO,CNzG1B,GAAO,CM0GzB,qBAAqC,CAAE,OAAO,CNyQ1B,GAAO,CMxQ3B,sBAAsC,CAAE,OAAO,CNyQ1B,GAAO,CMxQ5B,kBAAkC,CAAE,OAAO,CN+V1B,GAAO,CM9VxB,iBAAiC,CAAE,OAAO,CN9G1B,GAAO,CM+GvB,sCACgC,CAAE,OAAO,CNoR1B,GAAO,CMnRtB,qBAAqC,CAAE,OAAO,CN+C1B,GAAO,CM9C3B,mBAAmC,CAAE,OAAO,CNmB1B,GAAO,CMlBzB,wBAAwC,CAAE,OAAO,CNoB1B,GAAO,CMnB9B,kBAAkC,CAAE,OAAO,CNqU1B,GAAO,CMpUxB,kBAAkC,CAAE,OAAO,CN2B1B,GAAO,CM1BxB,gBAAgC,CAAE,OAAO,CNgL1B,GAAO,CM/KtB,kBAAkC,CAAE,OAAO,CN2B1B,GAAO,CM1BxB,qBAAqC,CAAE,OAAO,CNuH1B,GAAO,CMtH3B,iBAAiC,CAAE,OAAO,CNM1B,GAAO,CMLvB,yBAAyC,CAAE,OAAO,CNI1B,GAAO,CMH/B,mBAAmC,CAAE,OAAO,CN6X1B,GAAO,CM5XzB,eAA+B,CAAE,OAAO,CNhH1B,GAAO,CMiHrB,8CACoC,CAAE,OAAO,CNuQ1B,GAAO,CMtQ1B,2EAEsC,CAAE,OAAO,CNsV1B,GAAO,CMrV5B,yBAAyC,CAAE,OAAO,CNwI1B,GAAO,CMvI/B,eAA+B,CAAE,OAAO,CNhG1B,GAAO,CMiGrB,oBAAoC,CAAE,OAAO,CNvH1B,GAAO,CMwH1B,yCACuC,CAAE,OAAO,CNtJ1B,GAAO,CMuJ7B,mBAAmC,CAAE,OAAO,CNyO1B,GAAO,CMxOzB,eAA+B,CAAE,OAAO,CN0F1B,GAAO,CMzFrB,sBAAsC,CAAE,OAAO,CN1D1B,GAAO,CM2D5B,sBAAsC,CAAE,OAAO,CNkW1B,GAAO,CMjW5B,oBAAoC,CAAE,OAAO,CN4V1B,GAAO,CM3V1B,iBAAiC,CAAE,OAAO,CNlE1B,GAAO,CMmEvB,uBAAuC,CAAE,OAAO,CNgO1B,GAAO,CM/N7B,qBAAqC,CAAE,OAAO,CN2J1B,GAAO,CM1J3B,2BAA2C,CAAE,OAAO,CN2J1B,GAAO,CM1JjC,iBAAiC,CAAE,OAAO,CNsR1B,GAAO,CMrRvB,qBAAqC,CAAE,OAAO,CN5L1B,GAAO,CM6L3B,4BAA4C,CAAE,OAAO,CNxB1B,GAAO,CMyBlC,iBAAiC,CAAE,OAAO,CNuP1B,GAAO,CMtPvB,iBAAiC,CAAE,OAAO,CN6I1B,GAAO,CM5IvB,8BAA8C,CAAE,OAAO,CN9J1B,GAAO,CM+JpC,+BAA+C,CAAE,OAAO,CN9J1B,GAAO,CM+JrC,4BAA4C,CAAE,OAAO,CN9J1B,GAAO,CM+JlC,8BAA8C,CAAE,OAAO,CNlK1B,GAAO,CMmKpC,gBAAgC,CAAE,OAAO,CN8D1B,GAAO,CM7DtB,eAA+B,CAAE,OAAO,CNrH1B,GAAO,CMsHrB,iBAAiC,CAAE,OAAO,CNvS1B,GAAO,CMwSvB,qBAAqC,CAAE,OAAO,CN2Z1B,GAAO,CM1Z3B,mBAAmC,CAAE,OAAO,CNhN1B,GAAO,CMiNzB,qBAAqC,CAAE,OAAO,CN7F1B,GAAO,CM8F3B,qBAAqC,CAAE,OAAO,CN7F1B,GAAO,CM8F3B,qBAAqC,CAAE,OAAO,CN+O1B,GAAO,CM9O3B,sBAAsC,CAAE,OAAO,CNiM1B,GAAO,CMhM5B,iBAAiC,CAAE,OAAO,CN6W1B,GAAO,CM5WvB,uBAAuC,CAAE,OAAO,CN0I1B,GAAO,CMzI7B,yBAAyC,CAAE,OAAO,CN0I1B,GAAO,CMzI/B,mBAAmC,CAAE,OAAO,CNqF1B,GAAO,CMpFzB,qBAAqC,CAAE,OAAO,CNmF1B,GAAO,CMlF3B,uBAAuC,CAAE,OAAO,CNnL1B,GAAO,CMoL7B,wBAAwC,CAAE,OAAO,CN0K1B,GAAO,CMzK9B,+BAA+C,CAAE,OAAO,CNpF1B,GAAO,CMqFrC,uBAAuC,CAAE,OAAO,CNwP1B,GAAO,CMvP7B,kBAAkC,CAAE,OAAO,CNjJ1B,GAAO,CMkJxB,qDAC8C,CAAE,OAAO,CN/M1B,GAAO,CMgNpC,iDAC4C,CAAE,OAAO,CN9M1B,GAAO,CM+MlC,uDAC+C,CAAE,OAAO,CNjN1B,GAAO,CMkNrC,8BAC8B,CAAE,OAAO,CNvG1B,GAAO,CMwGpB,cAA8B,CAAE,OAAO,CNhC1B,GAAO,CMiCpB,gCAC8B,CAAE,OAAO,CNqY1B,GAAO,CMpYpB,+BAC8B,CAAE,OAAO,CN4C1B,GAAO,CM3CpB,2DAG8B,CAAE,OAAO,CNgD1B,GAAO,CM/CpB,iDAE8B,CAAE,OAAO,CNiN1B,GAAO,CMhNpB,6BAC8B,CAAE,OAAO,CN+C1B,GAAO,CM9CpB,iCAC8B,CAAE,OAAO,CN3P1B,GAAO,CM4PpB,eAA+B,CAAE,OAAO,CNhG1B,GAAO,CMiGrB,oBAAoC,CAAE,OAAO,CNpF1B,GAAO,CMqF1B,yBAAyC,CAAE,OAAO,CN0P1B,GAAO,CMzP/B,0BAA0C,CAAE,OAAO,CN0P1B,GAAO,CMzPhC,0BAA0C,CAAE,OAAO,CN0P1B,GAAO,CMzPhC,2BAA2C,CAAE,OAAO,CN0P1B,GAAO,CMzPjC,2BAA2C,CAAE,OAAO,CN6P1B,GAAO,CM5PjC,4BAA4C,CAAE,OAAO,CN6P1B,GAAO,CM5PlC,oBAAoC,CAAE,OAAO,CNkU1B,GAAO,CMjU1B,sBAAsC,CAAE,OAAO,CN8T1B,GAAO,CM7T5B,yBAAyC,CAAE,OAAO,CNya1B,GAAO,CMxa/B,kBAAkC,CAAE,OAAO,CNsa1B,GAAO,CMraxB,eAA+B,CAAE,OAAO,CN2Z1B,GAAO,CM1ZrB,sBAAsC,CAAE,OAAO,CN2Z1B,GAAO,CM1Z5B,uBAAuC,CAAE,OAAO,CNoa1B,GAAO,CMna7B,kBAAkC,CAAE,OAAO,CNxJ1B,GAAO,CMyJxB,yBAAyC,CAAE,OAAO,CN8P1B,GAAO,CM7P/B,oBAAoC,CAAE,OAAO,CNgB1B,GAAO,CMf1B,iBAAiC,CAAE,OAAO,CNpF1B,GAAO,CMqFvB,cAA8B,CAAE,OAAO,CN3W1B,GAAO,CM4WpB,oBAAoC,CAAE,OAAO,CN/R1B,GAAO,CMgS1B,2BAA2C,CAAE,OAAO,CN/R1B,GAAO,CMgSjC,iBAAiC,CAAE,OAAO,CN+U1B,GAAO,CM9UvB,wBAAwC,CAAE,OAAO,CN+U1B,GAAO,CM9U9B,0BAA0C,CAAE,OAAO,CNgD1B,GAAO,CM/ChC,wBAAwC,CAAE,OAAO,CNkD1B,GAAO,CMjD9B,0BAA0C,CAAE,OAAO,CN+C1B,GAAO,CM9ChC,2BAA2C,CAAE,OAAO,CN+C1B,GAAO,CM9CjC,gBAAgC,CAAE,OAAO,CNjW1B,GAAO,CMkWtB,kBAAkC,CAAE,OAAO,CNmY1B,GAAO,CMlYxB,kBAAkC,CAAE,OAAO,CN7W1B,GAAO,CM8WxB,gBAAgC,CAAE,OAAO,CNkC1B,GAAO,CMjCtB,mBAAmC,CAAE,OAAO,CN5K1B,GAAO,CM6KzB,gBAAgC,CAAE,OAAO,CNgN1B,GAAO,CM/MtB,qBAAqC,CAAE,OAAO,CNxF1B,GAAO,CMyF3B,iBAAiC,CAAE,OAAO,CN4T1B,GAAO,CM3TvB,iBAAiC,CAAE,OAAO,CNtI1B,GAAO,CMuIvB,eAA+B,CAAE,OAAO,CN6C1B,GAAO,CM5CrB,qCACmC,CAAE,OAAO,CN5D1B,GAAO,CM6DzB,gBAAgC,CAAE,OAAO,CN8P1B,GAAO,CM7PtB,iBAAiC,CAAE,OAAO,CNuE1B,GAAO,CMtEvB,kBAAkC,CAAE,OAAO,CN9W1B,GAAO,CM+WxB,cAA8B,CAAE,OAAO,CNtS1B,GAAO,CMuSpB,aAA6B,CAAE,OAAO,CNiW1B,GAAO,CMhWnB,gBAAgC,CAAE,OAAO,CNuW1B,GAAO,CMtWtB,iBAAiC,CAAE,OAAO,CN+I1B,GAAO,CM9IvB,oBAAoC,CAAE,OAAO,CNkF1B,GAAO,CMjF1B,yBAAyC,CAAE,OAAO,CN6N1B,GAAO,CM5N/B,+BAA+C,CAAE,OAAO,CN/W1B,GAAO,CMgXrC,8BAA8C,CAAE,OAAO,CNjX1B,GAAO,CMkXpC,qDAC8C,CAAE,OAAO,CNzR1B,GAAO,CM0RpC,uBAAuC,CAAE,OAAO,CNnM1B,GAAO,CMoM7B,qBAAqC,CAAE,OAAO,CNiW1B,GAAO,CMhW3B,uBAAuC,CAAE,OAAO,CNoV1B,GAAO,CMnV7B,sCAC8B,CAAE,OAAO,CN0S1B,GAAO,CMzSpB,wBAAwC,CAAE,OAAO,CN0G1B,GAAO,CMzG9B,wBAAwC,CAAE,OAAO,CN4M1B,GAAO,CM3M9B,gBAAgC,CAAE,OAAO,CNsL1B,GAAO,CMrLtB,0BAA0C,CAAE,OAAO,CNzL1B,GAAO,CM0LhC,oBAAoC,CAAE,OAAO,CNoW1B,GAAO,CMnW1B,iBAAiC,CAAE,OAAO,CN8D1B,GAAO,CM7DvB,4DAEqC,CAAE,OAAO,CN8S1B,GAAO,CM7S3B,iDACyC,CAAE,OAAO,CN1F1B,GAAO,CM2F/B,gBAAgC,CAAE,OAAO,CNsW1B,GAAO,CMrWtB,iBAAiC,CAAE,OAAO,CNlG1B,GAAO,CMmGvB,iBAAiC,CAAE,OAAO,CNgH1B,GAAO,CM/GvB,wBAAwC,CAAE,OAAO,CNiH1B,GAAO,CMhH9B,6BAA6C,CAAE,OAAO,CNyN1B,GAAO,CMxNnC,sBAAsC,CAAE,OAAO,CNuN1B,GAAO,CMtN5B,oBAAoC,CAAE,OAAO,CN/N1B,GAAO,CMgO1B,eAA+B,CAAE,OAAO,CN5N1B,GAAO,CM6NrB,wBAAwC,CAAE,OAAO,CN2E1B,GAAO,CM1E9B,yBAAyC,CAAE,OAAO,CNyE1B,GAAO,CMxE/B,iBAAiC,CAAE,OAAO,CNvN1B,GAAO,CMwNvB,iBAAiC,CAAE,OAAO,CNzC1B,GAAO,CM0CvB,mBAAmC,CAAE,OAAO,CNpC1B,GAAO,CMqCzB,cAA8B,CAAE,OAAO,CNtL1B,GAAO,CMuLpB,mBAAmC,CAAE,OAAO,CN7U1B,GAAO,CM8UzB,gBAAgC,CAAE,OAAO,CN1R1B,GAAO,CM2RtB,cAA8B,CAAE,OAAO,CNsD1B,GAAO,CMrDpB,gBAAgC,CAAE,OAAO,CNmL1B,GAAO,CMlLtB,eAA+B,CAAE,OAAO,CNrP1B,GAAO,CMsPrB,gBAAgC,CAAE,OAAO,CNrP1B,GAAO,CMsPtB,kBAAkC,CAAE,OAAO,CN7W1B,GAAO,CM8WxB,yBAAyC,CAAE,OAAO,CN7W1B,GAAO,CM8W/B,gBAAgC,CAAE,OAAO,CN0L1B,GAAO,CMzLtB,uBAAuC,CAAE,OAAO,CN0L1B,GAAO,CMzL7B,kBAAkC,CAAE,OAAO,CNyF1B,GAAO,CMxFxB,oCAC8B,CAAE,OAAO,CNzU1B,GAAO,CM0UpB,8BAC+B,CAAE,OAAO,CN+M1B,GAAO,CM9MrB,eAA+B,CAAE,OAAO,CN4P1B,GAAO,CM3PrB,kBAAkC,CAAE,OAAO,CNuK1B,GAAO,CMtKxB,qBAAqC,CAAE,OAAO,CNtP1B,GAAO,CMuP3B,qBAAqC,CAAE,OAAO,CNiK1B,GAAO,CMhK3B,mBAAmC,CAAE,OAAO,CN9P1B,GAAO,CM+PzB,qBAAqC,CAAE,OAAO,CN/L1B,GAAO,CMgM3B,sBAAsC,CAAE,OAAO,CNxL1B,GAAO,CMyL5B,uBAAuC,CAAE,OAAO,CNrM1B,GAAO,CMsM7B,4BAA4C,CAAE,OAAO,CN/L1B,GAAO,CMgMlC,yEAEuC,CAAE,OAAO,CNxM1B,GAAO,CMyM7B,+CACyC,CAAE,OAAO,CN9M1B,GAAO,CM+M/B,+CACuC,CAAE,OAAO,CN/M1B,GAAO,CMgN7B,+CACuC,CAAE,OAAO,CNpM1B,GAAO,CMqM7B,sBAAsC,CAAE,OAAO,CNjN1B,GAAO,CMkN5B,eAA+B,CAAE,OAAO,CNuR1B,GAAO,CMtRrB,kBAAkC,CAAE,OAAO,CN5S1B,GAAO,CM6SxB,mBAAmC,CAAE,OAAO,CN9E1B,GAAO,CM+EzB,uGAIoC,CAAE,OAAO,CNnE1B,GAAO,CMoE1B,yBAAyC,CAAE,OAAO,CN/T1B,GAAO,CMgU/B,oDAEgC,CAAE,OAAO,CNqD1B,GAAO,CMpDtB,+BACiC,CAAE,OAAO,CNnQ1B,GAAO,CMoQvB,qBAAqC,CAAE,OAAO,CNzK1B,GAAO,CM0K3B,cAA8B,CAAE,OAAO,CN3K1B,GAAO,CM4KpB,0EAEsC,CAAE,OAAO,CNxJ1B,GAAO,CMyJ5B,wBAAwC,CAAE,OAAO,CN2K1B,GAAO,CM1K9B,aAA6B,CAAE,OAAO,CNiC1B,GAAO,CMhCnB,mCACiC,CAAE,OAAO,CN0Q1B,GAAO,CMzQvB,sCACsC,CAAE,OAAO,CNV1B,GAAO,CMW5B,0CACwC,CAAE,OAAO,CNX1B,GAAO,CMY9B,kBAAkC,CAAE,OAAO,CN1I1B,GAAO,CM2IxB,sBAAsC,CAAE,OAAO,CNlV1B,GAAO,CMmV5B,iBAAiC,CAAE,OAAO,CNjJ1B,GAAO,CMkJvB,oBAAoC,CAAE,OAAO,CNb1B,GAAO,CMc1B,kBAAkC,CAAE,OAAO,CN+F1B,GAAO,CM9FxB,oBAAoC,CAAE,OAAO,CNuE1B,GAAO,CMtE1B,2BAA2C,CAAE,OAAO,CNuE1B,GAAO,CMtEjC,eAA+B,CAAE,OAAO,CNzZ1B,GAAO,CM0ZrB,4CACmC,CAAE,OAAO,CN5M1B,GAAO,CM6MzB,cAA8B,CAAE,OAAO,CN0M1B,GAAO,CMzMpB,qBAAqC,CAAE,OAAO,CNxa1B,GAAO,CMya3B,eAA+B,CAAE,OAAO,CNI1B,GAAO,CMHrB,qBAAqC,CAAE,OAAO,CNuF1B,GAAO,CMtF3B,iBAAiC,CAAE,OAAO,CN2M1B,GAAO,CM1MvB,eAA+B,CAAE,OAAO,CN+Q1B,GAAO,CM9QrB,sBAAsC,CAAE,OAAO,CNzC1B,GAAO,CM0C5B,eAA+B,CAAE,OAAO,CNwP1B,GAAO,CMvPrB,qBAAqC,CAAE,OAAO,CNrZ1B,GAAO,CMsZ3B,iBAAiC,CAAE,OAAO,CNvB1B,GAAO,CMwBvB,wBAAwC,CAAE,OAAO,CN3L1B,GAAO,CM4L9B,kBAAkC,CAAE,OAAO,CN5X1B,GAAO,CM6XxB,wBAAwC,CAAE,OAAO,CNhY1B,GAAO,CMiY9B,sBAAsC,CAAE,OAAO,CNnY1B,GAAO,CMoY5B,kBAAkC,CAAE,OAAO,CNtY1B,GAAO,CMuYxB,oBAAoC,CAAE,OAAO,CNlY1B,GAAO,CMmY1B,oBAAoC,CAAE,OAAO,CNlY1B,GAAO,CMmY1B,qBAAqC,CAAE,OAAO,CN3b1B,GAAO,CM4b3B,uBAAuC,CAAE,OAAO,CN3b1B,GAAO,CM4b7B,gBAAgC,CAAE,OAAO,CN+K1B,GAAO,CM9KtB,oBAAoC,CAAE,OAAO,CNnV1B,GAAO,CMoV1B,aAA6B,CAAE,OAAO,CN9d1B,GAAO,CM+dnB,qBAAqC,CAAE,OAAO,CN5R1B,GAAO,CM6R3B,sBAAsC,CAAE,OAAO,CN/C1B,GAAO,CMgD5B,wBAAwC,CAAE,OAAO,CN9b1B,GAAO,CM+b9B,qBAAqC,CAAE,OAAO,CNtf1B,GAAO,CMuf3B,oBAAoC,CAAE,OAAO,CN/B1B,GAAO,CMgC1B,qBAAqC,CAAE,OAAO,CNzH1B,GAAO,CM0H3B,iBAAiC,CAAE,OAAO,CNvI1B,GAAO,CMwIvB,wBAAwC,CAAE,OAAO,CNvI1B,GAAO,CMwI9B,qBAAqC,CAAE,OAAO,CN4J1B,GAAO,CM3J3B,oBAAoC,CAAE,OAAO,CN4J1B,GAAO,CM3J1B,kBAAkC,CAAE,OAAO,CNxc1B,GAAO,CMycxB,cAA8B,CAAE,OAAO,CNjb1B,GAAO,CMkbpB,kBAAkC,CAAE,OAAO,CNvJ1B,GAAO,CMwJxB,oBAAoC,CAAE,OAAO,CN3gB1B,GAAO,CM4gB1B,aAA6B,CAAE,OAAO,CN7Z1B,GAAO,CM8ZnB,kDAE8B,CAAE,OAAO,CNzK1B,GAAO,CM0KpB,mBAAmC,CAAE,OAAO,CNpG1B,GAAO,CMqGzB,qBAAqC,CAAE,OAAO,CNxb1B,GAAO,CMyb3B,yBAAyC,CAAE,OAAO,CN5W1B,GAAO,CM6W/B,mBAAmC,CAAE,OAAO,CN9V1B,GAAO,CM+VzB,mBAAmC,CAAE,OAAO,CN9P1B,GAAO,CM+PzB,kBAAkC,CAAE,OAAO,CNrJ1B,GAAO,CMsJxB,iBAAiC,CAAE,OAAO,CNe1B,GAAO,CMdvB,uBAAuC,CAAE,OAAO,CN2B1B,GAAO,CM1B7B,sBAAsC,CAAE,OAAO,CNoC1B,GAAO,CMnC5B,mBAAmC,CAAE,OAAO,CNqC1B,GAAO,CMpCzB,oBAAoC,CAAE,OAAO,CN5a1B,GAAO,CM6a1B,0BAA0C,CAAE,OAAO,CN9a1B,GAAO,CM+ahC,kBAAkC,CAAE,OAAO,CN/V1B,GAAO,CMgWxB,eAA+B,CAAE,OAAO,CNoB1B,GAAO,CMnBrB,sBAAsC,CAAE,OAAO,CN8K1B,GAAO,CM7K5B,qBAAqC,CAAE,OAAO,CN/F1B,GAAO,CMgG3B,sBAAsC,CAAE,OAAO,CN6E1B,GAAO,CM5E5B,oBAAoC,CAAE,OAAO,CN9M1B,GAAO,CM+M1B,gBAAgC,CAAE,OAAO,CN+K1B,GAAO,CM9KtB,eAA+B,CAAE,OAAO,CN7H1B,GAAO,CM8HrB,kBAAkC,CAAE,OAAO,CNnH1B,GAAO,CMoHxB,0CACsC,CAAE,OAAO,CNkI1B,GAAO,CMjI5B,0BAA0C,CAAE,OAAO,CNkI1B,GAAO,CMjIhC,uBAAuC,CAAE,OAAO,CN0K1B,GAAO,CMzK7B,sBAAsC,CAAE,OAAO,CNlI1B,GAAO,CMmI5B,qBAAqC,CAAE,OAAO,CNyK1B,GAAO,CMxK3B,sBAAsC,CAAE,OAAO,CNnI1B,GAAO,CMoI5B,wBAAwC,CAAE,OAAO,CNlI1B,GAAO,CMmI9B,wBAAwC,CAAE,OAAO,CNpI1B,GAAO,CMqI9B,iBAAiC,CAAE,OAAO,CN1G1B,GAAO,CM2GvB,qBAAqC,CAAE,OAAO,CN7Q1B,GAAO,CM8Q3B,4BAA4C,CAAE,OAAO,CN1U1B,GAAO,CM2UlC,sBAAsC,CAAE,OAAO,CNzE1B,GAAO,CM0E5B,mBAAmC,CAAE,OAAO,CNkL1B,GAAO,CMjLzB,iBAAiC,CAAE,OAAO,CNX1B,GAAO,CMYvB,oBAAoC,CAAE,OAAO,CNuJ1B,GAAO,CMtJ1B,qBAAqC,CAAE,OAAO,CNwJ1B,GAAO,CMvJ3B,+BAC8B,CAAE,OAAO,CN/f1B,GAAO,CMggBpB,kBAAkC,CAAE,OAAO,CN4J1B,GAAO,CM3JxB,gBAAgC,CAAE,OAAO,CN8G1B,GAAO,CM7GtB,iBAAiC,CAAE,OAAO,CNwD1B,GAAO,CMvDvB,iBAAiC,CAAE,OAAO,CN9I1B,GAAO,CM+IvB,qCACuC,CAAE,OAAO,CN0L1B,GAAO,CMzL7B,wBAAwC,CAAE,OAAO,CNjH1B,GAAO,CMkH9B,mBAAmC,CAAE,OAAO,CNrH1B,GAAO,CMsHzB,uBAAuC,CAAE,OAAO,CNnW1B,GAAO,CMoW7B,+DAEuC,CAAE,OAAO,CN/gB1B,GAAO,CMghB7B,sDACiD,CAAE,OAAO,CN9gB1B,GAAO,CM+gBvC,4CACuC,CAAE,OAAO,CNlhB1B,GAAO,CMmhB7B,+CAC0C,CAAE,OAAO,CNnhB1B,GAAO,CMohBhC,6CACwC,CAAE,OAAO,CNxhB1B,GAAO,CMyhB9B,wBAAwC,CAAE,OAAO,CN3I1B,GAAO,CM4I9B,mBAAmC,CAAE,OAAO,CN3O1B,GAAO,CM4OzB,uBAAuC,CAAE,OAAO,CNxI1B,GAAO,CMyI7B,yBAAyC,CAAE,OAAO,CNxI1B,GAAO,CMyI/B,sBAAsC,CAAE,OAAO,CNwB1B,GAAO,CMvB5B,wBAAwC,CAAE,OAAO,CNwB1B,GAAO,CMvB9B,iBAAiC,CAAE,OAAO,CN/d1B,GAAO,CMgevB,yBAAyC,CAAE,OAAO,CNle1B,GAAO,CMme/B,gBAAgC,CAAE,OAAO,CNpc1B,GAAO,CMqctB,wBAAwC,CAAE,OAAO,CNljB1B,GAAO,CMmjB9B,sBAAsC,CAAE,OAAO,CNxP1B,GAAO,CMyP5B,iDAC0C,CAAE,OAAO,CNzP1B,GAAO,CM0PhC,gDACyC,CAAE,OAAO,CN7P1B,GAAO,CM8P/B,+CACwC,CAAE,OAAO,CNhQ1B,GAAO,CMiQ9B,oBAAoC,CAAE,OAAO,CNrQ1B,GAAO,CMsQ1B,6CACsC,CAAE,OAAO,CNxR1B,GAAO,CMyR5B,8CACuC,CAAE,OAAO,CN7R1B,GAAO,CM8R7B,0BAA0C,CAAE,OAAO,CN1R1B,GAAO,CM2RhC,wBAAwC,CAAE,OAAO,CNpS1B,GAAO,CMqS9B,uBAAuC,CAAE,OAAO,CN3R1B,GAAO,CM4R7B,yBAAyC,CAAE,OAAO,CN/R1B,GAAO,CMgS/B,uBAAuC,CAAE,OAAO,CNjS1B,GAAO,CMkS7B,oBAAoC,CAAE,OAAO,CN+D1B,GAAO,CM9D1B,qBAAqC,CAAE,OAAO,CN/F1B,GAAO,CMgG3B,2BAA2C,CAAE,OAAO,CN/b1B,GAAO,CMgcjC,aAA6B,CAAE,OAAO,CNtU1B,GAAO,CMuUnB,oBAAoC,CAAE,OAAO,CNtU1B,GAAO,CMuU1B,sBAAsC,CAAE,OAAO,CNkE1B,GAAO,CMjE5B,wBAAwC,CAAE,OAAO,CNrK1B,GAAO,CMsK9B,+BAA+C,CAAE,OAAO,CNrK1B,GAAO,CMsKrC,qBAAqC,CAAE,OAAO,CN5U1B,GAAO,CM6U3B,sBAAsC,CAAE,OAAO,CNwH1B,GAAO,CMvH5B,iBAAiC,CAAE,OAAO,CNnF1B,GAAO,CMoFvB,iBAAiC,CAAE,OAAO,CNze1B,GAAO,CM0evB,kBAAkC,CAAE,OAAO,CN9W1B,GAAO,CM+WxB,gBAAgC,CAAE,OAAO,CNxK1B,GAAO,CMyKtB,4BAA4C,CAAE,OAAO,CNpQ1B,GAAO,CMqQlC,mCACqC,CAAE,OAAO,CNS1B,GAAO,CMR3B,iBAAiC,CAAE,OAAO,CNjd1B,GAAO,CMkdvB,gBAAgC,CAAE,OAAO,CNzoB1B,GAAO,CM0oBtB,iBAAiC,CAAE,OAAO,CN/nB1B,GAAO,CMgoBvB,0BAA0C,CAAE,OAAO,CN3hB1B,GAAO,CM4hBhC,2BAA2C,CAAE,OAAO,CN9hB1B,GAAO,CM+hBjC,2BAA2C,CAAE,OAAO,CN5hB1B,GAAO,CM6hBjC,2BAA2C,CAAE,OAAO,CNjiB1B,GAAO,CMkiBjC,mBAAmC,CAAE,OAAO,CNpR1B,GAAO,CMqRzB,kBAAkC,CAAE,OAAO,CN5N1B,GAAO,CM6NxB,oBAAoC,CAAE,OAAO,CN5N1B,GAAO,CM6N1B,gBAAgC,CAAE,OAAO,CN/N1B,GAAO,CMgOtB,cAA8B,CAAE,OAAO,CNlO1B,GAAO,CMmOpB,qBAAqC,CAAE,OAAO,CNpe1B,GAAO,CMqe3B,uBAAuC,CAAE,OAAO,CNpe1B,GAAO,CMqe7B,gBAAgC,CAAE,OAAO,CNtS1B,GAAO,CMuStB,gBAAgC,CAAE,OAAO,CNiF1B,GAAO,CMhFtB,oBAAoC,CAAE,OAAO,CNlkB1B,GAAO,CMmkB1B,oBAAoC,CAAE,OAAO,CNrX1B,GAAO,CMsX1B,uBAAuC,CAAE,OAAO,CNpI1B,GAAO,CMqI7B,eAA+B,CAAE,OAAO,CNpc1B,GAAO,CMqcrB,0BAA0C,CAAE,OAAO,CNhe1B,GAAO,CMiehC,mBAAmC,CAAE,OAAO,CNpf1B,GAAO,CMqfzB,eAA+B,CAAE,OAAO,CNlN1B,GAAO,CMmNrB,uBAAuC,CAAE,OAAO,CN1X1B,GAAO,CM2X7B,cAA8B,CAAE,OAAO,CNoD1B,GAAO,CMnDpB,uBAAuC,CAAE,OAAO,CN3J1B,GAAO,CM4J7B,mBAAmC,CAAE,OAAO,CNzN1B,GAAO,CM0NzB,iBAAiC,CAAE,OAAO,CNlH1B,GAAO,CMmHvB,uBAAuC,CAAE,OAAO,CN7L1B,GAAO,CM8L7B,yBAAyC,CAAE,OAAO,CN7L1B,GAAO,CM8L/B,sBAAsC,CAAE,OAAO,CN3C1B,GAAO,CM4C5B,wBAAwC,CAAE,OAAO,CN3C1B,GAAO,CM4C9B,uBAAuC,CAAE,OAAO,CNrG1B,GAAO,CMsG7B,0BAA0C,CAAE,OAAO,CNrG1B,GAAO,CMsGhC,kBAAkC,CAAE,OAAO,CN7U1B,GAAO,CM8UxB,oBAAoC,CAAE,OAAO,CNnlB1B,GAAO,CMolB1B,sBAAsC,CAAE,OAAO,CNnlB1B,GAAO,CMolB5B,kBAAkC,CAAE,OAAO,CN/L1B,GAAO,CMgMxB,iBAAiC,CAAE,OAAO,CNlX1B,GAAO,CMmXvB,qBAAqC,CAAE,OAAO,CNkF1B,GAAO,CMjF3B,kBAAkC,CAAE,OAAO,CNmF1B,GAAO,CMlFxB,iBAAiC,CAAE,OAAO,CN9c1B,GAAO,CM+cvB,2BAA2C,CAAE,OAAO,CN2B1B,GAAO,CM1BjC,yBAAyC,CAAE,OAAO,CNmE1B,GAAO,CMlE/B,4BAA4C,CAAE,OAAO,CNxK1B,GAAO,CMyKlC,gBAAgC,CAAE,OAAO,CN9lB1B,GAAO,CM+lBtB,4BAA4C,CAAE,OAAO,CNtoB1B,GAAO,CMuoBlC,+BAA+C,CAAE,OAAO,CNqD1B,GAAO,CMpDrC,kBAAkC,CAAE,OAAO,CNxlB1B,GAAO,CMylBxB,sCAAsD,CAAE,OAAO,CN5oB1B,GAAO,CM6oB5C,0EAC8D,CAAE,OAAO,CN9qB1B,GAAO,CM+qBpD,8DAE+B,CAAE,OAAO,CNvf1B,GAAO,CMwfrB,gBAAgC,CAAE,OAAO,CNhY1B,GAAO,CMiYtB,kBAAkC,CAAE,OAAO,CNhY1B,GAAO,CMiYxB,2CACwC,CAAE,OAAO,CN1H1B,GAAO,CM2H9B,qBAAqC,CAAE,OAAO,CNzR1B,GAAO,CM0R3B,iBAAiC,CAAE,OAAO,CNiC1B,GAAO,CMhCvB,wBAAwC,CAAE,OAAO,CNiC1B,GAAO,CMhC9B,mBAAmC,CAAE,OAAO,CNlH1B,GAAO,CMmHzB,yBAAyC,CAAE,OAAO,CNlH1B,GAAO,CMmH/B,0BAA0C,CAAE,OAAO,CNlH1B,GAAO,CMmHhC,qBAAqC,CAAE,OAAO,CNrN1B,GAAO,CMsN3B,sBAAsC,CAAE,OAAO,CNpb1B,GAAO,CMqb5B,gBAAgC,CAAE,OAAO,CNmE1B,GAAO,CMlEtB,oBAAoC,CAAE,OAAO,CNpD1B,GAAO,CMqD1B,6DAC+C,CAAE,OAAO,CNzY1B,GAAO,CM0YrC,qCACuC,CAAE,OAAO,CN7a1B,GAAO,CM8a7B,sBAAsC,CAAE,OAAO,CNtX1B,GAAO,CMuX5B,wBAAwC,CAAE,OAAO,CNlf1B,GAAO,CMmf9B,0BAA0C,CAAE,OAAO,CNlf1B,GAAO,CMmfhC,iBAAiC,CAAE,OAAO,CNtT1B,GAAO,CMuTvB,uBAAuC,CAAE,OAAO,CNptB1B,GAAO,CMqtB7B,yBAAyC,CAAE,OAAO,CNptB1B,GAAO,CMqtB/B,wCACuC,CAAE,OAAO,CNrtB1B,GAAO,CMstB7B,4CACyC,CAAE,OAAO,CNttB1B,GAAO,CMutB/B,sBAAsC,CAAE,OAAO,CNJ1B,GAAO,CMK5B,wBAAwC,CAAE,OAAO,CNJ1B,GAAO,CMK9B,iBAAiC,CAAE,OAAO,CNH1B,GAAO,CMIvB,mBAAmC,CAAE,OAAO,CN3W1B,GAAO,CM4WzB,6CACkC,CAAE,OAAO,CN5W1B,GAAO,CM6WxB,iDACoC,CAAE,OAAO,CN7W1B,GAAO,CM8W1B,gBAAgC,CAAE,OAAO,CNtN1B,GAAO,CMuNtB,yBAAyC,CAAE,OAAO,CN3b1B,GAAO,CM4b/B,mBAAmC,CAAE,OAAO,CNtF1B,GAAO,CMuFzB,2EAE2C,CAAE,OAAO,CNxE1B,GAAO,CMyEjC,8DACqD,CAAE,OAAO,CNvE1B,GAAO,CMwE3C,oDAC2C,CAAE,OAAO,CN3E1B,GAAO,CM4EjC,uDAC8C,CAAE,OAAO,CN5E1B,GAAO,CM6EpC,qDAC4C,CAAE,OAAO,CNjF1B,GAAO,CMkFlC,iBAAiC,CAAE,OAAO,CN3K1B,GAAO,CM4KvB,iDAE+B,CAAE,OAAO,CNzrB1B,GAAO,CM0rBrB,kBAAkC,CAAE,OAAO,CNlP1B,GAAO,CMmPxB,0BAA0C,CAAE,OAAO,CNK1B,GAAO,CMJhC,0BAA0C,CAAE,OAAO,CNK1B,GAAO,CMJhC,yBAAyC,CAAE,OAAO,CNK1B,GAAO,CMJ/B,kDACuC,CAAE,OAAO,CND1B,GAAO,CME7B,sDACyC,CAAE,OAAO,CNF1B,GAAO,CMG/B,mBAAmC,CAAE,OAAO,CNxsB1B,GAAO,CMysBzB,eAA+B,CAAE,OAAO,CNpb1B,GAAO,CMqbrB,eAA+B,CAAE,OAAO,CN1hB1B,GAAO,CM2hBrB,eAA+B,CAAE,OAAO,CNxY1B,GAAO,CMyYrB,kBAAkC,CAAE,OAAO,CN/O1B,GAAO,CMgPxB,kBAAkC,CAAE,OAAO,CNziB1B,GAAO,CM0iBxB,oBAAoC,CAAE,OAAO,CNjU1B,GAAO,CMkU1B,sBAAsC,CAAE,OAAO,CN7K1B,GAAO,CM8K5B,sBAAsC,CAAE,OAAO,CNhI1B,GAAO,CMiI5B,qBAAqC,CAAE,OAAO,CNJ1B,GAAO,CMK3B,iBAAiC,CAAE,OAAO,CNxU1B,GAAO,COzcvB,QAAS,CH8BP,QAAQ,CAAE,QAAQ,CAClB,KAAK,CAAE,GAAG,CACV,MAAM,CAAE,GAAG,CACX,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,IAAI,CACZ,QAAQ,CAAE,MAAM,CAChB,IAAI,CAAE,gBAAa,CACnB,MAAM,CAAE,CAAC,CAUT,kDACQ,CACN,QAAQ,CAAE,MAAM,CAChB,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CACZ,MAAM,CAAE,CAAC,CACT,QAAQ,CAAE,OAAO,CACjB,IAAI,CAAE,IAAI,CVhDd,OAAU,CACT,SAAS,CAAE,GAAG,CACd,WAAW,CAAE,CAAC,CACd,QAAQ,CAAE,QAAQ,CAClB,cAAc,CAAE,QAAQ,CAKzB,qCAAU,CACT,KAAK,CAAE,OAAO,CACd,IAAI,CAAE,OAAO,CACb,MAAM,CAAE,CAAC,CAIV,IAAK,CACJ,WAAW,CciBH,UAAU,CdhBlB,oBAAoB,CAAE,IAAI,CAC1B,wBAAwB,CAAE,IAAI,CAC9B,WAAW,CAAE,gDAA2C,CACxD,WAAW,CAAE,GAAG,CAChB,KAAK,Cc3BI,IAAI,Cd4Bb,SAAS,CAAE,IAAI,CACf,WAAW,CAAE,MAAM,CACnB,uBAAmB,CAClB,kBAAkB,CAAE,MAAM,CAC1B,MAAM,CAAE,OAAO,CAEhB,oBAAgB,CACf,MAAM,CAAE,OAAO,CAGjB,IAAK,CACJ,MAAM,CAAE,CAAC,CACT,cAAc,CAAE,kBAAkB,CAClC,sBAAsB,CAAE,WAAW,CACnC,uBAAuB,CAAE,SAAS,CAClC,0BAA0B,CAAE,SAAS,CAEtC,OAAQ,CACP,OAAO,CAAE,KAAK,CAEf,KAAM,CACL,OAAO,CAAE,KAAK,CAEf,OAAQ,CACP,OAAO,CAAE,KAAK,CAEf,UAAW,CACV,OAAO,CAAE,KAAK,CAEf,MAAO,CACN,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,QAAQ,CAEjB,MAAO,CACN,OAAO,CAAE,KAAK,CAEf,MAAO,CACN,OAAO,CAAE,KAAK,CAEf,MAAO,CACN,OAAO,CAAE,KAAK,CAEf,IAAK,CACJ,OAAO,CAAE,KAAK,CAEf,IAAK,CACJ,OAAO,CAAE,KAAK,CAEf,GAAI,CACH,OAAO,CAAE,KAAK,CAEf,OAAQ,CACP,OAAO,CAAE,KAAK,CAEf,OAAQ,CACP,OAAO,CAAE,KAAK,CAEf,KAAM,CACL,OAAO,CAAE,YAAY,CACrB,cAAc,CAAE,QAAQ,CACxB,qBAAkB,CACjB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,CAAC,CAGX,MAAO,CACN,OAAO,CAAE,YAAY,CACrB,cAAc,CAAE,QAAQ,CAEzB,QAAS,CACR,OAAO,CAAE,YAAY,CACrB,cAAc,CAAE,QAAQ,CAEzB,KAAM,CACL,OAAO,CAAE,YAAY,CACrB,cAAc,CAAE,QAAQ,CAEzB,QAAS,CACR,OAAO,CAAE,IAAI,CAEd,QAAS,CACR,OAAO,CAAE,IAAI,CAEd,CAAE,CACD,gBAAgB,CAAE,WAAW,CAC7B,eAAe,CAAE,IAAI,CACrB,KAAK,CchHI,OAAO,CdkHhB,UAAU,CAAE,OAAO,CACnB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,OAAO,CACf,QAAS,CACR,OAAO,CAAE,CAAC,CAEX,OAAQ,CACP,OAAO,CAAE,CAAC,CACV,KAAK,Cc1HG,OAAO,Cd6HjB,WAAY,CACX,aAAa,CAAE,UAAU,CAE1B,CAAE,CACD,WAAW,CAAE,GAAG,CAChB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,MAAO,CACN,WAAW,CAAE,GAAG,CAChB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,GAAI,CACH,UAAU,CAAE,MAAM,CAClB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CACF,SAAS,CAAE,GAAG,CACd,MAAM,CAAE,OAAO,CACf,UAAU,CAAE,kBAAkB,CAC9B,WAAW,CAAE,mBAAmB,CAChC,aAAa,CAAE,kBAAkB,CACjC,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,gDAA2C,CACxD,WAAW,CAAE,GAAG,CAChB,KAAK,Cc3JI,IAAI,Cd4Jb,KAAK,CAAE,IAAI,CAEZ,IAAK,CACJ,UAAU,CcnKD,IAAI,CdoKb,KAAK,CcnKI,IAAI,CdqKd,KAAM,CACL,SAAS,CAAE,GAAG,CACd,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,CAAC,CAEf,GAAI,CAEH,MAAM,CAAE,MAAM,CACd,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,CAAC,CAEf,GAAI,CAEH,GAAG,CAAE,KAAK,CACV,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,CAAC,CACd,SAAM,CACL,SAAS,CAAE,GAAG,CAGhB,GAAI,CACH,MAAM,CAAE,CAAC,CACT,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CAEF,UAAU,CAAE,WAAW,CACvB,MAAM,CAAE,CAAC,CAEV,GAAI,CACH,QAAQ,CAAE,IAAI,CACd,OAAO,CAAE,MAAM,CACf,aAAa,CAAE,MAAM,CACrB,UAAU,CczLA,IAAI,Cd0Ld,WAAW,CAAE,CAAC,CACd,KAAK,Cc/KK,IAAO,CdiLjB,aAAa,CAAE,GAAG,CAClB,SAAS,CAAE,IAAI,CACf,WAAW,CctKH,SAAS,CduKjB,SAAS,CAAE,GAAG,CACd,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,MAAM,CACrB,QAAK,CACJ,OAAO,CAAE,CAAC,CAGZ,IAAK,CACJ,WAAW,CchLH,SAAS,CdiLjB,SAAS,CAAE,GAAG,CACd,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,sEAA2C,CACxD,OAAO,CAAE,eAAe,CACxB,WAAW,CAAE,CAAC,CAEf,GAAI,CACH,WAAW,CczLH,SAAS,Cd0LjB,SAAS,CAAE,GAAG,CACd,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,IAAK,CACJ,WAAW,Cc/LH,SAAS,CdgMjB,SAAS,CAAE,GAAG,CACd,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,MAAO,CAEN,QAAQ,CAAE,OAAO,CACjB,cAAc,CAAE,IAAI,CACpB,kBAAkB,CAAE,MAAM,CAC1B,MAAM,CAAE,OAAO,CACf,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,OAAO,CACf,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,cAAc,CACvB,aAAa,CAAE,SAAS,CAEzB,KAAM,CAEL,WAAW,CAAE,MAAM,CACnB,WAAQ,CACP,UAAU,CcjOD,IAAO,CdoOlB,QAAS,CAER,WAAW,CAAE,GAAG,CAEjB,MAAO,CAEN,cAAc,CAAE,IAAI,CACpB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,cAAmB,CAE3B,aAAa,CAAE,GAAG,CAClB,OAAO,CAAE,SAAS,CAClB,KAAK,CAAE,iBAAiB,CACxB,MAAM,CAAE,UAAU,CAClB,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CAEb,QAAS,CAER,QAAQ,CAAE,IAAI,CACd,OAAO,CAAE,KAAK,CACd,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,OAAO,CAChB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,SAAS,CACxB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,cAAmB,CAE3B,aAAa,CAAE,GAAG,CAClB,OAAO,CAAE,SAAS,CAClB,KAAK,CAAE,iBAAiB,CACxB,MAAM,CAAE,UAAU,CAEnB,iBAAkB,CACjB,kBAAkB,CAAE,MAAM,CAC1B,MAAM,CAAE,OAAO,CAEhB,kBAAmB,CAClB,kBAAkB,CAAE,MAAM,CAC1B,MAAM,CAAE,OAAO,CACf,OAAO,CAAE,KAAK,CACd,MAAM,CAAE,OAAO,CACf,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,cAAc,CACvB,aAAa,CAAE,SAAS,CAEzB,gBAAiB,CAChB,MAAM,CAAE,OAAO,CAEhB,wBAAyB,CACxB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,uBAAwB,CACvB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,oBAAqB,CAEpB,UAAU,CAAE,UAAU,CACtB,OAAO,CAAE,CAAC,CAEX,iBAAkB,CAEjB,UAAU,CAAE,UAAU,CACtB,OAAO,CAAE,CAAC,CAEX,6CAA8C,CAC7C,MAAM,CAAE,IAAI,CAEb,6CAA8C,CAC7C,MAAM,CAAE,IAAI,CAEb,kBAAmB,CAClB,kBAAkB,CAAE,SAAS,CAE7B,UAAU,CAAE,WAAW,CAExB,gDAAiD,CAChD,kBAAkB,CAAE,IAAI,CAEzB,6CAA8C,CAC7C,kBAAkB,CAAE,IAAI,CAEzB,QAAS,CACR,MAAM,CAAE,gBAAkB,CAC1B,MAAM,CAAE,KAAK,CACb,OAAO,CAAE,kBAAkB,CAC3B,OAAO,CAAE,oBAAoB,CAC7B,YAAY,CAAE,GAAG,CACjB,YAAY,CAAE,KAAK,CACnB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,QAAQ,CACvB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,eAAO,CACN,aAAa,CAAE,CAAC,CAEjB,2BAAmB,CAClB,aAAa,CAAE,CAAC,CAGlB,MAAO,CACN,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,KAAK,CcvWI,IAAI,CdwWb,WAAW,CAAE,GAAG,CAChB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,KAAM,CACL,KAAK,CAAE,IAAI,CACX,cAAc,CAAE,CAAC,CACjB,eAAe,CAAE,QAAQ,CACzB,aAAa,CAAE,QAAQ,CACvB,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,MAAM,CAEtB,EAAG,CACF,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,OAAO,CAAE,eAAe,CAEzB,EAAG,CACF,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,UAAU,CAAE,IAAI,CAChB,KAAK,CchYI,IAAI,CdiYb,OAAO,CAAE,eAAe,CAEzB,yBAAwB,CACvB,IAAK,CACJ,SAAS,CAAE,IAAI,CAEhB,EAAG,CACF,SAAS,CAAE,4DAA4D,CAExE,EAAG,CACF,SAAS,CAAE,4DAA4D,CAExE,EAAG,CACF,SAAS,CAAE,4DAA4D,CAExE,EAAG,CACF,SAAS,CAAE,6DAA6D,CAEzE,EAAG,CACF,SAAS,CAAE,4DAA4D,CAExE,EAAG,CACF,SAAS,CAAE,sCAAsC,EAGnD,IAAK,CACJ,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,uBAAuB,CACtC,MAAM,CAAE,IAAI,CAEb,OAAQ,CACP,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,uBAAuB,CACtC,MAAM,CAAE,IAAI,CAEb,OAAQ,CACP,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,MAAM,CACrB,UAAU,CAAE,MAAM,CAEnB,GAAI,CACH,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,CAAC,CAEf,UAAW,CACV,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,MAAM,CACrB,UAAU,CAAE,MAAM,CAClB,eAAK,CACJ,OAAO,CAAE,KAAK,CACd,UAAU,CAAE,MAAM,CAGpB,OAAQ,CACP,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,MAAO,CACN,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,IAAK,CACJ,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,GAAI,CACH,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,MAAM,CAEtB,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,KAAK,CcvdI,IAAI,Cdwdb,WAAW,CAAE,GAAG,CAEjB,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,IAAK,CACJ,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,gDAA2C,CACxD,WAAW,CAAE,GAAG,CAChB,KAAK,CcveI,IAAI,Cdweb,KAAK,CAAE,IAAI,CACX,SAAS,CAAE,mBAAmB,CAC9B,UAAU,CAAE,mBAAmB,CAC/B,WAAW,CAAE,mBAAmB,CAChC,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,gDAA2C,CACxD,WAAW,CAAE,GAAG,CAChB,KAAK,CcnfI,IAAI,Cdofb,KAAK,CAAE,IAAI,CACX,SAAS,CAAE,oBAAoB,CAC/B,UAAU,CAAE,mBAAmB,CAC/B,WAAW,CAAE,mBAAmB,CAChC,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,gDAA2C,CACxD,WAAW,CAAE,GAAG,CAChB,KAAK,Cc/fI,IAAI,CdggBb,KAAK,CAAE,IAAI,CACX,SAAS,CAAE,oBAAoB,CAC/B,UAAU,CAAE,mBAAmB,CAC/B,WAAW,CAAE,kBAAkB,CAC/B,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,gDAA2C,CACxD,WAAW,CAAE,GAAG,CAChB,KAAK,Cc3gBI,IAAI,Cd4gBb,KAAK,CAAE,IAAI,CACX,SAAS,CAAE,oBAAoB,CAC/B,UAAU,CAAE,kBAAkB,CAC9B,WAAW,CAAE,mBAAmB,CAChC,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,WAAW,CAAE,gDAA2C,CACxD,WAAW,CAAE,GAAG,CAChB,KAAK,CcvhBI,IAAI,CdwhBb,KAAK,CAAE,IAAI,CACX,SAAS,CAAE,IAAI,CACf,UAAU,CAAE,QAAQ,CACpB,WAAW,CAAE,OAAO,CACpB,aAAa,CAAE,kBAAkB,CAElC,CAAE,CACD,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,GAAI,CACH,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,KAAM,CACL,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,OAAO,CAAE,KAAK,CACd,cAAc,CAAE,QAAQ,CACxB,aAAa,CAAE,SAAS,CACxB,MAAM,CAAE,OAAO,CAEhB,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,MAAM,CACrB,YAAY,CAAE,KAAK,CAEpB,CAAE,CACD,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,MAAM,CACrB,MAAM,CAAE,UAAU,CAEnB,CAAE,CACD,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,CAAE,CACD,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,MAAO,CACN,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,KAAM,CACL,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,KAAM,CACL,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,KAAM,CACL,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,CAAE,CACD,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,EAAG,CACF,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CACV,aAAa,CAAE,MAAM,CACrB,YAAY,CAAE,KAAK,CAEpB,GAAI,CACH,MAAM,CAAE,CAAC,CACT,OAAO,CAAE,CAAC,CAEX,0BAAyB,CACxB,EAAG,CACF,SAAS,CAAE,oBAAoB,CAC/B,UAAU,CAAE,kBAAkB,CAC9B,WAAW,CAAE,mBAAmB,CAChC,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,SAAS,CAAE,oBAAoB,CAC/B,UAAU,CAAE,mBAAmB,CAC/B,WAAW,CAAE,mBAAmB,CAChC,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,SAAS,CAAE,mBAAmB,CAC9B,UAAU,CAAE,mBAAmB,CAC/B,WAAW,CAAE,mBAAmB,CAChC,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,SAAS,CAAE,mBAAmB,CAC9B,UAAU,CAAE,mBAAmB,CAC/B,WAAW,CAAE,kBAAkB,CAC/B,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,SAAS,CAAE,oBAAoB,CAC/B,UAAU,CAAE,kBAAkB,CAC9B,WAAW,CAAE,mBAAmB,CAChC,aAAa,CAAE,kBAAkB,CAElC,EAAG,CACF,SAAS,CAAE,IAAI,CACf,UAAU,CAAE,QAAQ,CACpB,WAAW,CAAE,OAAO,CACpB,aAAa,CAAE,kBAAkB,CAElC,QAAS,CACR,aAAa,CAAE,UAAU,CAE1B,iBAAkB,CACjB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,UAAU,CAE1B,oBAAqB,CACpB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,UAAU,CAE1B,gBAAiB,CAChB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,UAAU,CAE1B,QAAS,CACR,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,UAAU,CAE1B,MAAO,CACN,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,QAAQ,CAExB,kBAAmB,CAClB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,QAAQ,CAExB,KAAM,CACL,aAAa,CAAE,SAAS,CAEzB,EAAG,CACF,OAAO,CAAE,cAAc,CAExB,EAAG,CACF,OAAO,CAAE,cAAc,EAGzB,iBAAkB,CACjB,OAAO,CAAE,KAAK,CACd,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,OAAO,CAChB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,SAAS,CAEzB,oBAAqB,CACpB,OAAO,CAAE,KAAK,CACd,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,OAAO,CAChB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,SAAS,CAEzB,gBAAiB,CAChB,OAAO,CAAE,KAAK,CACd,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,OAAO,CAChB,SAAS,CAAE,IAAI,CACf,aAAa,CAAE,SAAS,CAEzB,OAAQ,CACP,gBAAgB,CAAE,4BAAW,CAC7B,eAAe,CAAE,KAAK,CACtB,mBAAmB,CAAE,GAAG,CACxB,UAAU,CAAE,KAAK,CACjB,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,WAAW,CACpB,OAAO,CAAE,IAAI,CACb,uBAAuB,CAAE,MAAM,CAC/B,aAAa,CAAE,MAAM,CACrB,eAAe,CAAE,MAAM,CACvB,mBAAmB,CAAE,MAAM,CAC3B,cAAc,CAAE,MAAM,CACtB,WAAW,CAAE,MAAM,CAEpB,IAAK,CACJ,KAAK,CAAE,KAAK,CAEZ,aAAa,CAAE,WAAW,CAC1B,QAAQ,CAAE,MAAM,CAEhB,UAAU,CAAE,UAAU,CAEtB,UAAU,CAAE,uDAAwC,CAErD,OAAQ,CACP,gBAAgB,CchuBP,OAAO,CdiuBhB,OAAO,CAAE,cAAc,CAEvB,aAAa,CAAE,WAAW,CAC1B,UAAU,CAAE,MAAM,CAEnB,aAAc,CACb,WAAW,CAAE,GAAG,CAChB,cAAc,CAAE,SAAS,CACzB,SAAS,CAAE,IAAI,CACf,cAAc,CAAE,KAAK,CACrB,MAAM,CAAE,QAAQ,CAChB,mBAAmB,CAAE,IAAI,CACzB,gBAAgB,CAAE,IAAI,CACtB,eAAe,CAAE,IAAI,CAErB,WAAW,CAAE,IAAI,CACjB,KAAK,CchvBK,IAAI,CdkvBf,cAAe,CACd,mBAAmB,CAAE,IAAI,CACzB,gBAAgB,CAAE,IAAI,CACtB,eAAe,CAAE,IAAI,CAErB,WAAW,CAAE,IAAI,CACjB,KAAK,CcxvBK,IAAI,CdyvBd,WAAW,CAAE,GAAG,CAChB,SAAS,CAAE,IAAI,CACf,MAAM,CAAE,QAAQ,CAEjB,KAAM,CACL,QAAQ,CAAE,QAAQ,CAClB,OAAO,CAAE,CAAC,CACV,YAAY,CAAE,CAAC,CACf,UAAU,CAAE,IAAI,CAChB,aAAa,CAAE,CAAC,CAChB,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,WAAW,CACpB,OAAO,CAAE,IAAI,CACb,sBAAsB,CAAE,WAAW,CACnC,kBAAkB,CAAE,WAAW,CAC/B,cAAc,CAAE,WAAW,CAC3B,uBAAuB,CAAE,MAAM,CAC/B,aAAa,CAAE,MAAM,CACrB,eAAe,CAAE,MAAM,CACvB,mBAAmB,CAAE,MAAM,CAC3B,cAAc,CAAE,MAAM,CACtB,WAAW,CAAE,MAAM,CACnB,UAAU,CAAE,KAAK,CAElB,cAAe,CACd,gBAAgB,CcjxBN,OAAO,CdkxBjB,mBAAmB,CAAE,IAAI,CACzB,gBAAgB,CAAE,IAAI,CACtB,eAAe,CAAE,IAAI,CAErB,WAAW,CAAE,IAAI,CACjB,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,GAAG,CACX,0BAAc,CACb,YAAY,CAAE,QAAQ,CACtB,QAAQ,CAAE,QAAQ,CAClB,IAAI,CAAE,QAAQ,CAEf,yBAAa,CACZ,YAAY,CAAE,QAAQ,CACtB,QAAQ,CAAE,QAAQ,CAClB,IAAI,CAAE,QAAQ,CAGhB,WAAY,CACX,gBAAgB,CcryBN,OAAO,CdsyBjB,UAAU,CAAE,MAAM,CAClB,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CACZ,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,WAAW,CACpB,OAAO,CAAE,IAAI,CACb,uBAAuB,CAAE,MAAM,CAC/B,aAAa,CAAE,MAAM,CACrB,eAAe,CAAE,MAAM,CACvB,mBAAmB,CAAE,MAAM,CAC3B,cAAc,CAAE,MAAM,CACtB,WAAW,CAAE,MAAM,CAEnB,aAAa,CAAE,GAAG,CAClB,KAAK,CcrzBK,IAAI,CdszBd,0BAAiB,CAChB,OAAO,CAAE,OAAO,CAChB,WAAW,Cc9wBH,QAAQ,CdgxBjB,+BAAsB,CACrB,OAAO,CAAE,OAAO,CAChB,WAAW,CclxBH,QAAQ,CdoxBjB,8BAAqB,CACpB,OAAO,CAAE,OAAO,CAChB,WAAW,CctxBH,QAAQ,CdwxBjB,2BAAkB,CACjB,OAAO,CAAE,OAAO,CAChB,WAAW,Cc1xBH,QAAQ,Cd4xBjB,yBAAgB,CACf,OAAO,CAAE,OAAO,CAChB,WAAW,Cc9xBH,QAAQ,CdiyBlB,KAAM,CACL,UAAU,CAAE,KAAK,CACjB,gBAAgB,Cc70BN,IAAI,Cd+0Bd,aAAa,CAAE,WAAW,CAC1B,OAAO,CAAE,cAAc,CAExB,QAAS,CACR,UAAU,CAAE,MAAM,CAClB,gBAAQ,CACP,MAAM,CAAE,IAAI,CAGd,eAAgB,CACf,UAAU,CAAE,KAAK,CAElB,OAAQ,CACP,OAAO,CAAE,YAAY,CACrB,gBAAgB,Cc31BN,OAAO,Cd61BjB,aAAa,CAAE,GAAG,CAClB,OAAO,CAAE,QAAQ,CACjB,KAAK,Ccj2BK,IAAI,Cdm2Bd,UAAU,CAAE,uDAA0C,CACtD,eAAe,CAAE,IAAI,CACrB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,IAAI,CAEZ,UAAU,CAAE,8CAA8C,CAC1D,MAAM,CAAE,OAAO,CACf,aAAQ,CACP,KAAK,Cc32BI,IAAI,Cd62Bb,UAAU,CAAE,uDAAwC,CACpD,gBAAgB,Ccx2BP,OAAO,Cd22BlB,cAAe,CACd,OAAO,CAAE,QAAQ,CACjB,SAAS,CAAE,IAAI,CACf,UAAU,CAAE,cAAmB,CAC/B,KAAK,Cc72BK,IAAI,Cd82Bd,UAAU,Cct3BA,IAAI,Cdu3Bd,oBAAQ,CACP,KAAK,Cch3BI,IAAI,Cdi3Bb,UAAU,Ccz3BD,IAAI,Cd23Bb,UAAU,CAAE,qDAAuC,CAGrD,KAAM,CACL,YAAY,CAAE,CAAC,CACf,UAAU,CAAE,IAAI,CAChB,aAAa,CAAE,CAAC,CAChB,MAAM,CAAE,WAAW,CACnB,MAAM,CAAE,0BAAmB,CAE3B,aAAa,CAAE,GAAG,CAClB,6BAAwB,CACvB,UAAU,Ccn4BD,gBAAkB,Cdq4B1B,0CAAK,CACJ,KAAK,Ccr3BE,KAAO,Cdu3Bf,gDAAW,CACV,KAAK,Ccx3BE,KAAO,Cd43Bf,wCAAK,CACJ,KAAK,Ccp4BE,GAAO,Cds4Bf,8CAAW,CACV,KAAK,Ccv4BE,GAAO,Cd44BlB,WAAY,CACX,QAAQ,CAAE,QAAQ,CAClB,QAAQ,CAAE,MAAM,CAChB,OAAO,CAAE,QAAQ,CACjB,aAAa,CAAE,0BAAmB,CAClC,wBAAe,CACd,cAAc,CAAE,SAAS,CAE1B,sBAAa,CACZ,aAAa,CAAE,IAAI,CAEpB,+BAAoB,CACnB,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,WAAW,CACpB,OAAO,CAAE,IAAI,CACb,uBAAuB,CAAE,MAAM,CAC/B,aAAa,CAAE,MAAM,CACrB,eAAe,CAAE,MAAM,CACvB,mBAAmB,CAAE,MAAM,CAC3B,cAAc,CAAE,MAAM,CACtB,WAAW,CAAE,MAAM,CACnB,OAAO,CAAE,QAAQ,CACjB,QAAQ,CAAE,QAAQ,CAClB,GAAG,CAAE,CAAC,CACN,KAAK,CAAE,CAAC,CACR,MAAM,CAAE,CAAC,CAEV,8BAAqB,CACpB,KAAK,Cc56BI,OAAO,Cd86BjB,4BAAmB,CAClB,KAAK,Cc96BI,OAAO,Cdk7BjB,+BAAS,CACR,OAAO,CAAE,aAAa,CAEvB,6BAAK,CACJ,OAAO,CAAE,YAAY,CACrB,OAAO,CAAE,WAAW,CACpB,OAAO,CAAE,IAAI,CACb,uBAAuB,CAAE,MAAM,CAC/B,aAAa,CAAE,MAAM,CACrB,eAAe,CAAE,MAAM,CACvB,mBAAmB,CAAE,MAAM,CAC3B,cAAc,CAAE,MAAM,CACtB,WAAW,CAAE,MAAM,CACnB,OAAO,CAAE,QAAQ,CACjB,QAAQ,CAAE,QAAQ,CAClB,GAAG,CAAE,CAAC,CACN,KAAK,CAAE,CAAC,CACR,MAAM,CAAE,CAAC,CACT,gBAAgB,Ccn8BP,OAAO,Cdo8BhB,WAAW,CAAE,GAAG,CAChB,SAAS,CAAE,IAAI,CACf,oCAAS,CACR,YAAY,CAAE,GAAG,CACjB,WAAW,CAAE,GAAG,CAGlB,yCAAmB,CAClB,KAAK,Cc98BI,OAAO,Cd+8BhB,cAAc,CAAE,GAAG,CAEpB,uCAAiB,CAChB,KAAK,Ccj9BI,OAAO,Cdk9BhB,cAAc,CAAE,GAAG,CAGrB,SAAU,CAET,UAAU,CAAE,UAAU,CACtB,KAAK,CAAE,IAAI,CACX,SAAS,CAAE,IAAI,CACf,WAAW,CAAE,IAAI,CACjB,MAAM,CAAE,KAAK,CACb,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,yBAAmB,CAC3B,mBAAU,CACT,aAAa,CAAE,IAAI,CAGrB,YAAa,CACZ,UAAU,CAAE,MAAM,CAEnB,aAAc,CACb,MAAM,CAAE,IAAI,CACZ,KAAK,CAAE,IAAI,CAEZ,UAAW,CACV,KAAK,Ccv+BK,GAAO,Cdw+BjB,gBAAM,CACL,KAAK,Ccx+BI,IAAO,Cdy+BhB,MAAM,CAAE,aAAmB,CAG7B,QAAS,CACR,QAAQ,CAAE,QAAQ,CAClB,KAAK,CAAE,GAAG,CACV,MAAM,CAAE,GAAG,CACX,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,IAAI,CACZ,QAAQ,CAAE,MAAM,CAChB,IAAI,CAAE,gBAAgB,CACtB,MAAM,CAAE,CAAC,CAEV,kBAAmB,CAClB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,cAAmB,CAE3B,aAAa,CAAE,GAAG,CAClB,OAAO,CAAE,SAAS,CAClB,KAAK,CAAE,iBAAiB,CACxB,MAAM,CAAE,UAAU,CAEnB,sBAAuB,CACtB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,cAAmB,CAE3B,aAAa,CAAE,GAAG,CAClB,OAAO,CAAE,SAAS,CAClB,KAAK,CAAE,iBAAiB,CACxB,MAAM,CAAE,UAAU,CAEnB,iBAAkB,CACjB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,cAAmB,CAE3B,aAAa,CAAE,GAAG,CAClB,OAAO,CAAE,SAAS,CAClB,KAAK,CAAE,iBAAiB,CACxB,MAAM,CAAE,UAAU,CAEnB,oBAAqB,CACpB,OAAO,CAAE,IAAI,CACb,MAAM,CAAE,cAAmB,CAE3B,aAAa,CAAE,GAAG,CAClB,OAAO,CAAE,SAAS,CAClB,KAAK,CAAE,iBAAiB,CACxB,MAAM,CAAE,UAAU,CAEnB,KAAM,CACL,OAAO,CAAE,CAAC,CACV,gBAAW,CACV,OAAO,CAAE,IAAI,CACb,mCAAuB,CACtB,gBAAgB,Cc9iCR,IAAI,Cd+iCZ,KAAK,Cc5hCG,IAAI,Cd+hCd,gBAAW,CACV,KAAK,CcjiCI,IAAI,CdkiCb,MAAM,CAAE,OAAO,CACf,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,GAAG,CACZ,UAAU,CAAE,MAAM,CAClB,kBAAkB,CAAE,oBAAoB,CACxC,eAAe,CAAE,oBAAoB,CACrC,aAAa,CAAE,oBAAoB,CAEnC,UAAU,CAAE,oBAAoB,CAChC,sBAAQ,CACP,KAAK,Cc3iCG,IAAI,Cd8iCd,gBAAW,CACV,KAAK,CAAE,IAAI,CAEZ,UAAK,CACJ,OAAO,CAAE,IAAI,CACb,uBAAe,CACd,aAAa,CAAE,CAAC,CAInB,WAAY,CACX,KAAK,CAAE,IAAI,CAEZ,YAAa,CACZ,KAAK,CAAE,KAAK,CAEb,kBAAmB,CAClB,UAAU,CAAE,IAAI,CAChB,MAAM,CAAE,OAAO,CAEhB,MAAO,CAEN,UAAU,CAAE,iBAAmB,CAC/B,0BAAoB,CACnB,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,IAAI,CACb,gCAAQ,CACP,UAAU,CcrkCF,IAAO,CdskCf,KAAK,Cc7lCG,IAAI,Cd8lCZ,WAAW,CAAE,GAAG,CAChB,kBAAkB,CAAE,oBAAoB,CACxC,eAAe,CAAE,oBAAoB,CACrC,aAAa,CAAE,oBAAoB,CAEnC,UAAU,CAAE,oBAAoB,CAChC,sCAAQ,CACP,UAAU,Cc7kCH,OAAO,Cd8kCd,KAAK,CctmCE,IAAI,CdumCX,WAAW,CAAE,GAAG,CAIjB,wCAAQ,CACP,gBAAgB,CcnlCT,OAAO,CdqlCf,0CAAQ,CACP,MAAM,CAAE,KAAK,CACb,UAAU,CAAE,IAAI,CAChB,kBAAkB,CAAE,oBAAoB,CACxC,eAAe,CAAE,oBAAoB,CACrC,aAAa,CAAE,oBAAoB,CAEnC,UAAU,CAAE,oBAAoB,CAGlC,sCAAc,CACb,WAAW,CAAE,IAAI,CAGnB,YAAM,CACL,KAAK,CAAE,KAAK,CACZ,SAAS,CAAE,IAAI,CACf,MAAM,CAAE,OAAO,CAEhB,WAAK,CACJ,WAAW,Cc7lCJ,KAAK,Cd8lCZ,WAAW,CAAE,GAAG,CAChB,OAAO,CAAE,KAAK,CACd,OAAO,CAAE,mBAAmB,CAC5B,MAAM,CAAE,CAAC,CACT,MAAM,CAAE,OAAO,CAGjB,KAAM,CACL,UAAU,Cc3oCA,IAAI,Cd4oCd,KAAK,CcpoCK,IAAI,CdqoCd,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,CAAC,CACT,WAAW,CAAE,CAAC,CACd,YAAY,CAAE,IAAI,CAClB,aAAa,CAAE,IAAI,CACnB,OAAO,CAAE,KAAK,CACd,QAAQ,CAAE,MAAM,CAEhB,UAAU,CAAE,UAAU,CAEtB,UAAU,CAAE,YAAY,CAEzB,WAAY,CACX,UAAU,CclpCA,IAAI,CdmpCd,KAAK,Cc3pCK,IAAI,Cd6pCf,mBAAoB,CACnB,KAAK,CAAE,IAAI,CAEZ,yBAA0B,CACzB,kBAAkB,CAAE,6BAAuB,CAC3C,qBAAqB,CAAE,CAAC,CAExB,aAAa,CAAE,CAAC,CAEjB,yBAA0B,CACzB,qBAAqB,CAAE,CAAC,CAExB,aAAa,CAAE,CAAC,CAChB,UAAU,Cc/oCA,IAAI,CdgpCd,kBAAkB,CAAE,6BAAuB,CAE5C,gBAAiB,CAChB,aAAa,CAAE,GAAG,CAEnB,gBAAiB,CAChB,aAAa,CAAE,GAAG,CAEnB,aAAc,CACb,UAAU,CAAE,GAAG,CAEhB,aAAc,CACb,UAAU,CAAE,GAAG,CAEhB,MAAO,CACN,MAAM,CAAE,QAAQ,CAChB,SAAS,CAAE,KAAK,CAChB,gBAAgB,CchrCN,OAAO,CdkrCjB,aAAa,CAAE,GAAG,CAClB,OAAO,CAAE,IAAI,CACb,QAAQ,CAAE,QAAQ,CAClB,mBAAe,CACd,UAAU,CcprCD,GAAO,CdqrChB,KAAK,CctqCI,IAAO,CduqChB,OAAO,CAAE,cAAc,CACvB,sBAAG,CACF,KAAK,CczqCG,IAAO,Cd0qCf,MAAM,CAAE,CAAC,CAEV,sBAAG,CACF,MAAM,CAAE,CAAC,CAGX,aAAO,CACN,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,IAAI,CACZ,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,CAAC,CAET,UAAU,CAAE,IAAI,CAChB,MAAM,CAAE,aAAmB,CAC3B,OAAO,CAAE,IAAI,CACb,KAAK,CAAE,KAAK,CAEZ,aAAa,CAAE,GAAG,CAClB,QAAQ,CAAE,QAAQ,CAClB,KAAK,CAAE,CAAC,CACR,GAAG,CAAE,CAAC,CACN,gBAAgB,CAAE,WAAW,CAC7B,MAAM,CAAE,OAAO,CACf,kBAAkB,CAAE,oBAAoB,CACxC,eAAe,CAAE,oBAAoB,CACrC,aAAa,CAAE,oBAAoB,CAEnC,UAAU,CAAE,oBAAoB,CAChC,mBAAQ,CACP,gBAAgB,CcvsCR,IAAO,CdwsCf,KAAK,CcvtCG,GAAO,Cd2tClB,cAAe,CACd,QAAQ,CAAE,MAAM,CAEjB,+GAE2C,CACvC,gBAAgB,Cc7uCT,OAAO,Cd8uCd,kBAAkB,CAAE,oBAAoB,CACxC,eAAe,CAAE,oBAAoB,CACrC,aAAa,CAAE,oBAAoB,CACnC,UAAU,CAAE,oBAAoB,CAEpC,uFACiD,CAC7C,gBAAgB,CcpvCT,OAAO,CduvCjB,4BAAO,CACN,YAAY,Cc7uCH,GAAO,Cd+uCjB,8BAAS,CACR,YAAY,CchvCH,GAAO,CdkvCjB,wCAAmB,CAClB,YAAY,CcnvCH,GAAO,CdqvCjB,4CAAuB,CACtB,YAAY,CctvCH,GAAO,CdwvCjB,uCAAkB,CACjB,YAAY,CczvCH,GAAO,Cd2vCjB,0CAAqB,CACpB,YAAY,Cc5vCH,GAAO,Cd8vCjB,kCAAa,CACZ,MAAM,CAAE,SAAS,CACjB,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,IAAI,CACX,SAAS,CAAE,IAAI,CACf,KAAK,CcnwCI,GAAO,CdowChB,WAAW,CAAE,GAAG,CAGlB,qBAAsB,CACrB,OAAO,CAAE,UAAU,CACnB,KAAK,CAAE,IAAI,CACX,KAAK,CAAE,EAAE,CAEV,uCAAwC,CACvC,OAAO,CAAE,KAAK,CAEf,uCAAwC,CACvC,OAAO,CAAE,KAAK,CAEf,uCAAwC,CACvC,OAAO,CAAE,KAAK,CAEf,uCAAwC,CACvC,OAAO,CAAE,KAAK,CAEf,uCAAwC,CACvC,OAAO,CAAE,KAAK,CAEf,WAAY,CACX,QAAQ,CAAE,QAAQ,CAClB,GAAG,CAAE,CAAC,CACN,KAAK,CAAE,CAAC,CACR,MAAM,CAAE,CAAC,CAGT,+BAAc,CAEb,aAAa,CAAE,WAAW,CAE3B,8BAAa,CAEZ,aAAa,CAAE,WAAW", +"sources": ["scss/font-awesome/font-awesome.scss","scss/style.scss","scss/font-awesome/_path.scss","scss/font-awesome/_core.scss","scss/font-awesome/_larger.scss","scss/font-awesome/_fixed-width.scss","scss/font-awesome/_list.scss","scss/font-awesome/_variables.scss","scss/font-awesome/_bordered-pulled.scss","scss/font-awesome/_animated.scss","scss/font-awesome/_rotated-flipped.scss","scss/font-awesome/_mixins.scss","scss/font-awesome/_stacked.scss","scss/font-awesome/_icons.scss","scss/font-awesome/_screen-reader.scss","scss/_variables.scss"], +"names": [], +"file": "style.min.css" +} diff --git a/public/installer/fonts/FontAwesome.otf b/public/installer/fonts/FontAwesome.otf new file mode 100755 index 000000000..401ec0f36 Binary files /dev/null and b/public/installer/fonts/FontAwesome.otf differ diff --git a/public/installer/fonts/fontawesome-webfont.eot b/public/installer/fonts/fontawesome-webfont.eot new file mode 100755 index 000000000..e9f60ca95 Binary files /dev/null and b/public/installer/fonts/fontawesome-webfont.eot differ diff --git a/public/installer/fonts/fontawesome-webfont.svg b/public/installer/fonts/fontawesome-webfont.svg new file mode 100755 index 000000000..855c845e5 --- /dev/null +++ b/public/installer/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/installer/fonts/fontawesome-webfont.ttf b/public/installer/fonts/fontawesome-webfont.ttf new file mode 100755 index 000000000..35acda2fa Binary files /dev/null and b/public/installer/fonts/fontawesome-webfont.ttf differ diff --git a/public/installer/fonts/fontawesome-webfont.woff b/public/installer/fonts/fontawesome-webfont.woff new file mode 100755 index 000000000..400014a4b Binary files /dev/null and b/public/installer/fonts/fontawesome-webfont.woff differ diff --git a/public/installer/fonts/fontawesome-webfont.woff2 b/public/installer/fonts/fontawesome-webfont.woff2 new file mode 100755 index 000000000..4d13fc604 Binary files /dev/null and b/public/installer/fonts/fontawesome-webfont.woff2 differ diff --git a/public/installer/fonts/ionicons.eot b/public/installer/fonts/ionicons.eot new file mode 100755 index 000000000..92a3f20a3 Binary files /dev/null and b/public/installer/fonts/ionicons.eot differ diff --git a/public/installer/fonts/ionicons.svg b/public/installer/fonts/ionicons.svg new file mode 100755 index 000000000..49fc8f367 --- /dev/null +++ b/public/installer/fonts/ionicons.svg @@ -0,0 +1,2230 @@ + + + + + +Created by FontForge 20120731 at Thu Dec 4 09:51:48 2014 + By Adam Bradley +Created by Adam Bradley with FontForge 2.0 (http://fontforge.sf.net) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/installer/fonts/ionicons.ttf b/public/installer/fonts/ionicons.ttf new file mode 100755 index 000000000..c4e463248 Binary files /dev/null and b/public/installer/fonts/ionicons.ttf differ diff --git a/public/installer/fonts/ionicons.woff b/public/installer/fonts/ionicons.woff new file mode 100755 index 000000000..5f3a14e0a Binary files /dev/null and b/public/installer/fonts/ionicons.woff differ diff --git a/public/installer/img/background.png b/public/installer/img/background.png new file mode 100755 index 000000000..7ebb9d8a1 Binary files /dev/null and b/public/installer/img/background.png differ diff --git a/public/installer/img/favicon/favicon-16x16.png b/public/installer/img/favicon/favicon-16x16.png new file mode 100755 index 000000000..f42bb1887 Binary files /dev/null and b/public/installer/img/favicon/favicon-16x16.png differ diff --git a/public/installer/img/favicon/favicon-32x32.png b/public/installer/img/favicon/favicon-32x32.png new file mode 100755 index 000000000..b7290faa6 Binary files /dev/null and b/public/installer/img/favicon/favicon-32x32.png differ diff --git a/public/installer/img/favicon/favicon-96x96.png b/public/installer/img/favicon/favicon-96x96.png new file mode 100755 index 000000000..ee8b93d4f Binary files /dev/null and b/public/installer/img/favicon/favicon-96x96.png differ diff --git a/public/installer/img/pattern.png b/public/installer/img/pattern.png new file mode 100755 index 000000000..8b3a82581 Binary files /dev/null and b/public/installer/img/pattern.png differ diff --git a/public/js/jquery.min.js b/public/js/jquery.min.js new file mode 100755 index 000000000..bcd3956c8 --- /dev/null +++ b/public/js/jquery.min.js @@ -0,0 +1,4 @@ +/*! jQuery v3.2.1 | (c) JS Foundation and other contributors | jquery.org/license */ +!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.2.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext;function B(a,b){return a.nodeName&&a.nodeName.toLowerCase()===b.toLowerCase()}var C=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,D=/^.[^:#\[\.,]*$/;function E(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):D.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(E(this,a||[],!1))},not:function(a){return this.pushStack(E(this,a||[],!0))},is:function(a){return!!E(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var F,G=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,H=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||F,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:G.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),C.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};H.prototype=r.fn,F=r(d);var I=/^(?:parents|prev(?:Until|All))/,J={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function K(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return K(a,"nextSibling")},prev:function(a){return K(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return B(a,"iframe")?a.contentDocument:(B(a,"template")&&(a=a.content||a),r.merge([],a.childNodes))}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(J[a]||r.uniqueSort(e),I.test(a)&&e.reverse()),this.pushStack(e)}});var L=/[^\x20\t\r\n\f]+/g;function M(a){var b={};return r.each(a.match(L)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?M(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=e||a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function N(a){return a}function O(a){throw a}function P(a,b,c,d){var e;try{a&&r.isFunction(e=a.promise)?e.call(a).done(b).fail(c):a&&r.isFunction(e=a.then)?e.call(a,b,c):b.apply(void 0,[a].slice(d))}catch(a){c.apply(void 0,[a])}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==O&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:N,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:N)),c[2][3].add(g(0,a,r.isFunction(d)?d:O))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(P(a,g.done(h(c)).resolve,g.reject,!b),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)P(e[c],h(c),g.reject);return g.promise()}});var Q=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&Q.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var R=r.Deferred();r.fn.ready=function(a){return R.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||R.resolveWith(d,[r]))}}),r.ready.then=R.then;function S(){d.removeEventListener("DOMContentLoaded",S), +a.removeEventListener("load",S),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",S),a.addEventListener("load",S));var T=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)T(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){X.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=W.get(a,b),c&&(!d||Array.isArray(c)?d=W.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return W.get(a,c)||W.access(a,c,{empty:r.Callbacks("once memory").add(function(){W.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,la=/^$|\/(?:java|ecma)script/i,ma={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};ma.optgroup=ma.option,ma.tbody=ma.tfoot=ma.colgroup=ma.caption=ma.thead,ma.th=ma.td;function na(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&B(a,b)?r.merge([a],c):c}function oa(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=na(l.appendChild(f),"script"),j&&oa(g),c){k=0;while(f=g[k++])la.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var ra=d.documentElement,sa=/^key/,ta=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ua=/^([^.]*)(?:\.(.+)|)/;function va(){return!0}function wa(){return!1}function xa(){try{return d.activeElement}catch(a){}}function ya(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)ya(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=wa;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(ra,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(L)||[""],j=b.length;while(j--)h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=W.hasData(a)&&W.get(a);if(q&&(i=q.events)){b=(b||"").match(L)||[""],j=b.length;while(j--)if(h=ua.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&W.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(W.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,Aa=/\s*$/g;function Ea(a,b){return B(a,"table")&&B(11!==b.nodeType?b:b.firstChild,"tr")?r(">tbody",a)[0]||a:a}function Fa(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Ga(a){var b=Ca.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ha(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(W.hasData(a)&&(f=W.access(a),g=W.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Ba.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ja(f,b,c,d)});if(m&&(e=qa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(na(e,"script"),Fa),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=na(h),f=na(a),d=0,e=f.length;d0&&oa(g,!i&&na(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(U(c)){if(b=c[W.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[W.expando]=void 0}c[X.expando]&&(c[X.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ka(this,a,!0)},remove:function(a){return Ka(this,a)},text:function(a){return T(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.appendChild(a)}})},prepend:function(){return Ja(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Ea(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ja(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(na(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return T(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!Aa.test(a)&&!ma[(ka.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function _a(a,b,c,d,e){return new _a.prototype.init(a,b,c,d,e)}r.Tween=_a,_a.prototype={constructor:_a,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=_a.propHooks[this.prop];return a&&a.get?a.get(this):_a.propHooks._default.get(this)},run:function(a){var b,c=_a.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):_a.propHooks._default.set(this),this}},_a.prototype.init.prototype=_a.prototype,_a.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},_a.propHooks.scrollTop=_a.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=_a.prototype.init,r.fx.step={};var ab,bb,cb=/^(?:toggle|show|hide)$/,db=/queueHooks$/;function eb(){bb&&(d.hidden===!1&&a.requestAnimationFrame?a.requestAnimationFrame(eb):a.setTimeout(eb,r.fx.interval),r.fx.tick())}function fb(){return a.setTimeout(function(){ab=void 0}),ab=r.now()}function gb(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ca[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function hb(a,b,c){for(var d,e=(kb.tweeners[b]||[]).concat(kb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?lb:void 0)),void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b), +null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&B(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(L);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),lb={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=mb[b]||r.find.attr;mb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=mb[g],mb[g]=e,e=null!=c(a,b,d)?g:null,mb[g]=f),e}});var nb=/^(?:input|select|textarea|button)$/i,ob=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return T(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):nb.test(a.nodeName)||ob.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function pb(a){var b=a.match(L)||[];return b.join(" ")}function qb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,qb(this)))});if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,qb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(L)||[];while(c=this[i++])if(e=qb(c),d=1===c.nodeType&&" "+pb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=pb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,qb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(L)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=qb(this),b&&W.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":W.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+pb(qb(c))+" ").indexOf(b)>-1)return!0;return!1}});var rb=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":Array.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(rb,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:pb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(Array.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var sb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!sb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,sb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(W.get(h,"events")||{})[b.type]&&W.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&U(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!U(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=W.access(d,b);e||d.addEventListener(a,c,!0),W.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=W.access(d,b)-1;e?W.access(d,b,e):(d.removeEventListener(a,c,!0),W.remove(d,b))}}});var tb=a.location,ub=r.now(),vb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var wb=/\[\]$/,xb=/\r?\n/g,yb=/^(?:submit|button|image|reset|file)$/i,zb=/^(?:input|select|textarea|keygen)/i;function Ab(a,b,c,d){var e;if(Array.isArray(b))r.each(b,function(b,e){c||wb.test(a)?d(a,e):Ab(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)Ab(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(Array.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)Ab(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&zb.test(this.nodeName)&&!yb.test(a)&&(this.checked||!ja.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:Array.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(xb,"\r\n")}}):{name:b.name,value:c.replace(xb,"\r\n")}}).get()}});var Bb=/%20/g,Cb=/#.*$/,Db=/([?&])_=[^&]*/,Eb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Fb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Gb=/^(?:GET|HEAD)$/,Hb=/^\/\//,Ib={},Jb={},Kb="*/".concat("*"),Lb=d.createElement("a");Lb.href=tb.href;function Mb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(L)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Nb(a,b,c,d){var e={},f=a===Jb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Ob(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Pb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Qb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:tb.href,type:"GET",isLocal:Fb.test(tb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Kb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Ob(Ob(a,r.ajaxSettings),b):Ob(r.ajaxSettings,a)},ajaxPrefilter:Mb(Ib),ajaxTransport:Mb(Jb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Eb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||tb.href)+"").replace(Hb,tb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(L)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Lb.protocol+"//"+Lb.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Nb(Ib,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Gb.test(o.type),f=o.url.replace(Cb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(Bb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(vb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Db,"$1"),n=(vb.test(f)?"&":"?")+"_="+ub++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Kb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Nb(Jb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Pb(o,y,d)),v=Qb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Rb={0:200,1223:204},Sb=r.ajaxSettings.xhr();o.cors=!!Sb&&"withCredentials"in Sb,o.ajax=Sb=!!Sb,r.ajaxTransport(function(b){var c,d;if(o.cors||Sb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Rb[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + + +
+ +
+ + \ No newline at end of file diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php new file mode 100755 index 000000000..d360dbef2 --- /dev/null +++ b/resources/views/app.blade.php @@ -0,0 +1,76 @@ + + ($appearance ?? 'system') == 'dark'])> + + + + + + + {{-- Inline script to detect system dark mode preference and apply it immediately --}} + + + {{-- Inline style to set the HTML background color based on our theme in app.css --}} + + + {{ config('app.name', 'Laravel') }} + + + + + @routes + @if (app()->environment('local')) + @viteReactRefresh + @endif + @vite(['resources/js/app.tsx', "resources/js/pages/{$page['component']}.tsx"]) + + @inertiaHead + + + + @inertia + + + diff --git a/resources/views/emails/test.blade.php b/resources/views/emails/test.blade.php new file mode 100755 index 000000000..e1f7e98a7 --- /dev/null +++ b/resources/views/emails/test.blade.php @@ -0,0 +1,53 @@ + + + + + Test Email + + + +
+
+

Test Email

+
+ +

Hello,

+ +

This is a test email from {{ config('app.name') }}.

+ +

If you received this email, your email configuration is working correctly.

+ +

Thank you!

+ + +
+ + \ No newline at end of file diff --git a/resources/views/employees/certificates/experience-certificate.blade.php b/resources/views/employees/certificates/experience-certificate.blade.php new file mode 100755 index 000000000..5438a8a01 --- /dev/null +++ b/resources/views/employees/certificates/experience-certificate.blade.php @@ -0,0 +1,60 @@ +@extends('employees.certificates.layouts.master') + +@section('title', ucfirst(str_replace('_', ' ', $type))) +@section('certificate-type', ucfirst(str_replace('_', ' ', $type))) +@section('element-id', 'boxes') + +@section('content') + {!! $content !!} +@endsection + +@section('scripts') + + +@endsection diff --git a/resources/views/employees/certificates/joining-letter.blade.php b/resources/views/employees/certificates/joining-letter.blade.php new file mode 100755 index 000000000..5438a8a01 --- /dev/null +++ b/resources/views/employees/certificates/joining-letter.blade.php @@ -0,0 +1,60 @@ +@extends('employees.certificates.layouts.master') + +@section('title', ucfirst(str_replace('_', ' ', $type))) +@section('certificate-type', ucfirst(str_replace('_', ' ', $type))) +@section('element-id', 'boxes') + +@section('content') + {!! $content !!} +@endsection + +@section('scripts') + + +@endsection diff --git a/resources/views/employees/certificates/layouts/master.blade.php b/resources/views/employees/certificates/layouts/master.blade.php new file mode 100755 index 000000000..0bfc2cdd1 --- /dev/null +++ b/resources/views/employees/certificates/layouts/master.blade.php @@ -0,0 +1,99 @@ + + + + + + + @yield('title', 'Employee Certificate') + + + + @yield('additional-styles') + + + +
+
+
{{ $companyName ?? 'Company Name' }}
+
@yield('certificate-type', 'Certificate')
+
+ +
+ @yield('content') +
+ + @hasSection('employee-details') +
+ @yield('employee-details') +
+ @endif +
+ + @yield('scripts') + + + diff --git a/resources/views/employees/certificates/noc-certificate.blade.php b/resources/views/employees/certificates/noc-certificate.blade.php new file mode 100755 index 000000000..5438a8a01 --- /dev/null +++ b/resources/views/employees/certificates/noc-certificate.blade.php @@ -0,0 +1,60 @@ +@extends('employees.certificates.layouts.master') + +@section('title', ucfirst(str_replace('_', ' ', $type))) +@section('certificate-type', ucfirst(str_replace('_', ' ', $type))) +@section('element-id', 'boxes') + +@section('content') + {!! $content !!} +@endsection + +@section('scripts') + + +@endsection diff --git a/resources/views/exports/holidays-pdf.blade.php b/resources/views/exports/holidays-pdf.blade.php new file mode 100755 index 000000000..21ff97171 --- /dev/null +++ b/resources/views/exports/holidays-pdf.blade.php @@ -0,0 +1,75 @@ + + + + + Holidays {{ $year }} + + + +
+

Company Holidays - {{ $year }}

+

Generated on {{ date('F j, Y') }}

+
+ + + + + + + + + + + + + + @foreach($holidays as $holiday) + + + + + + + + + @endforeach + +
Holiday NameDateCategoryTypeBranchesDescription
{{ $holiday->name }} + @if($holiday->end_date && $holiday->start_date !== $holiday->end_date) + {{ \Carbon\Carbon::parse($holiday->start_date)->format('M j, Y') }} - + {{ \Carbon\Carbon::parse($holiday->end_date)->format('M j, Y') }} + @else + {{ \Carbon\Carbon::parse($holiday->start_date)->format('M j, Y') }} + @endif + + + {{ ucfirst(str_replace('-', ' ', $holiday->category)) }} + + + @if($holiday->is_half_day) Half Day, @endif + {{ $holiday->is_paid ? 'Paid' : 'Unpaid' }} + @if($holiday->is_recurring), Recurring @endif + + @if($holiday->branches->count() > 0) + {{ $holiday->branches->pluck('name')->join(', ') }} + @else + All Branches + @endif + {{ $holiday->description ?: '-' }}
+ + @if($holidays->count() === 0) +

No holidays found for {{ $year }}.

+ @endif + + \ No newline at end of file diff --git a/resources/views/payslips/template.blade.php b/resources/views/payslips/template.blade.php new file mode 100755 index 000000000..56d792965 --- /dev/null +++ b/resources/views/payslips/template.blade.php @@ -0,0 +1,263 @@ + + + + + + Payslip - {{ $payrollEntry->employee->name }} + + + + +
+
+
+ {{ isset($companySettings['titleText']) ? $companySettings['titleText'] : config('app.name', 'HRMGo SaaS') }} +
+ @if (isset($companySettings['companyAddress'])) +
{{ $companySettings['companyAddress'] }}
+ @endif +
+ @if (isset($companySettings['companyEmail'])) + Email: {{ $companySettings['companyEmail'] }} + @endif + @if (isset($companySettings['companyMobile'])) + | Phone: {{ $companySettings['companyMobile'] }} + @endif +
+
Salary Slip
+
{{ $payrollEntry->payrollRun->pay_period_start->format('F Y') }}
+
+ + + + + + + + + + + + + + + + + + + + + + + + @if (isset($employeeData->bank_name) || isset($employeeData->bank_account_number)) + + + + + + + @endif +
Employee Information
Employee Name{{ $payrollEntry->employee->name }}Employee ID{{ $employeeData->employee_id ?? $payrollEntry->employee->id }}
Email{{ $payrollEntry->employee->email }}Pay Period{{ $payrollEntry->payrollRun->pay_period_start->format('d M Y') }} - + {{ $payrollEntry->payrollRun->pay_period_end->format('d M Y') }}
Basic Salary{{ formatCurrency($payrollEntry->basic_salary) }}Generated On{{ now()->format('d M Y') }}
Bank Name{{ $employeeData->bank_name ?? 'N/A' }}Account Number{{ $employeeData->account_number ?? 'N/A' }}
+ + @if ($payrollEntry->working_days > 0) + + + + + + + + + + + + + + + +
Attendance Summary
Working Days
{{ $payrollEntry->working_days }}
Present
{{ $payrollEntry->present_days }}
Paid Leave
{{ $payrollEntry->paid_leave_days }}
Unpaid Leave
{{ $payrollEntry->unpaid_leave_days }}
Half Days
{{ $payrollEntry->half_days }}
Absent
{{ $payrollEntry->absent_days }}
Overtime Hours: + {{ number_format($payrollEntry->overtime_hours, 1) }}h
+ @endif + + @php + $unpaidLeaveDeduction = $payrollEntry->unpaid_leave_deduction ?? 0; + @endphp + + @if ($unpaidLeaveDeduction > 0) + + + + + + + + + + + + +
Deduction Calculation
Per Day Salary + ({{ formatCurrency($payrollEntry->basic_salary) }} / {{ $payrollEntry->working_days }} days) + {{ formatCurrency($payrollEntry->per_day_salary ?? 0) }}
Unpaid Leave Deduction (Absent + Half Days + Unpaid Leave){{ formatCurrency($unpaidLeaveDeduction) }}
+ @endif + + + + + + + + + + + + @php + $earnings = $payrollEntry->earnings_breakdown ?? []; + $deductions = $payrollEntry->deductions_breakdown ?? []; + + if ($payrollEntry->overtime_amount > 0) { + $earnings['Overtime Amount'] = $payrollEntry->overtime_amount; + } + + if ($payrollEntry->unpaid_leave_deduction > 0) { + $deductions['Unpaid Leave Deduction'] = $payrollEntry->unpaid_leave_deduction; + } + + $maxRows = max(count($earnings), count($deductions), 1); + $earningsKeys = array_keys($earnings); + $deductionsKeys = array_keys($deductions); + + $totalEarnings = $payrollEntry->total_earnings + $payrollEntry->overtime_amount; + $totalDeductions = $payrollEntry->total_deductions + $payrollEntry->unpaid_leave_deduction; + @endphp + + @for ($i = 0; $i < $maxRows; $i++) + + + + + + + @endfor + + + + + + + + + + + + +
Salary Details
EarningsAmountDeductionsAmount
{{ $earningsKeys[$i] ?? '' }} + {{ isset($earningsKeys[$i]) ? formatCurrency($earnings[$earningsKeys[$i]]) : '' }}{{ $deductionsKeys[$i] ?? '' }} + {{ isset($deductionsKeys[$i]) ? formatCurrency($deductions[$deductionsKeys[$i]]) : '' }}
Total Earnings{{ formatCurrency($totalEarnings) }}Total Deductions{{ formatCurrency($totalDeductions) }}
Net Salary (Take Home){{ formatCurrency($payrollEntry->net_pay) }}
+ + +
+ + + + + diff --git a/resources/views/vendor/installer/environment-classic.blade.php b/resources/views/vendor/installer/environment-classic.blade.php new file mode 100755 index 000000000..20f4dafb9 --- /dev/null +++ b/resources/views/vendor/installer/environment-classic.blade.php @@ -0,0 +1,38 @@ +@extends('vendor.installer.layouts.master') + +@section('template_title') + {{ trans('installer_messages.environment.classic.templateTitle') }} +@endsection + +@section('title') + {{ trans('installer_messages.environment.classic.title') }} +@endsection + +@section('container') + +
+ {!! csrf_field() !!} + +
+ +
+
+ + @if( ! isset($environment['errors'])) + + @endif + +@endsection \ No newline at end of file diff --git a/resources/views/vendor/installer/environment-wizard.blade.php b/resources/views/vendor/installer/environment-wizard.blade.php new file mode 100755 index 000000000..04a57102f --- /dev/null +++ b/resources/views/vendor/installer/environment-wizard.blade.php @@ -0,0 +1,184 @@ +@extends('vendor.installer.layouts.master') + +@section('template_title') + {{ trans('installer_messages.environment.wizard.templateTitle') }} +@endsection + +@section('title') + + {!! trans('installer_messages.environment.wizard.title') !!} +@endsection + +@section('container') +
+ @csrf + + +
+

+ + {{ trans('installer_messages.environment.wizard.tabs.environment') }} +

+
+
+ + + @if ($errors->has('app_name')) +

+ + {{ $errors->first('app_name') }} +

+ @endif +
+ +
+ + + +
+ +
+ +
+ + +
+
+ +
+ + +
+
+
+ + +
+

+ + {{ trans('installer_messages.environment.wizard.tabs.database') }} +

+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + + + + + + + + + + + + + + + + + + + +
+ +
+
+@endsection + +@section('scripts') + +@endsection \ No newline at end of file diff --git a/resources/views/vendor/installer/environment.blade.php b/resources/views/vendor/installer/environment.blade.php new file mode 100755 index 000000000..35465bb5c --- /dev/null +++ b/resources/views/vendor/installer/environment.blade.php @@ -0,0 +1,91 @@ +@extends('vendor.installer.layouts.master') + +@section('template_title') + {{ trans('installer_messages.environment.menu.templateTitle') }} +@endsection + +@section('title') + + {!! trans('installer_messages.environment.menu.title') !!} +@endsection + +@section('container') +
+

+ {!! trans('installer_messages.environment.menu.desc') !!} +

+
+ +
+ +
+
+
+ +
+

{{ trans('installer_messages.environment.menu.wizard-button') }}

+

+ Guided setup with step-by-step configuration. Perfect for beginners and quick installations. +

+
+
+ + Easy form-based setup +
+
+ + Automatic validation +
+
+ + Recommended for most users +
+
+ + + Use Wizard Setup + +
+
+ + +
+
+
+ +
+

{{ trans('installer_messages.environment.menu.classic-button') }}

+

+ Manual configuration by editing the .env file directly. For advanced users who prefer full control. +

+
+
+ + Direct .env file editing +
+
+ + Full configuration control +
+
+ + For advanced users +
+
+ + + Use Classic Setup + +
+
+
+ +
+
+
+ + Recommendation: Use the Wizard setup for the best experience +
+
+
+@endsection diff --git a/resources/views/vendor/installer/finished.blade.php b/resources/views/vendor/installer/finished.blade.php new file mode 100755 index 000000000..728086d66 --- /dev/null +++ b/resources/views/vendor/installer/finished.blade.php @@ -0,0 +1,124 @@ +@extends('vendor.installer.layouts.master') + +@section('template_title') + {{ trans('installer_messages.final.templateTitle') }} +@endsection + +@section('title') + + {{ trans('installer_messages.final.title') }} +@endsection + +@section('container') +
+
+ +
+

Installation Completed Successfully!

+

Your application has been installed and is ready to use.

+
+ + +
+

+ + Default User Credentials +

+ @if (isSaas()) +
+
+

Super Admin

+
+
+ Email: + superadmin@example.com +
+
+ Password: + password +
+
+
+
+

Company User

+
+
+ Email: + company@example.com +
+
+ Password: + password +
+
+
+
+ @else +
+
+

Company User

+
+
+ Email: + company@example.com +
+
+ Password: + password +
+
+
+
+ @endif +
+
+ + Please change these default passwords after logging in for security. +
+
+
+ +
+ @if (session('message')['dbOutputLog']) +
+

+ + {{ trans('installer_messages.final.migration') }} +

+
{{ session('message')['dbOutputLog'] }}
+
+ @endif + +
+

+ + {{ trans('installer_messages.final.console') }} +

+
{{ $finalMessages }}
+
+ +
+

+ + {{ trans('installer_messages.final.log') }} +

+
{{ $finalStatusMessage }}
+
+ +
+

+ + {{ trans('installer_messages.final.env') }} +

+
{{ $finalEnvFile }}
+
+
+ + +@endsection diff --git a/resources/views/vendor/installer/layouts/master-update.blade.php b/resources/views/vendor/installer/layouts/master-update.blade.php new file mode 100755 index 000000000..cb8f138b2 --- /dev/null +++ b/resources/views/vendor/installer/layouts/master-update.blade.php @@ -0,0 +1,55 @@ + + + + + + + + @if (trim($__env->yieldContent('template_title')))@yield('template_title') | @endif {{ trans('installer_messages.updater.title') }} + + + + @yield('style') + + + +
+
+
+

@yield('title')

+

{{ trans('installer_messages.updater.title') }}

+
+
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ @yield('container') +
+
+
+ @yield('scripts') + + \ No newline at end of file diff --git a/resources/views/vendor/installer/layouts/master.blade.php b/resources/views/vendor/installer/layouts/master.blade.php new file mode 100755 index 000000000..e9ab1df51 --- /dev/null +++ b/resources/views/vendor/installer/layouts/master.blade.php @@ -0,0 +1,124 @@ + + + + + + + + @if (trim($__env->yieldContent('template_title')))@yield('template_title') | @endif {{ trans('installer_messages.title') }} + + + + @yield('style') + + + +
+
+ +
+

@yield('title')

+

{{ trans('installer_messages.title') }}

+
+ + +
+
+
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+ + +
+ @if (session('message')) +
+
+ +

+ @if(is_array(session('message'))) + {{ session('message')['message'] }} + @else + {{ session('message') }} + @endif +

+
+
+ @endif + + @if(session()->has('errors')) +
+
+ +
+

{{ trans('installer_messages.forms.errorTitle') }}

+
    + @foreach($errors->all() as $error) +
  • • {{ $error }}
  • + @endforeach +
+
+ +
+
+ @endif + + @yield('container') +
+
+
+ + @yield('scripts') + + + diff --git a/resources/views/vendor/installer/permissions.blade.php b/resources/views/vendor/installer/permissions.blade.php new file mode 100755 index 000000000..1d91e0e68 --- /dev/null +++ b/resources/views/vendor/installer/permissions.blade.php @@ -0,0 +1,74 @@ +@extends('vendor.installer.layouts.master') + +@section('template_title') + {{ trans('installer_messages.permissions.templateTitle') }} +@endsection + +@section('title') + + {{ trans('installer_messages.permissions.title') }} +@endsection + +@section('container') +
+
+
+ +

Checking folder permissions required for the application to function properly.

+
+
+
+ +
+

+ + Directory Permissions +

+ +
+ @foreach($permissions['permissions'] as $permission) +
+
+
+ +
+
+ {{ $permission['folder'] }} +
Required for file operations
+
+
+
+ + {{ $permission['permission'] }} + +
+
+ @endforeach +
+
+ + @if ( ! isset($permissions['errors'])) +
+
+
+ + All permissions are correctly set! +
+
+ + {{ trans('installer_messages.permissions.next') }} + + +
+ @else +
+
+
+ + Permission issues detected +
+

Please fix the folder permissions above before continuing.

+
+
+ @endif +@endsection diff --git a/resources/views/vendor/installer/requirements.blade.php b/resources/views/vendor/installer/requirements.blade.php new file mode 100755 index 000000000..5100f79fa --- /dev/null +++ b/resources/views/vendor/installer/requirements.blade.php @@ -0,0 +1,75 @@ +@extends('vendor.installer.layouts.master') + +@section('template_title') + {{ trans('installer_messages.requirements.templateTitle') }} +@endsection + +@section('title') + + {{ trans('installer_messages.requirements.title') }} +@endsection + +@section('container') +
+ @foreach($requirements['requirements'] as $type => $requirement) +
+
+

+ + {{ ucfirst($type) }} Requirements + @if($type == 'php') + + (version {{ $phpSupportInfo['minimum'] }} required) + + @endif +

+ @if($type == 'php') +
+ + {{ $phpSupportInfo['current'] }} + +
+ +
+
+ @endif +
+ +
+ @foreach($requirements['requirements'][$type] as $extension => $enabled) +
+ {{ $extension }} +
+ +
+
+ @endforeach +
+
+ @endforeach +
+ + @if ( ! isset($requirements['errors']) && $phpSupportInfo['supported'] ) +
+
+
+ + All requirements are satisfied! +
+
+ + {{ trans('installer_messages.requirements.next') }} + + +
+ @else +
+
+
+ + Please fix the requirements above before continuing. +
+
+
+ @endif +@endsection \ No newline at end of file diff --git a/resources/views/vendor/installer/update/finished.blade.php b/resources/views/vendor/installer/update/finished.blade.php new file mode 100755 index 000000000..280c342e4 --- /dev/null +++ b/resources/views/vendor/installer/update/finished.blade.php @@ -0,0 +1,26 @@ +@extends('vendor.installer.layouts.master-update') + +@section('title', trans('installer_messages.updater.final.title')) +@section('container') +
+
+
+ +
+

Update Completed Successfully!

+

{{ session('message')['message'] }}

+
+ +
+
+ + Your application has been updated successfully! +
+
+ + + + {{ trans('installer_messages.updater.final.exit') }} + +
+@stop \ No newline at end of file diff --git a/resources/views/vendor/installer/update/overview.blade.php b/resources/views/vendor/installer/update/overview.blade.php new file mode 100755 index 000000000..80af51f14 --- /dev/null +++ b/resources/views/vendor/installer/update/overview.blade.php @@ -0,0 +1,28 @@ +@extends('vendor.installer.layouts.master-update') + +@section('title', trans('installer_messages.updater.overview.title')) +@section('container') +
+
+
+ +
+

Update Overview

+

+ {{ trans_choice('installer_messages.updater.overview.message', $numberOfUpdatesPending, ['number' => $numberOfUpdatesPending]) }} +

+
+ +
+
+ + {{ $numberOfUpdatesPending }} update(s) pending +
+
+ + + + {{ trans('installer_messages.updater.overview.install_updates') }} + +
+@stop \ No newline at end of file diff --git a/resources/views/vendor/installer/update/welcome.blade.php b/resources/views/vendor/installer/update/welcome.blade.php new file mode 100755 index 000000000..0680efa6e --- /dev/null +++ b/resources/views/vendor/installer/update/welcome.blade.php @@ -0,0 +1,21 @@ +@extends('vendor.installer.layouts.master-update') + +@section('title', trans('installer_messages.updater.welcome.title')) +@section('container') +
+
+
+ +
+

Welcome to the Update Wizard

+

+ {{ trans('installer_messages.updater.welcome.message') }} +

+
+ + + {{ trans('installer_messages.next') }} + + +
+@stop \ No newline at end of file diff --git a/resources/views/vendor/installer/welcome.blade.php b/resources/views/vendor/installer/welcome.blade.php new file mode 100755 index 000000000..c70add771 --- /dev/null +++ b/resources/views/vendor/installer/welcome.blade.php @@ -0,0 +1,52 @@ +@extends('vendor.installer.layouts.master') + +@section('template_title') + {{ trans('installer_messages.welcome.templateTitle') }} +@endsection + +@section('title') + {{ trans('installer_messages.welcome.title') }} +@endsection + +@section('container') +
+
+
+ +
+

Welcome to the Installation Wizard

+

+ {{ trans('installer_messages.welcome.message') }} +

+
+ +
+
+

What we'll set up:

+
+
+ + System Requirements +
+
+ + File Permissions +
+
+ + Database Configuration +
+
+ + Application Setup +
+
+
+ + + {{ trans('installer_messages.welcome.next') }} + + +
+
+@endsection diff --git a/routes/auth.php b/routes/auth.php new file mode 100644 index 000000000..c421ef643 --- /dev/null +++ b/routes/auth.php @@ -0,0 +1,59 @@ +group(function () { + + Route::middleware('checksaas')->group(function () { + Route::get('register', [RegisteredUserController::class, 'create']) + ->name('register'); + Route::post('register', [RegisteredUserController::class, 'store']); + }); + + + Route::get('login', [AuthenticatedSessionController::class, 'create']) + ->name('login'); + + Route::post('login', [AuthenticatedSessionController::class, 'store']); + + Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) + ->name('password.request'); + + Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) + ->name('password.email'); + + Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) + ->name('password.reset'); + + Route::post('reset-password', [NewPasswordController::class, 'store']) + ->name('password.store'); +}); + +Route::middleware('auth')->group(function () { + Route::get('verify-email', EmailVerificationPromptController::class) + ->name('verification.notice'); + + Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) + ->middleware(['signed', 'throttle:6,1']) + ->name('verification.verify'); + + Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) + ->middleware('throttle:6,1') + ->name('verification.send'); + + Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) + ->name('password.confirm'); + + Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); + + Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) + ->name('logout'); +}); diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 000000000..3c9adf1af --- /dev/null +++ b/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/routes/settings.php b/routes/settings.php new file mode 100644 index 000000000..4150061f5 --- /dev/null +++ b/routes/settings.php @@ -0,0 +1,122 @@ +group(function () { + Route::get('/payment-methods', [PaymentSettingController::class, 'getPaymentMethods'])->name('payment.methods'); + Route::get('/enabled-payment-methods', [PaymentSettingController::class, 'getEnabledMethods'])->name('payment.enabled-methods'); + Route::post('/plan-orders', [PlanOrderController::class, 'create'])->name('plan-orders.create'); + Route::post('/stripe-payment', [StripePaymentController::class, 'processPayment'])->name('settings.stripe.payment'); +}); + +Route::middleware(['auth', 'verified', 'plan.access'])->group(function () { + // Payment Settings (admin only) + Route::post('/payment-settings', [PaymentSettingController::class, 'store'])->name('payment.settings'); + + // Profile settings page with profile and password sections + Route::get('profile', function () { + return Inertia::render('settings/profile-settings'); + })->name('profile'); + + // Routes for form submissions + Route::patch('profile', [ProfileController::class, 'update'])->name('profile.update'); + Route::post('profile', [ProfileController::class, 'update']); // For file uploads with method spoofing + Route::delete('profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); + Route::put('profile/password', [PasswordController::class, 'update'])->name('password.update'); + + // Email settings page + Route::get('settings/email', function () { + return Inertia::render('settings/components/email-settings'); + })->name('settings.email'); + + // Email settings routes + Route::get('settings/email/get', [EmailSettingController::class, 'getEmailSettings'])->name('settings.email.get'); + Route::post('settings/email/update', [EmailSettingController::class, 'updateEmailSettings'])->name('settings.email.update'); + Route::post('settings/email/test', [EmailSettingController::class, 'sendTestEmail'])->name('settings.email.test'); + + // General settings page with system and company settings + Route::get('settings', [SettingsController::class, 'index'])->name('settings'); + + // System Settings routes + Route::post('settings/system', [SystemSettingsController::class, 'update'])->name('settings.system.update'); + Route::post('settings/brand', [SystemSettingsController::class, 'updateBrand'])->name('settings.brand.update'); + Route::post('settings/storage', [SystemSettingsController::class, 'updateStorage'])->name('settings.storage.update'); + Route::post('settings/recaptcha', [SystemSettingsController::class, 'updateRecaptcha'])->name('settings.recaptcha.update'); + Route::post('settings/chatgpt', [SystemSettingsController::class, 'updateChatgpt'])->name('settings.chatgpt.update'); + Route::post('settings/cookie', [SystemSettingsController::class, 'updateCookie'])->name('settings.cookie.update'); + Route::post('settings/seo', [SystemSettingsController::class, 'updateSeo'])->name('settings.seo.update'); + Route::post('settings/cache/clear', [SystemSettingsController::class, 'clearCache'])->name('settings.cache.clear'); + + // Currency Settings routes + Route::post('settings/currency', [CurrencySettingController::class, 'update'])->name('settings.currency.update'); + + // Working Days Settings routes + Route::get('settings/working-days/get', [WorkingDaysSettingController::class, 'getWorkingDaysSettings'])->name('settings.working-days.get'); + Route::post('settings/working-days/update', [WorkingDaysSettingController::class, 'updateWorkingDaysSettings'])->name('settings.working-days.update'); + + // Webhook Settings routes + Route::get('settings/webhooks', [WebhookController::class, 'index'])->name('settings.webhooks.index'); + Route::post('settings/webhooks', [WebhookController::class, 'store'])->name('settings.webhooks.store'); + Route::put('settings/webhooks/{webhook}', [WebhookController::class, 'update'])->name('settings.webhooks.update'); + Route::delete('settings/webhooks/{webhook}', [WebhookController::class, 'destroy'])->name('settings.webhooks.destroy'); + + // Google Calendar Settings routes + Route::post('settings/google-calendar', [SystemSettingsController::class, 'updateGoogleCalendar'])->name('settings.google-calendar.update'); + + // IP Restriction Settings routes + Route::middleware('permission:manage-ip-restriction-settings')->group(function () { + Route::post('ip-restrictions', [IpRestrictionController::class, 'store'])->name('ip-restrictions.store'); + Route::put('ip-restrictions/{ipRestriction}', [IpRestrictionController::class, 'update'])->name('ip-restrictions.update'); + Route::delete('ip-restrictions/{ipRestriction}', [IpRestrictionController::class, 'destroy'])->name('ip-restrictions.destroy'); + }); + + // Zekto Settings routes + Route::middleware('permission:manage-biomatric-attedance-settings')->group(function () { + Route::post('settings/zekto/update', [ZektoSettingsController::class, 'update'])->name('settings.zekto.update'); + Route::post('settings/zekto/generate-token', [ZektoSettingsController::class, 'generateToken'])->name('settings.zekto.generate-token'); + }); + + // NOC Settings routes + Route::middleware('permission:update-noc')->group(function () { + Route::post('settings/noc/update', [NocTemplateController::class, 'update'])->name('settings.noc.update'); + }); + + // Joining Letter Settings routes + Route::middleware('permission:update-joining-letter')->group(function () { + Route::post('settings/joining-letter/update', [JoiningLetterTemplateController::class, 'update'])->name('settings.joining-letter.update'); + }); + + // Experience Certificate Settings routes + Route::middleware('permission:update-experience-certificate')->group(function () { + Route::post('settings/experience-certificate/update', [ExperienceCertificateTemplateController::class, 'update'])->name('settings.experience-certificate.update'); + }); +}); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 000000000..4bae04af6 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,1318 @@ +name('home'); + +// Public form submission routes + +// Cashfree webhook (public route) +Route::post('cashfree/webhook', [CashfreeController::class, 'webhook'])->name('cashfree.webhook'); + +// Benefit webhook (public route) +Route::post('benefit/webhook', [BenefitPaymentController::class, 'webhook'])->name('benefit.webhook'); +Route::get('payments/benefit/success', [BenefitPaymentController::class, 'success'])->name('benefit.success'); +Route::post('payments/benefit/callback', [BenefitPaymentController::class, 'callback'])->name('benefit.callback'); + +// FedaPay callback (public route) +Route::match(['GET', 'POST'], 'payments/fedapay/callback', [FedaPayPaymentController::class, 'callback'])->name('fedapay.callback'); + +// YooKassa success/callback (public routes) +Route::get('payments/yookassa/success', [YooKassaPaymentController::class, 'success'])->name('yookassa.success'); +Route::post('payments/yookassa/callback', [YooKassaPaymentController::class, 'callback'])->name('yookassa.callback'); + +// Nepalste success/callback (public routes) +Route::get('payments/nepalste/success', [NepalstePaymentController::class, 'success'])->name('nepalste.success'); +Route::post('payments/nepalste/callback', [NepalstePaymentController::class, 'callback'])->name('nepalste.callback'); + +// PayTR callback (public route) +Route::post('payments/paytr/callback', [PayTRPaymentController::class, 'callback'])->name('paytr.callback'); + +// PayTabs callback (public route) +Route::match(['GET', 'POST'], 'payments/paytabs/callback', [PayTabsPaymentController::class, 'callback'])->name('paytabs.callback'); +Route::get('payments/paytabs/success', [PayTabsPaymentController::class, 'success'])->name('paytabs.success'); + +// Tap payment routes (public routes) +Route::get('payments/tap/success', [TapPaymentController::class, 'success'])->name('tap.success'); +Route::post('payments/tap/callback', [TapPaymentController::class, 'callback'])->name('tap.callback'); + +// Aamarpay payment routes (public routes) +Route::match(['GET', 'POST'], 'payments/aamarpay/success', [AamarpayPaymentController::class, 'success'])->name('aamarpay.success'); +Route::post('payments/aamarpay/callback', [AamarpayPaymentController::class, 'callback'])->name('aamarpay.callback'); + +// PaymentWall callback (public route) +Route::match(['GET', 'POST'], 'payments/paymentwall/callback', [PaymentWallPaymentController::class, 'callback'])->name('paymentwall.callback'); +Route::get('payments/paymentwall/success', [PaymentWallPaymentController::class, 'success'])->name('paymentwall.success'); + +// PayFast payment routes (public routes) +Route::get('payments/payfast/success', [PayfastPaymentController::class, 'success'])->name('payfast.success'); +Route::post('payments/payfast/callback', [PayfastPaymentController::class, 'callback'])->name('payfast.callback'); + +// CoinGate callback (public route) +Route::match(['GET', 'POST'], 'payments/coingate/callback', [CoinGatePaymentController::class, 'callback'])->name('coingate.callback'); + +// Xendit payment routes (public routes) +Route::get('payments/xendit/success', [XenditPaymentController::class, 'success'])->name('xendit.success'); +Route::post('payments/xendit/callback', [XenditPaymentController::class, 'callback'])->name('xendit.callback'); + +// PWA Manifest routes removed + +Route::get('/landing-page', [LandingPageController::class, 'settings'])->name('landing-page')->middleware('auth'); +Route::post('/landing-page/contact', [LandingPageController::class, 'submitContact'])->name('landing-page.contact'); +Route::post('/landing-page/subscribe', [LandingPageController::class, 'subscribe'])->name('landing-page.subscribe'); +Route::get('/page/{slug}', [CustomPageController::class, 'show'])->name('custom-page.show')->middleware('guest'); + +Route::get('/translations/{locale}', [TranslationController::class, 'getTranslations'])->name('translations'); +Route::get('/refresh-language/{locale}', [TranslationController::class, 'refreshLanguage'])->name('refresh-language'); +Route::get('/initial-locale', [TranslationController::class, 'getInitialLocale'])->name('initial-locale'); +Route::post('/change-language', [TranslationController::class, 'changeLanguage'])->name('change-language'); + +// Career Pages (Public Routes +// Career Pages (Public Routes with Multi-tenant support) +Route::middleware(['career.shared'])->prefix('{userSlug?}/career')->name('career.')->group(function () { + Route::get('/', [CareerController::class, 'index'])->name('index'); + Route::get('/jobs/{jobCode}', [CareerController::class, 'show'])->name('job-details'); + Route::get('/apply/{jobCode}', [CareerController::class, 'showApplicationForm'])->name('apply'); + Route::post('/apply/{jobCode}', [CareerController::class, 'submitApplication'])->name('job.submit'); +}); + +// Email Templates routes (no middleware for testing) +Route::get('email-templates', [EmailTemplateController::class, 'index'])->name('email-templates.index'); +Route::get('email-templates/{emailTemplate}', [EmailTemplateController::class, 'show'])->name('email-templates.show'); +Route::put('email-templates/{emailTemplate}/settings', [EmailTemplateController::class, 'updateSettings'])->name('email-templates.update-settings'); +Route::put('email-templates/{emailTemplate}/content', [EmailTemplateController::class, 'updateContent'])->name('email-templates.update-content'); + +Route::middleware(['auth', 'verified', 'setting'])->group(function () { + + Route::middleware('checksaas')->group(function () { + // Plans routes - accessible without plan check + Route::get('plans', [PlanController::class, 'index'])->name('plans.index'); + Route::post('plans/request', [PlanController::class, 'requestPlan'])->name('plans.request'); + Route::post('plans/trial', [PlanController::class, 'startTrial'])->name('plans.trial'); + Route::post('plans/subscribe', [PlanController::class, 'subscribe'])->name('plans.subscribe'); + Route::post('plans/coupons/validate', [CouponController::class, 'validate'])->name('coupons.validate'); + + // Payment routes - accessible without plan check + Route::post('payments/stripe', [StripePaymentController::class, 'processPayment'])->name('stripe.payment'); + Route::post('payments/paypal', [PayPalPaymentController::class, 'processPayment'])->name('paypal.payment'); + Route::post('payments/bank', [BankPaymentController::class, 'processPayment'])->name('bank.payment'); + Route::post('payments/paystack', [PaystackPaymentController::class, 'processPayment'])->name('paystack.payment'); + Route::post('payments/flutterwave', [FlutterwavePaymentController::class, 'processPayment'])->name('flutterwave.payment'); + Route::post('payments/paytabs', [PayTabsPaymentController::class, 'processPayment'])->name('paytabs.payment'); + Route::post('payments/skrill', [SkrillPaymentController::class, 'processPayment'])->name('skrill.payment'); + Route::post('payments/coingate', [CoinGatePaymentController::class, 'processPayment'])->name('coingate.payment'); + Route::post('payments/payfast', [PayfastPaymentController::class, 'processPayment'])->name('payfast.payment'); + Route::post('payments/mollie', [MolliePaymentController::class, 'processPayment'])->name('mollie.payment'); + Route::post('payments/toyyibpay', [ToyyibPayPaymentController::class, 'processPayment'])->name('toyyibpay.payment'); + Route::post('payments/iyzipay', [IyzipayPaymentController::class, 'processPayment'])->name('iyzipay.payment'); + Route::post('payments/benefit', [BenefitPaymentController::class, 'processPayment'])->name('benefit.payment'); + Route::post('payments/ozow', [OzowPaymentController::class, 'processPayment'])->name('ozow.payment'); + Route::post('payments/easebuzz', [EasebuzzPaymentController::class, 'processPayment'])->name('easebuzz.payment'); + Route::post('payments/khalti', [KhaltiPaymentController::class, 'processPayment'])->name('khalti.payment'); + Route::post('payments/authorizenet', [AuthorizeNetPaymentController::class, 'processPayment'])->name('authorizenet.payment'); + Route::post('payments/fedapay', [FedaPayPaymentController::class, 'processPayment'])->name('fedapay.payment'); + Route::post('payments/payhere', [PayHerePaymentController::class, 'processPayment'])->name('payhere.payment'); + Route::post('payments/cinetpay', [CinetPayPaymentController::class, 'processPayment'])->name('cinetpay.payment'); + Route::post('payments/paiement', [PaiementPaymentController::class, 'processPayment'])->name('paiement.payment'); + Route::post('payments/nepalste', [NepalstePaymentController::class, 'processPayment'])->name('nepalste.payment'); + Route::post('payments/yookassa', [YooKassaPaymentController::class, 'processPayment'])->name('yookassa.payment'); + Route::post('payments/aamarpay', [AamarpayPaymentController::class, 'processPayment'])->name('aamarpay.payment'); + Route::post('payments/midtrans', [MidtransPaymentController::class, 'processPayment'])->name('midtrans.payment'); + Route::post('payments/paymentwall', [PaymentWallPaymentController::class, 'processPayment'])->name('paymentwall.payment'); + Route::post('payments/sspay', [SSPayPaymentController::class, 'processPayment'])->name('sspay.payment'); + + // Payment gateway specific routes + Route::post('razorpay/create-order', [RazorpayController::class, 'createOrder'])->name('razorpay.create-order'); + Route::post('razorpay/verify-payment', [RazorpayController::class, 'verifyPayment'])->name('razorpay.verify-payment'); + Route::post('cashfree/create-session', [CashfreeController::class, 'createPaymentSession'])->name('cashfree.create-session'); + Route::post('cashfree/verify-payment', [CashfreeController::class, 'verifyPayment'])->name('cashfree.verify-payment'); + Route::post('mercadopago/create-preference', [MercadoPagoController::class, 'createPreference'])->name('mercadopago.create-preference'); + Route::post('mercadopago/process-payment', [MercadoPagoController::class, 'processPayment'])->name('mercadopago.process-payment'); + + // Other payment creation routes + Route::post('tap/create-payment', [TapPaymentController::class, 'createPayment'])->name('tap.create-payment'); + Route::post('xendit/create-payment', [XenditPaymentController::class, 'createPayment'])->name('xendit.create-payment'); + Route::post('payments/paytr/create-token', [PayTRPaymentController::class, 'createPaymentToken'])->name('paytr.create-token'); + Route::post('iyzipay/create-form', [IyzipayPaymentController::class, 'createPaymentForm'])->name('iyzipay.create-form'); + Route::post('benefit/create-session', [BenefitPaymentController::class, 'createPaymentSession'])->name('benefit.create-session'); + Route::post('ozow/create-payment', [OzowPaymentController::class, 'createPayment'])->name('ozow.create-payment'); + Route::post('easebuzz/create-payment', [EasebuzzPaymentController::class, 'createPayment'])->name('easebuzz.create-payment'); + Route::post('khalti/create-payment', [KhaltiPaymentController::class, 'createPayment'])->name('khalti.create-payment'); + Route::post('authorizenet/create-form', [AuthorizeNetPaymentController::class, 'createPaymentForm'])->name('authorizenet.create-form'); + Route::post('fedapay/create-payment', [FedaPayPaymentController::class, 'createPayment'])->name('fedapay.create-payment'); + Route::post('payhere/create-payment', [PayHerePaymentController::class, 'createPayment'])->name('payhere.create-payment'); + Route::post('cinetpay/create-payment', [CinetPayPaymentController::class, 'createPayment'])->name('cinetpay.create-payment'); + Route::post('paiement/create-payment', [PaiementPaymentController::class, 'createPayment'])->name('paiement.create-payment'); + Route::post('nepalste/create-payment', [NepalstePaymentController::class, 'createPayment'])->name('nepalste.create-payment'); + Route::post('yookassa/create-payment', [YooKassaPaymentController::class, 'createPayment'])->name('yookassa.create-payment'); + Route::post('aamarpay/create-payment', [AamarpayPaymentController::class, 'createPayment'])->name('aamarpay.create-payment'); + Route::post('midtrans/create-payment', [MidtransPaymentController::class, 'createPayment'])->name('midtrans.create-payment'); + Route::post('paymentwall/create-payment', [PaymentWallPaymentController::class, 'createPayment'])->name('paymentwall.create-payment'); + Route::post('sspay/create-payment', [SSPayPaymentController::class, 'createPayment'])->name('sspay.create-payment'); + + // Payment success/callback routes + Route::post('payments/skrill/callback', [SkrillPaymentController::class, 'callback'])->name('skrill.callback'); + Route::get('payments/paytr/success', [PayTRPaymentController::class, 'success'])->name('paytr.success'); + Route::get('payments/paytr/failure', [PayTRPaymentController::class, 'failure'])->name('paytr.failure'); + Route::get('payments/mollie/success', [MolliePaymentController::class, 'success'])->name('mollie.success'); + Route::post('payments/mollie/callback', [MolliePaymentController::class, 'callback'])->name('mollie.callback'); + Route::match(['GET', 'POST'], 'payments/toyyibpay/success', [ToyyibPayPaymentController::class, 'success'])->name('toyyibpay.success'); + Route::post('payments/toyyibpay/callback', [ToyyibPayPaymentController::class, 'callback'])->name('toyyibpay.callback'); + Route::post('payments/iyzipay/callback', [IyzipayPaymentController::class, 'callback'])->name('iyzipay.callback'); + Route::get('payments/ozow/success', [OzowPaymentController::class, 'success'])->name('ozow.success'); + Route::post('payments/ozow/callback', [OzowPaymentController::class, 'callback'])->name('ozow.callback'); + Route::get('payments/payhere/success', [PayHerePaymentController::class, 'success'])->name('payhere.success'); + Route::post('payments/payhere/callback', [PayHerePaymentController::class, 'callback'])->name('payhere.callback'); + Route::get('payments/cinetpay/success', [CinetPayPaymentController::class, 'success'])->name('cinetpay.success'); + Route::post('payments/cinetpay/callback', [CinetPayPaymentController::class, 'callback'])->name('cinetpay.callback'); + Route::get('payments/paiement/success', [PaiementPaymentController::class, 'success'])->name('paiement.success'); + Route::post('payments/paiement/callback', [PaiementPaymentController::class, 'callback'])->name('paiement.callback'); + Route::post('payments/midtrans/callback', [MidtransPaymentController::class, 'callback'])->name('midtrans.callback'); + Route::post('paymentwall/process', [PaymentWallPaymentController::class, 'processPayment'])->name('paymentwall.process'); + Route::get('payments/sspay/success', [SSPayPaymentController::class, 'success'])->name('sspay.success'); + Route::post('payments/sspay/callback', [SSPayPaymentController::class, 'callback'])->name('sspay.callback'); + Route::get('mercadopago/success', [MercadoPagoController::class, 'success'])->name('mercadopago.success'); + Route::get('mercadopago/failure', [MercadoPagoController::class, 'failure'])->name('mercadopago.failure'); + Route::get('mercadopago/pending', [MercadoPagoController::class, 'pending'])->name('mercadopago.pending'); + Route::post('mercadopago/webhook', [MercadoPagoController::class, 'webhook'])->name('mercadopago.webhook'); + Route::post('authorizenet/test-connection', [AuthorizeNetPaymentController::class, 'testConnection'])->name('authorizenet.test-connection'); + }); + + // All other routes require plan access check + Route::middleware('plan.access')->group(function () { + Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard'); + Route::get('dashboard/redirect', [DashboardController::class, 'redirectToFirstAvailablePage'])->name('dashboard.redirect'); + + Route::get('media-library', function () { + return Inertia::render('media-library'); + })->name('media-library'); + + // Media Library API routes + Route::get('api/media', [MediaController::class, 'index'])->middleware('permission:manage-media')->name('api.media.index'); + Route::post('api/media/batch', [MediaController::class, 'batchStore'])->middleware('permission:create-media')->name('api.media.batch'); + Route::get('api/media/{id}/download', [MediaController::class, 'download'])->middleware('permission:download-media')->name('api.media.download'); + Route::delete('api/media/{id}', [MediaController::class, 'destroy'])->middleware('permission:delete-media')->name('api.media.destroy'); + Route::post('api/media/directories', [MediaController::class, 'createDirectory'])->name('api.media.directories.create'); + + // Permissions routes with granular permissions + Route::middleware('permission:manage-permissions')->group(function () { + Route::get('permissions', [PermissionController::class, 'index'])->middleware('permission:manage-permissions')->name('permissions.index'); + Route::get('permissions/create', [PermissionController::class, 'create'])->middleware('permission:create-permissions')->name('permissions.create'); + Route::post('permissions', [PermissionController::class, 'store'])->middleware('permission:create-permissions')->name('permissions.store'); + Route::get('permissions/{permission}', [PermissionController::class, 'show'])->middleware('permission:view-permissions')->name('permissions.show'); + Route::get('permissions/{permission}/edit', [PermissionController::class, 'edit'])->middleware('permission:edit-permissions')->name('permissions.edit'); + Route::put('permissions/{permission}', [PermissionController::class, 'update'])->middleware('permission:edit-permissions')->name('permissions.update'); + Route::patch('permissions/{permission}', [PermissionController::class, 'update'])->middleware('permission:edit-permissions'); + Route::delete('permissions/{permission}', [PermissionController::class, 'destroy'])->middleware('permission:delete-permissions')->name('permissions.destroy'); + }); + + // Roles routes with granular permissions + Route::middleware('permission:manage-roles')->group(function () { + Route::get('roles', [RoleController::class, 'index'])->middleware('permission:manage-roles')->name('roles.index'); + Route::get('roles/create', [RoleController::class, 'create'])->middleware('permission:create-roles')->name('roles.create'); + Route::post('roles', [RoleController::class, 'store'])->middleware('permission:create-roles')->name('roles.store'); + Route::get('roles/{role}', [RoleController::class, 'show'])->middleware('permission:view-roles')->name('roles.show'); + Route::get('roles/{role}/edit', [RoleController::class, 'edit'])->middleware('permission:edit-roles')->name('roles.edit'); + Route::put('roles/{role}', [RoleController::class, 'update'])->middleware('permission:edit-roles')->name('roles.update'); + Route::patch('roles/{role}', [RoleController::class, 'update'])->middleware('permission:edit-roles'); + Route::delete('roles/{role}', [RoleController::class, 'destroy'])->middleware('permission:delete-roles')->name('roles.destroy'); + }); + + // Users routes with granular permissions + Route::middleware('permission:manage-users')->group(function () { + Route::get('users', [UserController::class, 'index'])->middleware('permission:manage-users')->name('users.index'); + Route::get('users/create', [UserController::class, 'create'])->middleware('permission:create-users')->name('users.create'); + Route::post('users', [UserController::class, 'store'])->middleware('permission:create-users')->name('users.store'); + Route::get('users/{user}', [UserController::class, 'show'])->middleware('permission:view-users')->name('users.show'); + Route::get('users/{user}/edit', [UserController::class, 'edit'])->middleware('permission:edit-users')->name('users.edit'); + Route::put('users/{user}', [UserController::class, 'update'])->middleware('permission:edit-users')->name('users.update'); + Route::patch('users/{user}', [UserController::class, 'update'])->middleware('permission:edit-users'); + Route::delete('users/{user}', [UserController::class, 'destroy'])->middleware('permission:delete-users')->name('users.destroy'); + + // Additional user routes + Route::put('users/{user}/reset-password', [UserController::class, 'resetPassword'])->middleware('permission:reset-password-users')->name('users.reset-password'); + Route::put('users/{user}/toggle-status', [UserController::class, 'toggleStatus'])->middleware('permission:toggle-status-users')->name('users.toggle-status'); + }); + + // HR Module routes + // Branch routes + Route::middleware('permission:manage-branches')->group(function () { + Route::get('hr/branches', [BranchController::class, 'index'])->name('hr.branches.index'); + Route::post('hr/branches', [BranchController::class, 'store'])->middleware('permission:create-branches')->name('hr.branches.store'); + Route::put('hr/branches/{branch}', [BranchController::class, 'update'])->middleware('permission:edit-branches')->name('hr.branches.update'); + Route::delete('hr/branches/{branch}', [BranchController::class, 'destroy'])->middleware('permission:delete-branches')->name('hr.branches.destroy'); + Route::put('hr/branches/{branch}/toggle-status', [BranchController::class, 'toggleStatus'])->middleware('permission:edit-branches')->name('hr.branches.toggle-status'); + }); + + // Department routes + Route::middleware('permission:manage-departments')->group(function () { + Route::get('hr/departments', [DepartmentController::class, 'index'])->name('hr.departments.index'); + Route::post('hr/departments', [DepartmentController::class, 'store'])->middleware('permission:create-departments')->name('hr.departments.store'); + Route::put('hr/departments/{department}', [DepartmentController::class, 'update'])->middleware('permission:edit-departments')->name('hr.departments.update'); + Route::delete('hr/departments/{department}', [DepartmentController::class, 'destroy'])->middleware('permission:delete-departments')->name('hr.departments.destroy'); + Route::put('hr/departments/{department}/toggle-status', [DepartmentController::class, 'toggleStatus'])->middleware('permission:edit-departments')->name('hr.departments.toggle-status'); + }); + + // Designation routes + Route::middleware('permission:manage-designations')->group(function () { + Route::get('hr/designations', [DesignationController::class, 'index'])->name('hr.designations.index'); + Route::post('hr/designations', [DesignationController::class, 'store'])->middleware('permission:create-designations')->name('hr.designations.store'); + Route::put('hr/designations/{designation}', [DesignationController::class, 'update'])->middleware('permission:edit-designations')->name('hr.designations.update'); + Route::delete('hr/designations/{designation}', [DesignationController::class, 'destroy'])->middleware('permission:delete-designations')->name('hr.designations.destroy'); + Route::put('hr/designations/{designation}/toggle-status', [DesignationController::class, 'toggleStatus'])->middleware('permission:toggle-status-designations')->name('hr.designations.toggle-status'); + }); + + // Documenttype Routes + Route::middleware('permission:manage-document-types')->group(function () { + Route::get('hr/document-types', [DocumentTypeController::class, 'index'])->name('hr.document-types.index'); + Route::post('hr/document-types', [DocumentTypeController::class, 'store'])->middleware('permission:create-document-types')->name('hr.document-types.store'); + Route::put('hr/document-types/{documentType}', [DocumentTypeController::class, 'update'])->middleware('permission:edit-document-types')->name('hr.document-types.update'); + Route::delete('hr/document-types/{documentType}', [DocumentTypeController::class, 'destroy'])->middleware('permission:delete-document-types')->name('hr.document-types.destroy'); + }); + + // Employee Routes + Route::middleware('permission:manage-employees')->group(function () { + Route::get('hr/employees', [EmployeeController::class, 'index'])->name('hr.employees.index'); + Route::get('hr/employees/create', [EmployeeController::class, 'create'])->middleware('permission:create-employees')->name('hr.employees.create'); + Route::post('hr/employees', [EmployeeController::class, 'store'])->middleware('permission:create-employees')->name('hr.employees.store'); + + Route::get('hr/employees/export', [EmployeeController::class, 'export'])->name('hr.employees.export'); + Route::get('hr/employees/download-template', [EmployeeController::class, 'downloadTemplate'])->name('hr.employees.download.template'); + Route::post('hr/employees/parse', [EmployeeController::class, 'parseFile'])->name('hr.employees.parse'); + Route::post('hr/employees/import', [EmployeeController::class, 'fileImport'])->name('hr.employees.import'); + + Route::get('hr/employees/{employee}', [EmployeeController::class, 'show'])->middleware('permission:view-employees')->name('hr.employees.show'); + Route::get('hr/employees/{employee}/edit', [EmployeeController::class, 'edit'])->middleware('permission:edit-employees')->name('hr.employees.edit'); + Route::put('hr/employees/{employee}', [EmployeeController::class, 'update'])->middleware('permission:edit-employees')->name('hr.employees.update'); + Route::delete('hr/employees/{employee}', [EmployeeController::class, 'destroy'])->middleware('permission:delete-employees')->name('hr.employees.destroy'); + Route::put('hr/employees/{employee}/toggle-status', [EmployeeController::class, 'toggleStatus'])->middleware('permission:edit-employees')->name('hr.employees.toggle-status'); + Route::put('hr/employees/{employee}/change-password', [EmployeeController::class, 'changePassword'])->middleware('permission:edit-employees')->name('hr.employees.change-password'); + Route::delete('hr/employees/{userId}/documents/{documentId}', [EmployeeController::class, 'deleteDocument'])->middleware('permission:edit-employees')->name('hr.employees.documents.destroy'); + Route::put('hr/employees/{employee}/documents/{documentId}/approve', [EmployeeController::class, 'approveDocument'])->middleware('permission:edit-employees')->name('hr.employees.documents.approve'); + Route::put('hr/employees/{employee}/documents/{documentId}/reject', [EmployeeController::class, 'rejectDocument'])->middleware('permission:edit-employees')->name('hr.employees.documents.reject'); + Route::get('hr/employees/{userId}/documents/{documentId}/download', [EmployeeController::class, 'downloadDocument'])->middleware('permission:view-employees')->name('hr.employees.documents.download'); + Route::get('hr/employees/{employee}/documents/generate/{format}', [EmployeeController::class, 'downloadJoiningLetter'])->middleware('permission:view-employees')->name('hr.employees.download-joining-letter'); + Route::get('hr/employees/{employee}/experience-certificate/{format}', [EmployeeController::class, 'downloadExperienceCertificate'])->middleware('permission:view-employees')->name('hr.employees.download-experience-certificate'); + Route::get('hr/employees/{employee}/noc-certificate/{format}', [EmployeeController::class, 'downloadNocCertificate'])->middleware('permission:view-employees')->name('hr.employees.download-noc-certificate'); + }); + + // Award Type Routes + Route::middleware('permission:manage-award-types')->group(function () { + Route::get('hr/award-types', [AwardTypeController::class, 'index'])->name('hr.award-types.index'); + Route::post('hr/award-types', [AwardTypeController::class, 'store'])->middleware('permission:create-award-types')->name('hr.award-types.store'); + Route::put('hr/award-types/{awardType}', [AwardTypeController::class, 'update'])->middleware('permission:edit-award-types')->name('hr.award-types.update'); + Route::delete('hr/award-types/{awardType}', [AwardTypeController::class, 'destroy'])->middleware('permission:delete-award-types')->name('hr.award-types.destroy'); + Route::put('hr/award-types/{awardType}/toggle-status', [AwardTypeController::class, 'toggleStatus'])->middleware('permission:edit-award-types')->name('hr.award-types.toggle-status'); + }); + + // Award Routes + Route::middleware('permission:manage-awards')->group(function () { + Route::get('hr/awards', [AwardController::class, 'index'])->name('hr.awards.index'); + Route::get('hr/awards/create', [AwardController::class, 'create'])->middleware('permission:create-awards')->name('hr.awards.create'); + Route::post('hr/awards', [AwardController::class, 'store'])->middleware('permission:create-awards')->name('hr.awards.store'); + Route::get('hr/awards/{award}', [AwardController::class, 'show'])->middleware('permission:view-awards')->name('hr.awards.show'); + Route::get('hr/awards/{award}/edit', [AwardController::class, 'edit'])->middleware('permission:edit-awards')->name('hr.awards.edit'); + Route::put('hr/awards/{award}', [AwardController::class, 'update'])->middleware('permission:edit-awards')->name('hr.awards.update'); + Route::delete('hr/awards/{award}', [AwardController::class, 'destroy'])->middleware('permission:delete-awards')->name('hr.awards.destroy'); + Route::get('hr/awards/{award}/download-certificate', [AwardController::class, 'downloadCertificate'])->middleware('permission:view-awards')->name('hr.awards.download-certificate'); + Route::get('hr/awards/{award}/download-photo', [AwardController::class, 'downloadPhoto'])->middleware('permission:view-awards')->name('hr.awards.download-photo'); + }); + + // Promotion Routes + Route::middleware('permission:manage-promotions')->group(function () { + Route::get('hr/promotions', [PromotionController::class, 'index'])->name('hr.promotions.index'); + Route::post('hr/promotions', [PromotionController::class, 'store'])->middleware('permission:create-promotions')->name('hr.promotions.store'); + Route::put('hr/promotions/{promotion}', [PromotionController::class, 'update'])->middleware('permission:edit-promotions')->name('hr.promotions.update'); + Route::delete('hr/promotions/{promotion}', [PromotionController::class, 'destroy'])->middleware('permission:delete-promotions')->name('hr.promotions.destroy'); + Route::get('hr/promotions/{promotion}/download-document', [PromotionController::class, 'downloadDocument'])->middleware('permission:view-promotions')->name('hr.promotions.download-document'); + Route::put('hr/promotions/{promotion}/update-status', [PromotionController::class, 'updateStatus'])->middleware('permission:edit-promotions')->name('hr.promotions.update-status'); + }); + + // Resignation Routes + Route::middleware('permission:manage-resignations')->group(function () { + Route::get('hr/resignations', [ResignationController::class, 'index'])->name('hr.resignations.index'); + Route::post('hr/resignations', [ResignationController::class, 'store'])->middleware('permission:create-resignations')->name('hr.resignations.store'); + Route::put('hr/resignations/{resignation}', [ResignationController::class, 'update'])->middleware('permission:edit-resignations')->name('hr.resignations.update'); + Route::delete('hr/resignations/{resignation}', [ResignationController::class, 'destroy'])->middleware('permission:delete-resignations')->name('hr.resignations.destroy'); + Route::get('hr/resignations/{resignation}/download-document', [ResignationController::class, 'downloadDocument'])->middleware('permission:view-resignations')->name('hr.resignations.download-document'); + Route::put('hr/resignations/{resignation}/change-status', [ResignationController::class, 'changeStatus'])->middleware('permission:edit-resignations')->name('hr.resignations.change-status'); + }); + + // Termination Routes + Route::middleware('permission:manage-terminations')->group(function () { + Route::get('hr/terminations', [TerminationController::class, 'index'])->name('hr.terminations.index'); + Route::post('hr/terminations', [TerminationController::class, 'store'])->middleware('permission:create-terminations')->name('hr.terminations.store'); + Route::put('hr/terminations/{termination}', [TerminationController::class, 'update'])->middleware('permission:edit-terminations')->name('hr.terminations.update'); + Route::delete('hr/terminations/{termination}', [TerminationController::class, 'destroy'])->middleware('permission:delete-terminations')->name('hr.terminations.destroy'); + Route::get('hr/terminations/{termination}/download-document', [TerminationController::class, 'downloadDocument'])->middleware('permission:view-terminations')->name('hr.terminations.download-document'); + Route::put('hr/terminations/{termination}/change-status', [TerminationController::class, 'changeStatus'])->middleware('permission:edit-terminations')->name('hr.terminations.change-status'); + }); + + // Warning Routes + Route::middleware('permission:manage-warnings')->group(function () { + Route::get('hr/warnings', [WarningController::class, 'index'])->name('hr.warnings.index'); + Route::post('hr/warnings', [WarningController::class, 'store'])->middleware('permission:create-warnings')->name('hr.warnings.store'); + Route::put('hr/warnings/{warning}', [WarningController::class, 'update'])->middleware('permission:edit-warnings')->name('hr.warnings.update'); + Route::delete('hr/warnings/{warning}', [WarningController::class, 'destroy'])->middleware('permission:delete-warnings')->name('hr.warnings.destroy'); + Route::get('hr/warnings/{warning}/download-document', [WarningController::class, 'downloadDocument'])->middleware('permission:view-warnings')->name('hr.warnings.download-document'); + Route::put('hr/warnings/{warning}/change-status', [WarningController::class, 'changeStatus'])->middleware('permission:edit-warnings')->name('hr.warnings.change-status'); + Route::put('hr/warnings/{warning}/update-improvement-plan', [WarningController::class, 'updateImprovementPlan'])->middleware('permission:edit-warnings')->name('hr.warnings.update-improvement-plan'); + }); + + // Trip Routes + Route::middleware('permission:manage-trips')->group(function () { + Route::get('hr/trips', [TripController::class, 'index'])->name('hr.trips.index'); + Route::post('hr/trips', [TripController::class, 'store'])->middleware('permission:create-trips')->name('hr.trips.store'); + Route::put('hr/trips/{trip}', [TripController::class, 'update'])->middleware('permission:edit-trips')->name('hr.trips.update'); + Route::delete('hr/trips/{trip}', [TripController::class, 'destroy'])->middleware('permission:delete-trips')->name('hr.trips.destroy'); + Route::get('hr/trips/{trip}/download-document', [TripController::class, 'downloadDocument'])->middleware('permission:view-trips')->name('hr.trips.download-document'); + Route::put('hr/trips/{trip}/change-status', [TripController::class, 'changeStatus'])->middleware('permission:edit-trips')->name('hr.trips.change-status'); + Route::put('hr/trips/{trip}/update-advance-status', [TripController::class, 'updateAdvanceStatus'])->middleware('permission:edit-trips')->name('hr.trips.update-advance-status'); + Route::put('hr/trips/{trip}/update-reimbursement-status', [TripController::class, 'updateReimbursementStatus'])->middleware('permission:edit-trips')->name('hr.trips.update-reimbursement-status'); + + // Trip Expenses Routes + Route::get('hr/trips/{trip}/expenses', [TripController::class, 'showExpenses'])->middleware('permission:manage-trip-expenses')->name('hr.trips.expenses'); + Route::post('hr/trips/{trip}/expenses', [TripController::class, 'storeExpense'])->middleware('permission:manage-trip-expenses')->name('hr.trips.expenses.store'); + Route::put('hr/trips/{trip}/expenses/{expense}', [TripController::class, 'updateExpense'])->middleware('permission:manage-trip-expenses')->name('hr.trips.expenses.update'); + Route::delete('hr/trips/{trip}/expenses/{expense}', [TripController::class, 'destroyExpense'])->middleware('permission:manage-trip-expenses')->name('hr.trips.expenses.destroy'); + Route::get('hr/trips/{trip}/expenses/{expense}/download-receipt', [TripController::class, 'downloadReceipt'])->middleware('permission:manage-trip-expenses')->name('hr.trips.expenses.download-receipt'); + }); + + // Complaint Routes + Route::middleware('permission:manage-complaints')->group(function () { + Route::get('hr/complaints', [ComplaintController::class, 'index'])->name('hr.complaints.index'); + Route::post('hr/complaints', [ComplaintController::class, 'store'])->middleware('permission:create-complaints')->name('hr.complaints.store'); + Route::put('hr/complaints/{complaint}', [ComplaintController::class, 'update'])->middleware('permission:edit-complaints')->name('hr.complaints.update'); + Route::delete('hr/complaints/{complaint}', [ComplaintController::class, 'destroy'])->middleware('permission:delete-complaints')->name('hr.complaints.destroy'); + Route::get('hr/complaints/{complaint}/download-document', [ComplaintController::class, 'downloadDocument'])->middleware('permission:view-complaints')->name('hr.complaints.download-document'); + Route::put('hr/complaints/{complaint}/change-status', [ComplaintController::class, 'changeStatus'])->middleware('permission:edit-complaints')->name('hr.complaints.change-status'); + Route::put('hr/complaints/{complaint}/assign', [ComplaintController::class, 'assignComplaint'])->middleware('permission:assign-complaints')->name('hr.complaints.assign'); + Route::put('hr/complaints/{complaint}/resolve', [ComplaintController::class, 'resolveComplaint'])->middleware('permission:resolve-complaints')->name('hr.complaints.resolve'); + Route::put('hr/complaints/{complaint}/follow-up', [ComplaintController::class, 'updateFollowUp'])->middleware('permission:resolve-complaints')->name('hr.complaints.follow-up'); + }); + + // Employee Transfer Routes + Route::middleware('permission:manage-employee-transfers')->group(function () { + Route::get('hr/transfers', [EmployeeTransferController::class, 'index'])->name('hr.transfers.index'); + Route::post('hr/transfers', [EmployeeTransferController::class, 'store'])->middleware('permission:create-employee-transfers')->name('hr.transfers.store'); + Route::put('hr/transfers/{transfer}', [EmployeeTransferController::class, 'update'])->middleware('permission:edit-employee-transfers')->name('hr.transfers.update'); + Route::delete('hr/transfers/{transfer}', [EmployeeTransferController::class, 'destroy'])->middleware('permission:delete-employee-transfers')->name('hr.transfers.destroy'); + Route::get('hr/transfers/{transfer}/download-document', [EmployeeTransferController::class, 'downloadDocument'])->middleware('permission:view-employee-transfers')->name('hr.transfers.download-document'); + Route::put('hr/transfers/{transfer}/approve', [EmployeeTransferController::class, 'approve'])->middleware('permission:approve-employee-transfers')->name('hr.transfers.approve'); + Route::put('hr/transfers/{transfer}/reject', [EmployeeTransferController::class, 'reject'])->middleware('permission:reject-employee-transfers')->name('hr.transfers.reject'); + Route::get('hr/transfers/get-department/{branchId}', [EmployeeTransferController::class, 'getDepartment'])->name('hr.transfers.getdepartment'); + Route::get('hr/transfers/get-designation/{departmentId}', [EmployeeTransferController::class, 'getDesignation'])->name('hr.transfers.getdesignation'); + }); + + // Holiday Routes + Route::middleware('permission:manage-holidays')->group(function () { + Route::get('hr/holidays', [HolidayController::class, 'index'])->name('hr.holidays.index'); + Route::get('hr/holidays/calendar', [HolidayController::class, 'calendar'])->name('hr.holidays.calendar'); + Route::post('hr/holidays', [HolidayController::class, 'store'])->middleware('permission:create-holidays')->name('hr.holidays.store'); + Route::put('hr/holidays/{holiday}', [HolidayController::class, 'update'])->middleware('permission:edit-holidays')->name('hr.holidays.update'); + Route::delete('hr/holidays/{holiday}', [HolidayController::class, 'destroy'])->middleware('permission:delete-holidays')->name('hr.holidays.destroy'); + Route::get('hr/holidays/export/pdf', [HolidayController::class, 'exportPdf'])->name('hr.holidays.export.pdf'); + Route::get('hr/holidays/export/ical', [HolidayController::class, 'exportIcal'])->name('hr.holidays.export.ical'); + }); + + // Announcement Routes + Route::middleware('permission:manage-announcements')->group(function () { + Route::get('hr/announcements', [AnnouncementController::class, 'index'])->name('hr.announcements.index'); + Route::get('hr/announcements/dashboard', [AnnouncementController::class, 'dashboard'])->name('hr.announcements.dashboard'); + Route::get('hr/announcements/{announcement}', [AnnouncementController::class, 'show'])->name('hr.announcements.show'); + Route::post('hr/announcements', [AnnouncementController::class, 'store'])->middleware('permission:create-announcements')->name('hr.announcements.store'); + Route::put('hr/announcements/{announcement}', [AnnouncementController::class, 'update'])->middleware('permission:edit-announcements')->name('hr.announcements.update'); + Route::delete('hr/announcements/{announcement}', [AnnouncementController::class, 'destroy'])->middleware('permission:delete-announcements')->name('hr.announcements.destroy'); + Route::get('hr/announcements/{announcement}/download-attachment', [AnnouncementController::class, 'downloadAttachment'])->name('hr.announcements.download-attachment'); + Route::get('hr/announcements/{announcement}/statistics', [AnnouncementController::class, 'viewStatistics'])->name('hr.announcements.statistics'); + Route::post('hr/announcements/{announcement}/mark-as-read', [AnnouncementController::class, 'markAsRead'])->name('hr.announcements.mark-as-read'); + Route::get('hr/announcements/get-departments/{branchIds}', [AnnouncementController::class, 'getDepartments'])->name('hr.announcements.get-departments'); + }); + + // Asset Type Routes + Route::middleware('permission:manage-asset-types')->group(function () { + Route::get('hr/asset-types', [AssetTypeController::class, 'index'])->name('hr.asset-types.index'); + Route::post('hr/asset-types', [AssetTypeController::class, 'store'])->middleware('permission:create-asset-types')->name('hr.asset-types.store'); + Route::put('hr/asset-types/{assetType}', [AssetTypeController::class, 'update'])->middleware('permission:edit-asset-types')->name('hr.asset-types.update'); + Route::delete('hr/asset-types/{assetType}', [AssetTypeController::class, 'destroy'])->middleware('permission:delete-asset-types')->name('hr.asset-types.destroy'); + }); + + // Asset Routes + Route::middleware('permission:manage-assets')->group(function () { + Route::get('hr/assets', [AssetController::class, 'index'])->name('hr.assets.index'); + Route::get('hr/assets-dashboard', [AssetController::class, 'dashboard'])->name('hr.assets.dashboard'); + Route::get('hr/assets-depreciation-report', [AssetController::class, 'depreciationReport'])->name('hr.assets.depreciation-report'); + Route::get('hr/assets/export-depreciation-csv', [AssetController::class, 'exportDepreciationCsv'])->name('hr.assets.export-depreciation-csv'); + + Route::get('hr/assets/download-template', [AssetController::class, 'downloadTemplate'])->name('hr.assets.download.template'); + Route::get('hr/assets/export', [AssetController::class, 'export'])->name('hr.assets.export'); + Route::post('hr/assets/parse', [AssetController::class, 'parseFile'])->name('hr.assets.parse'); + Route::post('hr/assets/import', [AssetController::class, 'fileImport'])->name('hr.assets.import'); + + Route::get('hr/assets/{asset}', [AssetController::class, 'show'])->name('hr.assets.show'); + Route::post('hr/assets', [AssetController::class, 'store'])->middleware('permission:create-assets')->name('hr.assets.store'); + Route::put('hr/assets/{asset}', [AssetController::class, 'update'])->middleware('permission:edit-assets')->name('hr.assets.update'); + Route::delete('hr/assets/{asset}', [AssetController::class, 'destroy'])->middleware('permission:delete-assets')->name('hr.assets.destroy'); + Route::post('hr/assets/{asset}/assign', [AssetController::class, 'assign'])->middleware('permission:assign-assets')->name('hr.assets.assign'); + Route::post('hr/assets/{asset}/return', [AssetController::class, 'returnAsset'])->middleware('permission:assign-assets')->name('hr.assets.return'); + Route::post('hr/assets/{asset}/schedule-maintenance', [AssetController::class, 'scheduleMaintenance'])->middleware('permission:manage-asset-maintenance')->name('hr.assets.schedule-maintenance'); + Route::put('hr/assets/maintenance/{maintenance}', [AssetController::class, 'updateMaintenance'])->middleware('permission:manage-asset-maintenance')->name('hr.assets.update-maintenance'); + Route::get('hr/assets/{asset}/download-document', [AssetController::class, 'downloadDocument'])->name('hr.assets.download-document'); + Route::get('hr/assets/{asset}/view-image', [AssetController::class, 'viewImage'])->name('hr.assets.view-image'); + }); + + // Training Type Routes + Route::middleware('permission:manage-training-types')->group(function () { + Route::get('hr/training-types', [TrainingTypeController::class, 'index'])->name('hr.training-types.index'); + Route::post('hr/training-types', [TrainingTypeController::class, 'store'])->middleware('permission:create-training-types')->name('hr.training-types.store'); + Route::put('hr/training-types/{trainingType}', [TrainingTypeController::class, 'update'])->middleware('permission:edit-training-types')->name('hr.training-types.update'); + Route::put('hr/training-types/{trainingType}/assign-departments', [TrainingTypeController::class, 'assignDepartments'])->middleware('permission:edit-training-types')->name('hr.training-types.assign-departments'); + Route::delete('hr/training-types/{trainingType}', [TrainingTypeController::class, 'destroy'])->middleware('permission:delete-training-types')->name('hr.training-types.destroy'); + }); + + // Training Program Routes + Route::middleware('permission:manage-training-programs')->group(function () { + Route::get('hr/training-programs', [TrainingProgramController::class, 'index'])->name('hr.training-programs.index'); + Route::get('hr/training-programs/{trainingProgram}', [TrainingProgramController::class, 'show'])->name('hr.training-programs.show'); + Route::post('hr/training-programs', [TrainingProgramController::class, 'store'])->middleware('permission:create-training-programs')->name('hr.training-programs.store'); + Route::put('hr/training-programs/{trainingProgram}', [TrainingProgramController::class, 'update'])->middleware('permission:edit-training-programs')->name('hr.training-programs.update'); + Route::delete('hr/training-programs/{trainingProgram}', [TrainingProgramController::class, 'destroy'])->middleware('permission:delete-training-programs')->name('hr.training-programs.destroy'); + Route::get('hr/training-programs/{trainingProgram}/download-materials', [TrainingProgramController::class, 'downloadMaterials'])->name('hr.training-programs.download-materials'); + }); + + // Training Session Routes + Route::middleware('permission:manage-training-sessions')->group(function () { + Route::get('hr/training-sessions', [TrainingSessionController::class, 'index'])->name('hr.training-sessions.index'); + Route::get('hr/training-sessions/calendar', [TrainingSessionController::class, 'calendar'])->name('hr.training-sessions.calendar'); + Route::get('hr/training-sessions/{trainingSession}', [TrainingSessionController::class, 'show'])->name('hr.training-sessions.show'); + Route::post('hr/training-sessions', [TrainingSessionController::class, 'store'])->middleware('permission:create-training-sessions')->name('hr.training-sessions.store'); + Route::put('hr/training-sessions/{trainingSession}', [TrainingSessionController::class, 'update'])->middleware('permission:edit-training-sessions')->name('hr.training-sessions.update'); + Route::delete('hr/training-sessions/{trainingSession}', [TrainingSessionController::class, 'destroy'])->middleware('permission:delete-training-sessions')->name('hr.training-sessions.destroy'); + Route::post('hr/training-sessions/{trainingSession}/update-attendance', [TrainingSessionController::class, 'updateAttendance'])->middleware('permission:manage-attendance')->name('hr.training-sessions.update-attendance'); + }); + + // Employee Training Routes + Route::middleware('permission:manage-employee-trainings')->group(function () { + Route::get('hr/employee-trainings', [EmployeeTrainingController::class, 'index'])->name('hr.employee-trainings.index'); + Route::get('hr/employee-trainings/dashboard', [EmployeeTrainingController::class, 'dashboard'])->name('hr.employee-trainings.dashboard'); + Route::get('hr/employee-trainings/{employeeTraining}', [EmployeeTrainingController::class, 'show'])->middleware('permission:view-employee-trainings')->name('hr.employee-trainings.show'); + Route::post('hr/employee-trainings', [EmployeeTrainingController::class, 'store'])->middleware('permission:create-employee-trainings')->name('hr.employee-trainings.store'); + Route::put('hr/employee-trainings/{employeeTraining}', [EmployeeTrainingController::class, 'update'])->middleware('permission:edit-employee-trainings')->name('hr.employee-trainings.update'); + Route::delete('hr/employee-trainings/{employeeTraining}', [EmployeeTrainingController::class, 'destroy'])->middleware('permission:delete-employee-trainings')->name('hr.employee-trainings.destroy'); + Route::get('hr/employee-trainings/{employeeTraining}/download-certification', [EmployeeTrainingController::class, 'downloadCertification'])->middleware('permission:view-employee-trainings')->name('hr.employee-trainings.download-certification'); + Route::post('hr/employee-trainings/bulk-assign', [EmployeeTrainingController::class, 'bulkAssign'])->middleware('permission:create-employee-trainings')->name('hr.employee-trainings.bulk-assign'); + Route::post('hr/employee-trainings/{employeeTraining}/record-assessment', [EmployeeTrainingController::class, 'recordAssessment'])->middleware('permission:record-assessment-results')->name('hr.employee-trainings.record-assessment'); + }); + + // Training Assessment Routes + Route::middleware('permission:manage-assessments')->group(function () { + Route::get('hr/training-assessments', [TrainingAssessmentController::class, 'index'])->name('hr.training-assessments.index'); + Route::get('hr/training-assessments/{trainingAssessment}', [TrainingAssessmentController::class, 'show'])->name('hr.training-assessments.show'); + Route::post('hr/training-assessments', [TrainingAssessmentController::class, 'store'])->name('hr.training-assessments.store'); + Route::put('hr/training-assessments/{trainingAssessment}', [TrainingAssessmentController::class, 'update'])->name('hr.training-assessments.update'); + Route::delete('hr/training-assessments/{trainingAssessment}', [TrainingAssessmentController::class, 'destroy'])->name('hr.training-assessments.destroy'); + }); + + // Performance Module Routes + + // Performance Indicator Categories + Route::middleware('permission:manage-performance-indicator-categories')->group(function () { + Route::get('hr/performance/indicator-categories', [PerformanceIndicatorCategoryController::class, 'index'])->name('hr.performance.indicator-categories.index'); + Route::post('hr/performance/indicator-categories', [PerformanceIndicatorCategoryController::class, 'store'])->middleware('permission:create-performance-indicator-categories')->name('hr.performance.indicator-categories.store'); + Route::put('hr/performance/indicator-categories/{indicatorCategory}', [PerformanceIndicatorCategoryController::class, 'update'])->middleware('permission:edit-performance-indicator-categories')->name('hr.performance.indicator-categories.update'); + Route::delete('hr/performance/indicator-categories/{indicatorCategory}', [PerformanceIndicatorCategoryController::class, 'destroy'])->middleware('permission:delete-performance-indicator-categories')->name('hr.performance.indicator-categories.destroy'); + Route::put('hr/performance/indicator-categories/{indicatorCategory}/toggle-status', [PerformanceIndicatorCategoryController::class, 'toggleStatus'])->middleware('permission:edit-performance-indicator-categories')->name('hr.performance.indicator-categories.toggle-status'); + }); + + // Performance Indicators + Route::middleware('permission:manage-performance-indicators')->group(function () { + Route::get('hr/performance/indicators', [PerformanceIndicatorController::class, 'index'])->name('hr.performance.indicators.index'); + Route::post('hr/performance/indicators', [PerformanceIndicatorController::class, 'store'])->middleware('permission:create-performance-indicators')->name('hr.performance.indicators.store'); + Route::put('hr/performance/indicators/{indicator}', [PerformanceIndicatorController::class, 'update'])->middleware('permission:edit-performance-indicators')->name('hr.performance.indicators.update'); + Route::delete('hr/performance/indicators/{indicator}', [PerformanceIndicatorController::class, 'destroy'])->middleware('permission:delete-performance-indicators')->name('hr.performance.indicators.destroy'); + Route::put('hr/performance/indicators/{indicator}/toggle-status', [PerformanceIndicatorController::class, 'toggleStatus'])->middleware('permission:edit-performance-indicators')->name('hr.performance.indicators.toggle-status'); + }); + + // Goal Types + Route::middleware('permission:manage-goal-types')->group(function () { + Route::get('hr/performance/goal-types', [GoalTypeController::class, 'index'])->name('hr.performance.goal-types.index'); + Route::post('hr/performance/goal-types', [GoalTypeController::class, 'store'])->middleware('permission:create-goal-types')->name('hr.performance.goal-types.store'); + Route::put('hr/performance/goal-types/{goalType}', [GoalTypeController::class, 'update'])->middleware('permission:edit-goal-types')->name('hr.performance.goal-types.update'); + Route::delete('hr/performance/goal-types/{goalType}', [GoalTypeController::class, 'destroy'])->middleware('permission:delete-goal-types')->name('hr.performance.goal-types.destroy'); + Route::put('hr/performance/goal-types/{goalType}/toggle-status', [GoalTypeController::class, 'toggleStatus'])->middleware('permission:edit-goal-types')->name('hr.performance.goal-types.toggle-status'); + }); + + // Employee Goals + Route::middleware('permission:manage-employee-goals')->group(function () { + Route::get('hr/performance/employee-goals', [EmployeeGoalController::class, 'index'])->name('hr.performance.employee-goals.index'); + Route::post('hr/performance/employee-goals', [EmployeeGoalController::class, 'store'])->middleware('permission:create-employee-goals')->name('hr.performance.employee-goals.store'); + Route::put('hr/performance/employee-goals/{employeeGoal}', [EmployeeGoalController::class, 'update'])->middleware('permission:edit-employee-goals')->name('hr.performance.employee-goals.update'); + Route::delete('hr/performance/employee-goals/{employeeGoal}', [EmployeeGoalController::class, 'destroy'])->middleware('permission:delete-employee-goals')->name('hr.performance.employee-goals.destroy'); + Route::put('hr/performance/employee-goals/{employeeGoal}/progress', [EmployeeGoalController::class, 'updateProgress'])->middleware('permission:edit-employee-goals')->name('hr.performance.employee-goals.update-progress'); + }); + + // Review Cycles + Route::middleware('permission:manage-review-cycles')->group(function () { + Route::get('hr/performance/review-cycles', [ReviewCycleController::class, 'index'])->name('hr.performance.review-cycles.index'); + Route::post('hr/performance/review-cycles', [ReviewCycleController::class, 'store'])->middleware('permission:create-review-cycles')->name('hr.performance.review-cycles.store'); + Route::put('hr/performance/review-cycles/{reviewCycle}', [ReviewCycleController::class, 'update'])->middleware('permission:edit-review-cycles')->name('hr.performance.review-cycles.update'); + Route::delete('hr/performance/review-cycles/{reviewCycle}', [ReviewCycleController::class, 'destroy'])->middleware('permission:delete-review-cycles')->name('hr.performance.review-cycles.destroy'); + Route::put('hr/performance/review-cycles/{reviewCycle}/toggle-status', [ReviewCycleController::class, 'toggleStatus'])->middleware('permission:edit-review-cycles')->name('hr.performance.review-cycles.toggle-status'); + }); + + // Employee Reviews + Route::middleware('permission:manage-employee-reviews')->group(function () { + Route::get('hr/performance/employee-reviews', [EmployeeReviewController::class, 'index'])->name('hr.performance.employee-reviews.index'); + Route::get('hr/performance/employee-reviews/create', [EmployeeReviewController::class, 'create'])->middleware('permission:create-employee-reviews')->name('hr.performance.employee-reviews.create'); + Route::post('hr/performance/employee-reviews', [EmployeeReviewController::class, 'store'])->middleware('permission:create-employee-reviews')->name('hr.performance.employee-reviews.store'); + Route::get('hr/performance/employee-reviews/{employeeReview}', [EmployeeReviewController::class, 'show'])->middleware('permission:view-employee-reviews')->name('hr.performance.employee-reviews.show'); + Route::get('hr/performance/employee-reviews/{employeeReview}/conduct', [EmployeeReviewController::class, 'conduct'])->middleware('permission:edit-employee-reviews')->name('hr.performance.employee-reviews.conduct'); + Route::post('hr/performance/employee-reviews/{employeeReview}/submit-ratings', [EmployeeReviewController::class, 'submitRatings'])->middleware('permission:edit-employee-reviews')->name('hr.performance.employee-reviews.submit-ratings'); + Route::put('hr/performance/employee-reviews/{employeeReview}', [EmployeeReviewController::class, 'update'])->middleware('permission:edit-employee-reviews')->name('hr.performance.employee-reviews.update'); + Route::delete('hr/performance/employee-reviews/{employeeReview}', [EmployeeReviewController::class, 'destroy'])->middleware('permission:delete-employee-reviews')->name('hr.performance.employee-reviews.destroy'); + Route::put('hr/performance/employee-reviews/{employeeReview}/status', [EmployeeReviewController::class, 'updateStatus'])->middleware('permission:edit-employee-reviews')->name('hr.performance.employee-reviews.update-status'); + }); + + // Recruitment Module Routes + + // Job Categories Routes + Route::middleware('permission:manage-job-categories')->group(function () { + Route::get('hr/recruitment/job-categories', [JobCategoryController::class, 'index'])->name('hr.recruitment.job-categories.index'); + Route::post('hr/recruitment/job-categories', [JobCategoryController::class, 'store'])->middleware('permission:create-job-categories')->name('hr.recruitment.job-categories.store'); + Route::put('hr/recruitment/job-categories/{jobCategory}', [JobCategoryController::class, 'update'])->middleware('permission:edit-job-categories')->name('hr.recruitment.job-categories.update'); + Route::delete('hr/recruitment/job-categories/{jobCategory}', [JobCategoryController::class, 'destroy'])->middleware('permission:delete-job-categories')->name('hr.recruitment.job-categories.destroy'); + Route::put('hr/recruitment/job-categories/{jobCategory}/toggle-status', [JobCategoryController::class, 'toggleStatus'])->middleware('permission:edit-job-categories')->name('hr.recruitment.job-categories.toggle-status'); + }); + + // Job Requisitions Routes + Route::middleware('permission:manage-job-requisitions')->group(function () { + Route::get('hr/recruitment/job-requisitions', [JobRequisitionController::class, 'index'])->name('hr.recruitment.job-requisitions.index'); + Route::post('hr/recruitment/job-requisitions', [JobRequisitionController::class, 'store'])->middleware('permission:create-job-requisitions')->name('hr.recruitment.job-requisitions.store'); + Route::put('hr/recruitment/job-requisitions/{jobRequisition}', [JobRequisitionController::class, 'update'])->middleware('permission:edit-job-requisitions')->name('hr.recruitment.job-requisitions.update'); + Route::delete('hr/recruitment/job-requisitions/{jobRequisition}', [JobRequisitionController::class, 'destroy'])->middleware('permission:delete-job-requisitions')->name('hr.recruitment.job-requisitions.destroy'); + Route::put('hr/recruitment/job-requisitions/{jobRequisition}/status', [JobRequisitionController::class, 'updateStatus'])->middleware('permission:approve-job-requisitions')->name('hr.recruitment.job-requisitions.update-status'); + }); + + // Job Types Routes + Route::middleware('permission:manage-job-types')->group(function () { + Route::get('hr/recruitment/job-types', [JobTypeController::class, 'index'])->name('hr.recruitment.job-types.index'); + Route::post('hr/recruitment/job-types', [JobTypeController::class, 'store'])->middleware('permission:create-job-types')->name('hr.recruitment.job-types.store'); + Route::put('hr/recruitment/job-types/{jobType}', [JobTypeController::class, 'update'])->middleware('permission:edit-job-types')->name('hr.recruitment.job-types.update'); + Route::delete('hr/recruitment/job-types/{jobType}', [JobTypeController::class, 'destroy'])->middleware('permission:delete-job-types')->name('hr.recruitment.job-types.destroy'); + Route::put('hr/recruitment/job-types/{jobType}/toggle-status', [JobTypeController::class, 'toggleStatus'])->middleware('permission:edit-job-types')->name('hr.recruitment.job-types.toggle-status'); + }); + + // Job Locations Routes + Route::middleware('permission:manage-job-locations')->group(function () { + Route::get('hr/recruitment/job-locations', [JobLocationController::class, 'index'])->name('hr.recruitment.job-locations.index'); + Route::post('hr/recruitment/job-locations', [JobLocationController::class, 'store'])->middleware('permission:create-job-locations')->name('hr.recruitment.job-locations.store'); + Route::put('hr/recruitment/job-locations/{jobLocation}', [JobLocationController::class, 'update'])->middleware('permission:edit-job-locations')->name('hr.recruitment.job-locations.update'); + Route::delete('hr/recruitment/job-locations/{jobLocation}', [JobLocationController::class, 'destroy'])->middleware('permission:delete-job-locations')->name('hr.recruitment.job-locations.destroy'); + Route::put('hr/recruitment/job-locations/{jobLocation}/toggle-status', [JobLocationController::class, 'toggleStatus'])->middleware('permission:edit-job-locations')->name('hr.recruitment.job-locations.toggle-status'); + }); + + // Job Postings Routes + Route::middleware('permission:manage-job-postings')->group(function () { + Route::get('hr/recruitment/job-postings', [JobPostingController::class, 'index'])->name('hr.recruitment.job-postings.index'); + Route::get('hr/recruitment/job-postings/create', [JobPostingController::class, 'create'])->middleware('permission:create-job-postings')->name('hr.recruitment.job-postings.create'); + Route::post('hr/recruitment/job-postings', [JobPostingController::class, 'store'])->middleware('permission:create-job-postings')->name('hr.recruitment.job-postings.store'); + Route::get('hr/recruitment/job-postings/{jobPosting}', [JobPostingController::class, 'show'])->middleware('permission:view-job-postings')->name('hr.recruitment.job-postings.show'); + Route::get('hr/recruitment/job-postings/{jobPosting}/edit', [JobPostingController::class, 'edit'])->middleware('permission:edit-job-postings')->name('hr.recruitment.job-postings.edit'); + Route::put('hr/recruitment/job-postings/{jobPosting}', [JobPostingController::class, 'update'])->middleware('permission:edit-job-postings')->name('hr.recruitment.job-postings.update'); + Route::delete('hr/recruitment/job-postings/{jobPosting}', [JobPostingController::class, 'destroy'])->middleware('permission:delete-job-postings')->name('hr.recruitment.job-postings.destroy'); + Route::put('hr/recruitment/job-postings/{jobPosting}/publish', [JobPostingController::class, 'publish'])->middleware('permission:publish-job-postings')->name('hr.recruitment.job-postings.publish'); + Route::put('hr/recruitment/job-postings/{jobPosting}/unpublish', [JobPostingController::class, 'unpublish'])->middleware('permission:publish-job-postings')->name('hr.recruitment.job-postings.unpublish'); + }); + + // Candidate Sources Routes + Route::middleware('permission:manage-candidate-sources')->group(function () { + Route::get('hr/recruitment/candidate-sources', [CandidateSourceController::class, 'index'])->name('hr.recruitment.candidate-sources.index'); + Route::post('hr/recruitment/candidate-sources', [CandidateSourceController::class, 'store'])->middleware('permission:create-candidate-sources')->name('hr.recruitment.candidate-sources.store'); + Route::put('hr/recruitment/candidate-sources/{candidateSource}', [CandidateSourceController::class, 'update'])->middleware('permission:edit-candidate-sources')->name('hr.recruitment.candidate-sources.update'); + Route::delete('hr/recruitment/candidate-sources/{candidateSource}', [CandidateSourceController::class, 'destroy'])->middleware('permission:delete-candidate-sources')->name('hr.recruitment.candidate-sources.destroy'); + Route::put('hr/recruitment/candidate-sources/{candidateSource}/toggle-status', [CandidateSourceController::class, 'toggleStatus'])->middleware('permission:edit-candidate-sources')->name('hr.recruitment.candidate-sources.toggle-status'); + }); + + // Candidates Routes + Route::middleware('permission:manage-candidates')->group(function () { + Route::get('hr/recruitment/candidates', [CandidateController::class, 'index'])->name('hr.recruitment.candidates.index'); + Route::get('hr/recruitment/candidates/{candidate}', [CandidateController::class, 'show'])->middleware('permission:view-candidates')->name('hr.recruitment.candidates.show'); + // Route::post('hr/recruitment/candidates', [CandidateController::class, 'store'])->middleware('permission:create-candidates')->name('hr.recruitment.candidates.store'); + // Route::put('hr/recruitment/candidates/{candidate}', [CandidateController::class, 'update'])->middleware('permission:edit-candidates')->name('hr.recruitment.candidates.update'); + Route::delete('hr/recruitment/candidates/{candidate}', [CandidateController::class, 'destroy'])->middleware('permission:delete-candidates')->name('hr.recruitment.candidates.destroy'); + Route::put('hr/recruitment/candidates/{candidate}/status', [CandidateController::class, 'updateStatus'])->middleware('permission:edit-candidates')->name('hr.recruitment.candidates.update-status'); + + // Convert to Employee Routes + Route::get('hr/recruitment/candidates/{candidate}/convert-to-employee', [CandidateController::class, 'convertToEmployee'])->middleware('permission:create-employees')->name('hr.recruitment.candidates.convert-to-employee'); + Route::post('hr/recruitment/candidates/store-employee', [CandidateController::class, 'storeEmployee'])->middleware('permission:create-employees')->name('hr.recruitment.candidates.store-employee'); + }); + + // Interview Types Routes + Route::middleware('permission:manage-interview-types')->group(function () { + Route::get('hr/recruitment/interview-types', [InterviewTypeController::class, 'index'])->name('hr.recruitment.interview-types.index'); + Route::post('hr/recruitment/interview-types', [InterviewTypeController::class, 'store'])->middleware('permission:create-interview-types')->name('hr.recruitment.interview-types.store'); + Route::put('hr/recruitment/interview-types/{interviewType}', [InterviewTypeController::class, 'update'])->middleware('permission:edit-interview-types')->name('hr.recruitment.interview-types.update'); + Route::delete('hr/recruitment/interview-types/{interviewType}', [InterviewTypeController::class, 'destroy'])->middleware('permission:delete-interview-types')->name('hr.recruitment.interview-types.destroy'); + Route::put('hr/recruitment/interview-types/{interviewType}/toggle-status', [InterviewTypeController::class, 'toggleStatus'])->middleware('permission:edit-interview-types')->name('hr.recruitment.interview-types.toggle-status'); + }); + + // Interview Rounds Routes + Route::middleware('permission:manage-interview-rounds')->group(function () { + Route::get('hr/recruitment/interview-rounds', [InterviewRoundController::class, 'index'])->name('hr.recruitment.interview-rounds.index'); + Route::post('hr/recruitment/interview-rounds', [InterviewRoundController::class, 'store'])->middleware('permission:create-interview-rounds')->name('hr.recruitment.interview-rounds.store'); + Route::put('hr/recruitment/interview-rounds/{interviewRound}', [InterviewRoundController::class, 'update'])->middleware('permission:edit-interview-rounds')->name('hr.recruitment.interview-rounds.update'); + Route::delete('hr/recruitment/interview-rounds/{interviewRound}', [InterviewRoundController::class, 'destroy'])->middleware('permission:delete-interview-rounds')->name('hr.recruitment.interview-rounds.destroy'); + Route::put('hr/recruitment/interview-rounds/{interviewRound}/toggle-status', [InterviewRoundController::class, 'toggleStatus'])->middleware('permission:edit-interview-rounds')->name('hr.recruitment.interview-rounds.toggle-status'); + }); + + // Interviews Routes + Route::middleware('permission:manage-interviews')->group(function () { + Route::get('hr/recruitment/interviews', [InterviewController::class, 'index'])->name('hr.recruitment.interviews.index'); + Route::post('hr/recruitment/interviews', [InterviewController::class, 'store'])->middleware('permission:create-interviews')->name('hr.recruitment.interviews.store'); + Route::put('hr/recruitment/interviews/{interview}', [InterviewController::class, 'update'])->middleware('permission:edit-interviews')->name('hr.recruitment.interviews.update'); + Route::delete('hr/recruitment/interviews/{interview}', [InterviewController::class, 'destroy'])->middleware('permission:delete-interviews')->name('hr.recruitment.interviews.destroy'); + Route::put('hr/recruitment/interviews/{interview}/status', [InterviewController::class, 'updateStatus'])->middleware('permission:edit-interviews')->name('hr.recruitment.interviews.update-status'); + Route::get('hr/recruitment/interviews/rounds-by-candidate/{candidate}', [InterviewController::class, 'getRoundsByCandidate'])->name('hr.recruitment.interviews.rounds-by-candidate'); + }); + + // Interview Feedback Routes + Route::middleware('permission:manage-interview-feedback')->group(function () { + Route::get('hr/recruitment/interview-feedback', [InterviewFeedbackController::class, 'index'])->name('hr.recruitment.interview-feedback.index'); + Route::post('hr/recruitment/interview-feedback', [InterviewFeedbackController::class, 'store'])->middleware('permission:create-interview-feedback')->name('hr.recruitment.interview-feedback.store'); + Route::put('hr/recruitment/interview-feedback/{interviewFeedback}', [InterviewFeedbackController::class, 'update'])->middleware('permission:edit-interview-feedback')->name('hr.recruitment.interview-feedback.update'); + Route::delete('hr/recruitment/interview-feedback/{interviewFeedback}', [InterviewFeedbackController::class, 'destroy'])->middleware('permission:delete-interview-feedback')->name('hr.recruitment.interview-feedback.destroy'); + Route::get('hr/recruitment/interview-feedback/get-interviewers/{interview}', [InterviewFeedbackController::class, 'getInterviewers'])->name('hr.recruitment.interview-feedback.get-interviewers'); + }); + + // Custom Questions Routes + Route::middleware('permission:manage-custom-questions')->group(function () { + Route::get('hr/recruitment/custom-questions', [CustomQuestionController::class, 'index'])->name('hr.recruitment.custom-questions.index'); + Route::post('hr/recruitment/custom-questions', [CustomQuestionController::class, 'store'])->middleware('permission:create-custom-questions')->name('hr.recruitment.custom-questions.store'); + Route::put('hr/recruitment/custom-questions/{customQuestion}', [CustomQuestionController::class, 'update'])->middleware('permission:edit-custom-questions')->name('hr.recruitment.custom-questions.update'); + Route::delete('hr/recruitment/custom-questions/{customQuestion}', [CustomQuestionController::class, 'destroy'])->middleware('permission:delete-custom-questions')->name('hr.recruitment.custom-questions.destroy'); + }); + + // Candidate Assessments Routes + Route::middleware('permission:manage-candidate-assessments')->group(function () { + Route::get('hr/recruitment/candidate-assessments', [CandidateAssessmentController::class, 'index'])->name('hr.recruitment.candidate-assessments.index'); + Route::post('hr/recruitment/candidate-assessments', [CandidateAssessmentController::class, 'store'])->middleware('permission:create-candidate-assessments')->name('hr.recruitment.candidate-assessments.store'); + Route::put('hr/recruitment/candidate-assessments/{candidateAssessment}', [CandidateAssessmentController::class, 'update'])->middleware('permission:edit-candidate-assessments')->name('hr.recruitment.candidate-assessments.update'); + Route::delete('hr/recruitment/candidate-assessments/{candidateAssessment}', [CandidateAssessmentController::class, 'destroy'])->middleware('permission:delete-candidate-assessments')->name('hr.recruitment.candidate-assessments.destroy'); + }); + + // Offer Templates Routes + Route::middleware('permission:manage-offer-templates')->group(function () { + Route::get('hr/recruitment/offer-templates', [OfferTemplateController::class, 'index'])->name('hr.recruitment.offer-templates.index'); + Route::get('hr/recruitment/offer-templates/create', [OfferTemplateController::class, 'create'])->middleware('permission:create-offer-templates')->name('hr.recruitment.offer-templates.create'); + Route::post('hr/recruitment/offer-templates', [OfferTemplateController::class, 'store'])->middleware('permission:create-offer-templates')->name('hr.recruitment.offer-templates.store'); + Route::get('hr/recruitment/offer-templates/{offerTemplate}', [OfferTemplateController::class, 'show'])->middleware('permission:view-offer-templates')->name('hr.recruitment.offer-templates.show'); + Route::get('hr/recruitment/offer-templates/{offerTemplate}/edit', [OfferTemplateController::class, 'edit'])->middleware('permission:edit-offer-templates')->name('hr.recruitment.offer-templates.edit'); + Route::put('hr/recruitment/offer-templates/{offerTemplate}', [OfferTemplateController::class, 'update'])->middleware('permission:edit-offer-templates')->name('hr.recruitment.offer-templates.update'); + Route::delete('hr/recruitment/offer-templates/{offerTemplate}', [OfferTemplateController::class, 'destroy'])->middleware('permission:delete-offer-templates')->name('hr.recruitment.offer-templates.destroy'); + Route::put('hr/recruitment/offer-templates/{offerTemplate}/toggle-status', [OfferTemplateController::class, 'toggleStatus'])->middleware('permission:edit-offer-templates')->name('hr.recruitment.offer-templates.toggle-status'); + Route::post('hr/recruitment/offer-templates/{offerTemplate}/preview', [OfferTemplateController::class, 'preview'])->middleware('permission:view-offer-templates')->name('hr.recruitment.offer-templates.preview'); + Route::post('hr/recruitment/offer-templates/{offerTemplate}/generate', [OfferTemplateController::class, 'generate'])->middleware('permission:view-offer-templates')->name('hr.recruitment.offer-templates.generate'); + }); + + // Offers Routes + Route::middleware('permission:manage-offers')->group(function () { + Route::get('hr/recruitment/offers', [OfferController::class, 'index'])->name('hr.recruitment.offers.index'); + Route::get('hr/recruitment/offers/{offer}', [OfferController::class, 'show'])->middleware('permission:view-offers')->name('hr.recruitment.offers.show'); + Route::post('hr/recruitment/offers', [OfferController::class, 'store'])->middleware('permission:create-offers')->name('hr.recruitment.offers.store'); + Route::put('hr/recruitment/offers/{offer}', [OfferController::class, 'update'])->middleware('permission:edit-offers')->name('hr.recruitment.offers.update'); + Route::delete('hr/recruitment/offers/{offer}', [OfferController::class, 'destroy'])->middleware('permission:delete-offers')->name('hr.recruitment.offers.destroy'); + Route::put('hr/recruitment/offers/{offer}/status', [OfferController::class, 'updateStatus'])->middleware('permission:edit-offers')->name('hr.recruitment.offers.update-status'); + Route::get('hr/recruitment/offers/candidate/{candidateId}/job', [OfferController::class, 'getCandidateJob'])->name('hr.recruitment.offers.candidate-job'); + Route::get('hr/recruitment/offers/job/{jobId}/departments', [OfferController::class, 'getJobDepartments'])->name('hr.recruitment.offers.job-departments'); + }); + + // Onboarding Checklists Routes + Route::middleware('permission:manage-onboarding-checklists')->group(function () { + Route::get('hr/recruitment/onboarding-checklists', [OnboardingChecklistController::class, 'index'])->name('hr.recruitment.onboarding-checklists.index'); + Route::post('hr/recruitment/onboarding-checklists', [OnboardingChecklistController::class, 'store'])->middleware('permission:create-onboarding-checklists')->name('hr.recruitment.onboarding-checklists.store'); + Route::put('hr/recruitment/onboarding-checklists/{onboardingChecklist}', [OnboardingChecklistController::class, 'update'])->middleware('permission:edit-onboarding-checklists')->name('hr.recruitment.onboarding-checklists.update'); + Route::delete('hr/recruitment/onboarding-checklists/{onboardingChecklist}', [OnboardingChecklistController::class, 'destroy'])->middleware('permission:delete-onboarding-checklists')->name('hr.recruitment.onboarding-checklists.destroy'); + Route::put('hr/recruitment/onboarding-checklists/{onboardingChecklist}/toggle-status', [OnboardingChecklistController::class, 'toggleStatus'])->middleware('permission:edit-onboarding-checklists')->name('hr.recruitment.onboarding-checklists.toggle-status'); + }); + + // Checklist Items Routes + Route::middleware('permission:manage-checklist-items')->group(function () { + Route::get('hr/recruitment/checklist-items', [ChecklistItemController::class, 'index'])->name('hr.recruitment.checklist-items.index'); + Route::post('hr/recruitment/checklist-items', [ChecklistItemController::class, 'store'])->middleware('permission:create-checklist-items')->name('hr.recruitment.checklist-items.store'); + Route::put('hr/recruitment/checklist-items/{checklistItem}', [ChecklistItemController::class, 'update'])->middleware('permission:edit-checklist-items')->name('hr.recruitment.checklist-items.update'); + Route::delete('hr/recruitment/checklist-items/{checklistItem}', [ChecklistItemController::class, 'destroy'])->middleware('permission:delete-checklist-items')->name('hr.recruitment.checklist-items.destroy'); + Route::put('hr/recruitment/checklist-items/{checklistItem}/toggle-status', [ChecklistItemController::class, 'toggleStatus'])->middleware('permission:edit-checklist-items')->name('hr.recruitment.checklist-items.toggle-status'); + }); + + // Candidate Onboarding Routes + Route::middleware('permission:manage-candidate-onboarding')->group(function () { + Route::get('hr/recruitment/candidate-onboarding', [CandidateOnboardingController::class, 'index'])->name('hr.recruitment.candidate-onboarding.index'); + Route::get('hr/recruitment/candidate-onboarding/{candidateOnboarding}', [CandidateOnboardingController::class, 'show'])->middleware('permission:view-candidate-onboarding')->name('hr.recruitment.candidate-onboarding.show'); + Route::post('hr/recruitment/candidate-onboarding', [CandidateOnboardingController::class, 'store'])->middleware('permission:create-candidate-onboarding')->name('hr.recruitment.candidate-onboarding.store'); + Route::put('hr/recruitment/candidate-onboarding/{candidateOnboarding}', [CandidateOnboardingController::class, 'update'])->middleware('permission:edit-candidate-onboarding')->name('hr.recruitment.candidate-onboarding.update'); + Route::delete('hr/recruitment/candidate-onboarding/{candidateOnboarding}', [CandidateOnboardingController::class, 'destroy'])->middleware('permission:delete-candidate-onboarding')->name('hr.recruitment.candidate-onboarding.destroy'); + Route::put('hr/recruitment/candidate-onboarding/{candidateOnboarding}/status', [CandidateOnboardingController::class, 'updateStatus'])->middleware('permission:manage-candidate-onboarding-status')->name('hr.recruitment.candidate-onboarding.update-status'); + }); + + // Meeting Types Routes + Route::middleware('permission:manage-meeting-types')->group(function () { + Route::get('meetings/meeting-types', [MeetingTypeController::class, 'index'])->name('meetings.meeting-types.index'); + Route::post('meetings/meeting-types', [MeetingTypeController::class, 'store'])->middleware('permission:create-meeting-types')->name('meetings.meeting-types.store'); + Route::put('meetings/meeting-types/{meetingType}', [MeetingTypeController::class, 'update'])->middleware('permission:edit-meeting-types')->name('meetings.meeting-types.update'); + Route::delete('meetings/meeting-types/{meetingType}', [MeetingTypeController::class, 'destroy'])->middleware('permission:delete-meeting-types')->name('meetings.meeting-types.destroy'); + Route::put('meetings/meeting-types/{meetingType}/toggle-status', [MeetingTypeController::class, 'toggleStatus'])->middleware('permission:edit-meeting-types')->name('meetings.meeting-types.toggle-status'); + }); + + // Meeting Rooms Routes + Route::middleware('permission:manage-meeting-rooms')->group(function () { + Route::get('meetings/meeting-rooms', [MeetingRoomController::class, 'index'])->name('meetings.meeting-rooms.index'); + Route::post('meetings/meeting-rooms', [MeetingRoomController::class, 'store'])->middleware('permission:create-meeting-rooms')->name('meetings.meeting-rooms.store'); + Route::put('meetings/meeting-rooms/{meetingRoom}', [MeetingRoomController::class, 'update'])->middleware('permission:edit-meeting-rooms')->name('meetings.meeting-rooms.update'); + Route::delete('meetings/meeting-rooms/{meetingRoom}', [MeetingRoomController::class, 'destroy'])->middleware('permission:delete-meeting-rooms')->name('meetings.meeting-rooms.destroy'); + Route::put('meetings/meeting-rooms/{meetingRoom}/toggle-status', [MeetingRoomController::class, 'toggleStatus'])->middleware('permission:edit-meeting-rooms')->name('meetings.meeting-rooms.toggle-status'); + }); + + // Meetings Routes + Route::middleware('permission:manage-meetings')->group(function () { + Route::get('meetings/meetings', [MeetingController::class, 'index'])->name('meetings.meetings.index'); + Route::post('meetings/meetings', [MeetingController::class, 'store'])->middleware('permission:create-meetings')->name('meetings.meetings.store'); + Route::put('meetings/meetings/{meeting}', [MeetingController::class, 'update'])->middleware('permission:edit-meetings')->name('meetings.meetings.update'); + Route::delete('meetings/meetings/{meeting}', [MeetingController::class, 'destroy'])->middleware('permission:delete-meetings')->name('meetings.meetings.destroy'); + Route::put('meetings/meetings/{meeting}/status', [MeetingController::class, 'updateMeetingStatus'])->middleware('permission:manage-meeting-status')->name('meetings.meetings.update-status'); + }); + + // Meeting Attendees Routes + Route::middleware('permission:manage-meeting-attendees')->group(function () { + Route::get('meetings/meeting-attendees', [MeetingAttendeeController::class, 'index'])->name('meetings.meeting-attendees.index'); + Route::post('meetings/meeting-attendees', [MeetingAttendeeController::class, 'store'])->middleware('permission:create-meeting-attendees')->name('meetings.meeting-attendees.store'); + Route::put('meetings/meeting-attendees/{meetingAttendee}', [MeetingAttendeeController::class, 'update'])->middleware('permission:edit-meeting-attendees')->name('meetings.meeting-attendees.update'); + Route::delete('meetings/meeting-attendees/{meetingAttendee}', [MeetingAttendeeController::class, 'destroy'])->middleware('permission:delete-meeting-attendees')->name('meetings.meeting-attendees.destroy'); + Route::put('meetings/meeting-attendees/{meetingAttendee}/rsvp', [MeetingAttendeeController::class, 'updateMeetingRsvp'])->middleware('permission:manage-meeting-rsvp-status')->name('meetings.meeting-attendees.update-rsvp'); + Route::put('meetings/meeting-attendees/{meetingAttendee}/attendance', [MeetingAttendeeController::class, 'updateMeetingAttendance'])->middleware('permission:manage-meeting-attendance')->name('meetings.meeting-attendees.update-attendance'); + }); + + // Meeting Minutes Routes + Route::middleware('permission:manage-meeting-minutes')->group(function () { + Route::get('meetings/meeting-minutes', [MeetingMinuteController::class, 'index'])->name('meetings.meeting-minutes.index'); + Route::post('meetings/meeting-minutes', [MeetingMinuteController::class, 'store'])->middleware('permission:create-meeting-minutes')->name('meetings.meeting-minutes.store'); + Route::put('meetings/meeting-minutes/{meetingMinute}', [MeetingMinuteController::class, 'update'])->middleware('permission:edit-meeting-minutes')->name('meetings.meeting-minutes.update'); + Route::delete('meetings/meeting-minutes/{meetingMinute}', [MeetingMinuteController::class, 'destroy'])->middleware('permission:delete-meeting-minutes')->name('meetings.meeting-minutes.destroy'); + }); + + // Action Items Routes + Route::middleware('permission:manage-action-items')->group(function () { + Route::get('meetings/action-items', [ActionItemController::class, 'index'])->name('meetings.action-items.index'); + Route::post('meetings/action-items', [ActionItemController::class, 'store'])->middleware('permission:create-action-items')->name('meetings.action-items.store'); + Route::put('meetings/action-items/{actionItem}', [ActionItemController::class, 'update'])->middleware('permission:edit-action-items')->name('meetings.action-items.update'); + Route::delete('meetings/action-items/{actionItem}', [ActionItemController::class, 'destroy'])->middleware('permission:delete-action-items')->name('meetings.action-items.destroy'); + Route::put('meetings/action-items/{actionItem}/progress', [ActionItemController::class, 'updateProgress'])->middleware('permission:edit-action-items')->name('meetings.action-items.update-progress'); + }); + + // Contract Types Routes + Route::middleware('permission:manage-contract-types')->group(function () { + Route::get('hr/contracts/contract-types', [ContractTypeController::class, 'index'])->name('hr.contracts.contract-types.index'); + Route::post('hr/contracts/contract-types', [ContractTypeController::class, 'store'])->middleware('permission:create-contract-types')->name('hr.contracts.contract-types.store'); + Route::put('hr/contracts/contract-types/{contractType}', [ContractTypeController::class, 'update'])->middleware('permission:edit-contract-types')->name('hr.contracts.contract-types.update'); + Route::delete('hr/contracts/contract-types/{contractType}', [ContractTypeController::class, 'destroy'])->middleware('permission:delete-contract-types')->name('hr.contracts.contract-types.destroy'); + Route::put('hr/contracts/contract-types/{contractType}/toggle-status', [ContractTypeController::class, 'toggleStatus'])->middleware('permission:edit-contract-types')->name('hr.contracts.contract-types.toggle-status'); + }); + + // Employee Contracts Routes + Route::middleware('permission:manage-employee-contracts')->group(function () { + Route::get('hr/contracts/employee-contracts', [EmployeeContractController::class, 'index'])->name('hr.contracts.employee-contracts.index'); + Route::post('hr/contracts/employee-contracts', [EmployeeContractController::class, 'store'])->middleware('permission:create-employee-contracts')->name('hr.contracts.employee-contracts.store'); + Route::put('hr/contracts/employee-contracts/{employeeContract}', [EmployeeContractController::class, 'update'])->middleware('permission:edit-employee-contracts')->name('hr.contracts.employee-contracts.update'); + Route::delete('hr/contracts/employee-contracts/{employeeContract}', [EmployeeContractController::class, 'destroy'])->middleware('permission:delete-employee-contracts')->name('hr.contracts.employee-contracts.destroy'); + Route::put('hr/contracts/employee-contracts/{employeeContract}/status', [EmployeeContractController::class, 'updateStatus'])->middleware('permission:approve-employee-contracts')->name('hr.contracts.employee-contracts.update-status'); + }); + + // Contract Renewals Routes + Route::middleware('permission:manage-contract-renewals')->group(function () { + Route::get('hr/contracts/contract-renewals', [ContractRenewalController::class, 'index'])->name('hr.contracts.contract-renewals.index'); + Route::post('hr/contracts/contract-renewals', [ContractRenewalController::class, 'store'])->middleware('permission:create-contract-renewals')->name('hr.contracts.contract-renewals.store'); + Route::put('hr/contracts/contract-renewals/{contractRenewal}', [ContractRenewalController::class, 'update'])->middleware('permission:edit-contract-renewals')->name('hr.contracts.contract-renewals.update'); + Route::delete('hr/contracts/contract-renewals/{contractRenewal}', [ContractRenewalController::class, 'destroy'])->middleware('permission:delete-contract-renewals')->name('hr.contracts.contract-renewals.destroy'); + Route::put('hr/contracts/contract-renewals/{contractRenewal}/approve', [ContractRenewalController::class, 'approve'])->middleware('permission:approve-contract-renewals')->name('hr.contracts.contract-renewals.approve'); + Route::put('hr/contracts/contract-renewals/{contractRenewal}/reject', [ContractRenewalController::class, 'reject'])->middleware('permission:reject-contract-renewals')->name('hr.contracts.contract-renewals.reject'); + Route::put('hr/contracts/contract-renewals/{contractRenewal}/process', [ContractRenewalController::class, 'process'])->middleware('permission:edit-contract-renewals')->name('hr.contracts.contract-renewals.process'); + }); + + // Contract Templates Routes + Route::middleware('permission:manage-contract-templates')->group(function () { + Route::get('hr/contracts/contract-templates', [ContractTemplateController::class, 'index'])->name('hr.contracts.contract-templates.index'); + Route::get('hr/contracts/contract-templates/create', [ContractTemplateController::class, 'create'])->middleware('permission:create-contract-templates')->name('hr.contracts.contract-templates.create'); + Route::post('hr/contracts/contract-templates', [ContractTemplateController::class, 'store'])->middleware('permission:create-contract-templates')->name('hr.contracts.contract-templates.store'); + Route::get('hr/contracts/contract-templates/{contractTemplate}', [ContractTemplateController::class, 'show'])->middleware('permission:view-contract-templates')->name('hr.contracts.contract-templates.show'); + Route::get('hr/contracts/contract-templates/{contractTemplate}/edit', [ContractTemplateController::class, 'edit'])->middleware('permission:edit-contract-templates')->name('hr.contracts.contract-templates.edit'); + Route::put('hr/contracts/contract-templates/{contractTemplate}', [ContractTemplateController::class, 'update'])->middleware('permission:edit-contract-templates')->name('hr.contracts.contract-templates.update'); + Route::delete('hr/contracts/contract-templates/{contractTemplate}', [ContractTemplateController::class, 'destroy'])->middleware('permission:delete-contract-templates')->name('hr.contracts.contract-templates.destroy'); + Route::put('hr/contracts/contract-templates/{contractTemplate}/toggle-status', [ContractTemplateController::class, 'toggleStatus'])->middleware('permission:edit-contract-templates')->name('hr.contracts.contract-templates.toggle-status'); + Route::post('hr/contracts/contract-templates/{contractTemplate}/generate', [ContractTemplateController::class, 'generate'])->middleware('permission:view-contract-templates')->name('hr.contracts.contract-templates.generate'); + }); + + // Document Categories Routes + Route::middleware('permission:manage-document-categories')->group(function () { + Route::get('hr/documents/document-categories', [DocumentCategoryController::class, 'index'])->name('hr.documents.document-categories.index'); + Route::post('hr/documents/document-categories', [DocumentCategoryController::class, 'store'])->middleware('permission:create-document-categories')->name('hr.documents.document-categories.store'); + Route::put('hr/documents/document-categories/{documentCategory}', [DocumentCategoryController::class, 'update'])->middleware('permission:edit-document-categories')->name('hr.documents.document-categories.update'); + Route::delete('hr/documents/document-categories/{documentCategory}', [DocumentCategoryController::class, 'destroy'])->middleware('permission:delete-document-categories')->name('hr.documents.document-categories.destroy'); + Route::put('hr/documents/document-categories/{documentCategory}/toggle-status', [DocumentCategoryController::class, 'toggleStatus'])->middleware('permission:edit-document-categories')->name('hr.documents.document-categories.toggle-status'); + }); + + // HR Documents Routes + Route::middleware('permission:manage-hr-documents')->group(function () { + Route::get('hr/documents/hr-documents', [HrDocumentController::class, 'index'])->name('hr.documents.hr-documents.index'); + Route::post('hr/documents/hr-documents', [HrDocumentController::class, 'store'])->middleware('permission:create-hr-documents')->name('hr.documents.hr-documents.store'); + Route::put('hr/documents/hr-documents/{hrDocument}', [HrDocumentController::class, 'update'])->middleware('permission:edit-hr-documents')->name('hr.documents.hr-documents.update'); + Route::delete('hr/documents/hr-documents/{hrDocument}', [HrDocumentController::class, 'destroy'])->middleware('permission:delete-hr-documents')->name('hr.documents.hr-documents.destroy'); + Route::get('hr/documents/hr-documents/{hrDocument}/download', [HrDocumentController::class, 'download'])->middleware('permission:view-hr-documents')->name('hr.documents.hr-documents.download'); + Route::put('hr/documents/hr-documents/{hrDocument}/status', [HrDocumentController::class, 'updateStatus'])->middleware('permission:edit-hr-documents')->name('hr.documents.hr-documents.update-status'); + }); + + // Document Acknowledgments Routes + Route::middleware('permission:manage-document-acknowledgments')->group(function () { + Route::get('hr/documents/document-acknowledgments', [DocumentAcknowledgmentController::class, 'index'])->name('hr.documents.document-acknowledgments.index'); + Route::post('hr/documents/document-acknowledgments', [DocumentAcknowledgmentController::class, 'store'])->middleware('permission:create-document-acknowledgments')->name('hr.documents.document-acknowledgments.store'); + Route::put('hr/documents/document-acknowledgments/{documentAcknowledgment}', [DocumentAcknowledgmentController::class, 'update'])->middleware('permission:edit-document-acknowledgments')->name('hr.documents.document-acknowledgments.update'); + Route::delete('hr/documents/document-acknowledgments/{documentAcknowledgment}', [DocumentAcknowledgmentController::class, 'destroy'])->middleware('permission:delete-document-acknowledgments')->name('hr.documents.document-acknowledgments.destroy'); + Route::put('hr/documents/document-acknowledgments/{documentAcknowledgment}/acknowledge', [DocumentAcknowledgmentController::class, 'acknowledge'])->middleware('permission:acknowledge-document-acknowledgments')->name('hr.documents.document-acknowledgments.acknowledge'); + Route::post('hr/documents/document-acknowledgments/bulk-assign', [DocumentAcknowledgmentController::class, 'bulkAssign'])->middleware('permission:create-document-acknowledgments')->name('hr.documents.document-acknowledgments.bulk-assign'); + }); + + // Document Templates Routes + Route::middleware('permission:manage-document-templates')->group(function () { + Route::get('hr/documents/document-templates', [DocumentTemplateController::class, 'index'])->name('hr.documents.document-templates.index'); + Route::get('hr/documents/document-templates/create', [DocumentTemplateController::class, 'create'])->middleware('permission:create-document-templates')->name('hr.documents.document-templates.create'); + Route::post('hr/documents/document-templates', [DocumentTemplateController::class, 'store'])->middleware('permission:create-document-templates')->name('hr.documents.document-templates.store'); + Route::get('hr/documents/document-templates/{documentTemplate}', [DocumentTemplateController::class, 'show'])->middleware('permission:view-document-templates')->name('hr.documents.document-templates.show'); + Route::get('hr/documents/document-templates/{documentTemplate}/edit', [DocumentTemplateController::class, 'edit'])->middleware('permission:edit-document-templates')->name('hr.documents.document-templates.edit'); + Route::put('hr/documents/document-templates/{documentTemplate}', [DocumentTemplateController::class, 'update'])->middleware('permission:edit-document-templates')->name('hr.documents.document-templates.update'); + Route::delete('hr/documents/document-templates/{documentTemplate}', [DocumentTemplateController::class, 'destroy'])->middleware('permission:delete-document-templates')->name('hr.documents.document-templates.destroy'); + Route::put('hr/documents/document-templates/{documentTemplate}/toggle-status', [DocumentTemplateController::class, 'toggleStatus'])->middleware('permission:edit-document-templates')->name('hr.documents.document-templates.toggle-status'); + Route::post('hr/documents/document-templates/{documentTemplate}/preview', [DocumentTemplateController::class, 'preview'])->middleware('permission:view-document-templates')->name('hr.documents.document-templates.preview'); + Route::post('hr/documents/document-templates/{documentTemplate}/generate', [DocumentTemplateController::class, 'generate'])->middleware('permission:view-document-templates')->name('hr.documents.document-templates.generate'); + }); + + // Leave Types routes + Route::middleware('permission:manage-leave-types')->group(function () { + Route::get('hr/leave-types', [LeaveTypeController::class, 'index'])->name('hr.leave-types.index'); + Route::post('hr/leave-types', [LeaveTypeController::class, 'store'])->middleware('permission:create-leave-types')->name('hr.leave-types.store'); + Route::put('hr/leave-types/{leaveType}', [LeaveTypeController::class, 'update'])->middleware('permission:edit-leave-types')->name('hr.leave-types.update'); + Route::delete('hr/leave-types/{leaveType}', [LeaveTypeController::class, 'destroy'])->middleware('permission:delete-leave-types')->name('hr.leave-types.destroy'); + Route::put('hr/leave-types/{leaveType}/toggle-status', [LeaveTypeController::class, 'toggleStatus'])->middleware('permission:edit-leave-types')->name('hr.leave-types.toggle-status'); + }); + + // Leave Policies routes + Route::middleware('permission:manage-leave-policies')->group(function () { + Route::get('hr/leave-policies', [LeavePolicyController::class, 'index'])->name('hr.leave-policies.index'); + Route::post('hr/leave-policies', [LeavePolicyController::class, 'store'])->middleware('permission:create-leave-policies')->name('hr.leave-policies.store'); + Route::put('hr/leave-policies/{leavePolicy}', [LeavePolicyController::class, 'update'])->middleware('permission:edit-leave-policies')->name('hr.leave-policies.update'); + Route::delete('hr/leave-policies/{leavePolicy}', [LeavePolicyController::class, 'destroy'])->middleware('permission:delete-leave-policies')->name('hr.leave-policies.destroy'); + Route::put('hr/leave-policies/{leavePolicy}/toggle-status', [LeavePolicyController::class, 'toggleStatus'])->middleware('permission:edit-leave-policies')->name('hr.leave-policies.toggle-status'); + }); + + // Leave Applications routes + Route::middleware('permission:manage-leave-applications')->group(function () { + Route::get('hr/leave-applications', [LeaveApplicationController::class, 'index'])->name('hr.leave-applications.index'); + Route::get('hr/leave-applications/export', [LeaveApplicationController::class, 'export'])->name('hr.leave-applications.export'); + Route::post('hr/leave-applications', [LeaveApplicationController::class, 'store'])->middleware('permission:create-leave-applications')->name('hr.leave-applications.store'); + Route::put('hr/leave-applications/{leaveApplication}', [LeaveApplicationController::class, 'update'])->middleware('permission:edit-leave-applications')->name('hr.leave-applications.update'); + Route::delete('hr/leave-applications/{leaveApplication}', [LeaveApplicationController::class, 'destroy'])->middleware('permission:delete-leave-applications')->name('hr.leave-applications.destroy'); + Route::put('hr/leave-applications/{leaveApplication}/status', [LeaveApplicationController::class, 'updateStatus'])->middleware('permission:approve-leave-applications')->name('hr.leave-applications.update-status'); + }); + + // Leave Balances routes + Route::middleware('permission:manage-leave-balances')->group(function () { + Route::get('hr/leave-balances', [LeaveBalanceController::class, 'index'])->name('hr.leave-balances.index'); + Route::post('hr/leave-balances', [LeaveBalanceController::class, 'store'])->middleware('permission:create-leave-balances')->name('hr.leave-balances.store'); + Route::put('hr/leave-balances/{leaveBalance}', [LeaveBalanceController::class, 'update'])->middleware('permission:edit-leave-balances')->name('hr.leave-balances.update'); + Route::delete('hr/leave-balances/{leaveBalance}', [LeaveBalanceController::class, 'destroy'])->middleware('permission:delete-leave-balances')->name('hr.leave-balances.destroy'); + Route::put('hr/leave-balances/{leaveBalance}/adjust', [LeaveBalanceController::class, 'adjust'])->middleware('permission:adjust-leave-balances')->name('hr.leave-balances.adjust'); + }); + + // Shifts routes + Route::middleware('permission:manage-shifts')->group(function () { + Route::get('hr/shifts', [ShiftController::class, 'index'])->name('hr.shifts.index'); + Route::post('hr/shifts', [ShiftController::class, 'store'])->middleware('permission:create-shifts')->name('hr.shifts.store'); + Route::put('hr/shifts/{shift}', [ShiftController::class, 'update'])->middleware('permission:edit-shifts')->name('hr.shifts.update'); + Route::delete('hr/shifts/{shift}', [ShiftController::class, 'destroy'])->middleware('permission:delete-shifts')->name('hr.shifts.destroy'); + Route::put('hr/shifts/{shift}/toggle-status', [ShiftController::class, 'toggleStatus'])->middleware('permission:edit-shifts')->name('hr.shifts.toggle-status'); + }); + + // Attendance Policies routes + Route::middleware('permission:manage-attendance-policies')->group(function () { + Route::get('hr/attendance-policies', [AttendancePolicyController::class, 'index'])->name('hr.attendance-policies.index'); + Route::post('hr/attendance-policies', [AttendancePolicyController::class, 'store'])->middleware('permission:create-attendance-policies')->name('hr.attendance-policies.store'); + Route::put('hr/attendance-policies/{attendancePolicy}', [AttendancePolicyController::class, 'update'])->middleware('permission:edit-attendance-policies')->name('hr.attendance-policies.update'); + Route::delete('hr/attendance-policies/{attendancePolicy}', [AttendancePolicyController::class, 'destroy'])->middleware('permission:delete-attendance-policies')->name('hr.attendance-policies.destroy'); + Route::put('hr/attendance-policies/{attendancePolicy}/toggle-status', [AttendancePolicyController::class, 'toggleStatus'])->middleware('permission:edit-attendance-policies')->name('hr.attendance-policies.toggle-status'); + }); + + // Attendance Records routes + Route::middleware('permission:manage-attendance-records')->group(function () { + Route::get('hr/attendance-records', [AttendanceRecordController::class, 'index'])->name('hr.attendance-records.index'); + + Route::get('hr/attendance-records/export', [AttendanceRecordController::class, 'export'])->name('hr.attendance-records.export'); + Route::get('hr/attendance-records/download-template', [AttendanceRecordController::class, 'downloadTemplate'])->name('hr.attendance-records.download.template'); + Route::post('hr/attendance-records/parse', [AttendanceRecordController::class, 'parseFile'])->name('hr.attendance-records.parse'); + Route::post('hr/attendance-records/import', [AttendanceRecordController::class, 'fileImport'])->name('hr.attendance-records.import'); + + Route::post('hr/attendance-records', [AttendanceRecordController::class, 'store'])->middleware('permission:create-attendance-records')->name('hr.attendance-records.store'); + Route::put('hr/attendance-records/{attendanceRecord}', [AttendanceRecordController::class, 'update'])->middleware('permission:edit-attendance-records')->name('hr.attendance-records.update'); + Route::delete('hr/attendance-records/{attendanceRecord}', [AttendanceRecordController::class, 'destroy'])->middleware('permission:delete-attendance-records')->name('hr.attendance-records.destroy'); + }); + + // Clock In/Out routes + Route::middleware('permission:clock-in-out')->group(function () { + Route::post('hr/attendance/clock-in', [AttendanceRecordController::class, 'clockIn'])->name('hr.attendance.clock-in'); + Route::post('hr/attendance/clock-out', [AttendanceRecordController::class, 'clockOut'])->name('hr.attendance.clock-out'); + }); + + // Attendance Regularizations routes + Route::middleware('permission:manage-attendance-regularizations')->group(function () { + Route::get('hr/attendance-regularizations', [AttendanceRegularizationController::class, 'index'])->name('hr.attendance-regularizations.index'); + Route::post('hr/attendance-regularizations', [AttendanceRegularizationController::class, 'store'])->middleware('permission:create-attendance-regularizations')->name('hr.attendance-regularizations.store'); + Route::put('hr/attendance-regularizations/{regularization}', [AttendanceRegularizationController::class, 'update'])->middleware('permission:edit-attendance-regularizations')->name('hr.attendance-regularizations.update'); + Route::delete('hr/attendance-regularizations/{regularization}', [AttendanceRegularizationController::class, 'destroy'])->middleware('permission:delete-attendance-regularizations')->name('hr.attendance-regularizations.destroy'); + Route::put('hr/attendance-regularizations/{regularization}/status', [AttendanceRegularizationController::class, 'updateStatus'])->middleware('permission:approve-attendance-regularizations')->name('hr.attendance-regularizations.update-status'); + Route::get('hr/attendance-regularizations/get-employee-attendance/{id}', [AttendanceRegularizationController::class, 'getEmployeeAttendance'])->name('hr.attendance-regularizations.get-employee-attendance'); + }); + + // Time Entries routes + Route::middleware('permission:manage-time-entries')->group(function () { + Route::get('hr/time-entries', [TimeEntryController::class, 'index'])->name('hr.time-entries.index'); + + Route::get('hr/time-entries/export', [TimeEntryController::class, 'export'])->name('hr.time-entries.export'); + Route::get('hr/time-entries/download-template', [TimeEntryController::class, 'downloadTemplate'])->name('hr.time-entries.download.template'); + Route::post('hr/time-entries/parse', [TimeEntryController::class, 'parseFile'])->name('hr.time-entries.parse'); + Route::post('hr/time-entries/import', [TimeEntryController::class, 'fileImport'])->name('hr.time-entries.import'); + + Route::post('hr/time-entries', [TimeEntryController::class, 'store'])->middleware('permission:create-time-entries')->name('hr.time-entries.store'); + Route::put('hr/time-entries/{timeEntry}', [TimeEntryController::class, 'update'])->middleware('permission:edit-time-entries')->name('hr.time-entries.update'); + Route::delete('hr/time-entries/{timeEntry}', [TimeEntryController::class, 'destroy'])->middleware('permission:delete-time-entries')->name('hr.time-entries.destroy'); + Route::put('hr/time-entries/{timeEntry}/status', [TimeEntryController::class, 'updateStatus'])->middleware('permission:approve-time-entries')->name('hr.time-entries.update-status'); + }); + + // Salary Components routes + Route::middleware('permission:manage-salary-components')->group(function () { + Route::get('hr/salary-components', [SalaryComponentController::class, 'index'])->name('hr.salary-components.index'); + Route::post('hr/salary-components', [SalaryComponentController::class, 'store'])->middleware('permission:create-salary-components')->name('hr.salary-components.store'); + Route::put('hr/salary-components/{salaryComponent}', [SalaryComponentController::class, 'update'])->middleware('permission:edit-salary-components')->name('hr.salary-components.update'); + Route::delete('hr/salary-components/{salaryComponent}', [SalaryComponentController::class, 'destroy'])->middleware('permission:delete-salary-components')->name('hr.salary-components.destroy'); + Route::put('hr/salary-components/{salaryComponent}/toggle-status', [SalaryComponentController::class, 'toggleStatus'])->middleware('permission:edit-salary-components')->name('hr.salary-components.toggle-status'); + }); + + // Employee Salaries routes + Route::middleware('permission:manage-employee-salaries')->group(function () { + Route::get('hr/employee-salaries', [EmployeeSalaryController::class, 'index'])->name('hr.employee-salaries.index'); + Route::post('hr/employee-salaries', [EmployeeSalaryController::class, 'store'])->middleware('permission:create-employee-salaries')->name('hr.employee-salaries.store'); + Route::put('hr/employee-salaries/{employeeSalary}', [EmployeeSalaryController::class, 'update'])->middleware('permission:edit-employee-salaries')->name('hr.employee-salaries.update'); + Route::delete('hr/employee-salaries/{employeeSalary}', [EmployeeSalaryController::class, 'destroy'])->middleware('permission:delete-employee-salaries')->name('hr.employee-salaries.destroy'); + Route::put('hr/employee-salaries/{employeeSalary}/toggle-status', [EmployeeSalaryController::class, 'toggleStatus'])->middleware('permission:edit-employee-salaries')->name('hr.employee-salaries.toggle-status'); + Route::get('hr/employee-salaries/{employeeSalary}/payroll', [EmployeeSalaryController::class, 'showPayroll'])->middleware('permission:view-employee-salaries')->name('hr.employee-salaries.show-payroll'); + Route::get('hr/employee-salaries/{employeeSalary}/payroll/{payrollRun}', [EmployeeSalaryController::class, 'getPayrollCalculation'])->middleware('permission:view-employee-salaries')->name('hr.employee-salaries.get-payroll-calculation'); + }); + + // Payroll Runs routes + Route::middleware('permission:manage-payroll-runs')->group(function () { + Route::get('hr/payroll-runs', [PayrollRunController::class, 'index'])->name('hr.payroll-runs.index'); + Route::get('hr/payroll-runs/export', [PayrollRunController::class, 'export'])->name('hr.payroll-runs.export'); + Route::get('hr/payroll-runs/download-template', [PayrollRunController::class, 'downloadTemplate'])->name('hr.payroll-runs.download.template'); + Route::post('hr/payroll-runs/parse', [PayrollRunController::class, 'parseFile'])->name('hr.payroll-runs.parse'); + Route::post('hr/payroll-runs/import', [PayrollRunController::class, 'fileImport'])->name('hr.payroll-runs.import'); + Route::get('hr/payroll-runs/{payrollRun}', [PayrollRunController::class, 'show'])->middleware('permission:view-payroll-runs')->name('hr.payroll-runs.show'); + Route::post('hr/payroll-runs', [PayrollRunController::class, 'store'])->middleware('permission:create-payroll-runs')->name('hr.payroll-runs.store'); + Route::put('hr/payroll-runs/{payrollRun}', [PayrollRunController::class, 'update'])->middleware('permission:edit-payroll-runs')->name('hr.payroll-runs.update'); + Route::delete('hr/payroll-runs/{payrollRun}', [PayrollRunController::class, 'destroy'])->middleware('permission:delete-payroll-runs')->name('hr.payroll-runs.destroy'); + Route::put('hr/payroll-runs/{payrollRun}/process', [PayrollRunController::class, 'process'])->middleware('permission:process-payroll-runs')->name('hr.payroll-runs.process'); + Route::delete('hr/payroll-entries/{payrollEntry}', [PayrollRunController::class, 'destroyEntry'])->name('hr.payroll-entries.destroy'); + }); + + // Payslips routes + Route::middleware('permission:manage-payslips')->group(function () { + Route::get('hr/payslips', [PayslipController::class, 'index'])->name('hr.payslips.index'); + Route::post('hr/payslips/generate', [PayslipController::class, 'generate'])->middleware('permission:create-payslips')->name('hr.payslips.generate'); + Route::post('hr/payslips/bulk-generate', [PayslipController::class, 'bulkGenerate'])->middleware('permission:create-payslips')->name('hr.payslips.bulk-generate'); + Route::get('hr/payslips/{payslip}/download', [PayslipController::class, 'download'])->middleware('permission:download-payslips')->name('hr.payslips.download'); + }); + + // Biometric Attendance routes + Route::middleware('permission:manage-biometric-attendance')->group(function () { + Route::get('hr/biometric-attendance', [BiometricAttendanceController::class, 'index'])->name('hr.biometric-attendance.index'); + Route::get('hr/biometric-attendance/{employeeCode}/{date}', [BiometricAttendanceController::class, 'show'])->name('hr.biometric-attendance.show'); + Route::post('hr/biometric-attendance/{id}/sync', [BiometricAttendanceController::class, 'sync'])->name('hr.biometric-attendance.sync'); + }); + + // Plans management routes (admin only) + Route::middleware('permission:manage-plans')->group(function () { + Route::get('plans/create', [PlanController::class, 'create'])->middleware('permission:create-plans')->name('plans.create'); + Route::post('plans', [PlanController::class, 'store'])->middleware('permission:create-plans')->name('plans.store'); + Route::get('plans/{plan}/edit', [PlanController::class, 'edit'])->middleware('permission:edit-plans')->name('plans.edit'); + Route::put('plans/{plan}', [PlanController::class, 'update'])->middleware('permission:edit-plans')->name('plans.update'); + Route::delete('plans/{plan}', [PlanController::class, 'destroy'])->middleware('permission:delete-plans')->name('plans.destroy'); + Route::post('plans/{plan}/toggle-status', [PlanController::class, 'toggleStatus'])->name('plans.toggle-status'); + }); + + // Plan Orders routes + Route::middleware('permission:manage-plan-orders')->group(function () { + Route::get('plan-orders', [PlanOrderController::class, 'index'])->middleware('permission:manage-plan-orders')->name('plan-orders.index'); + Route::post('plan-orders/{planOrder}/approve', [PlanOrderController::class, 'approve'])->middleware('permission:approve-plan-orders')->name('plan-orders.approve'); + Route::post('plan-orders/{planOrder}/reject', [PlanOrderController::class, 'reject'])->middleware('permission:reject-plan-orders')->name('plan-orders.reject'); + }); + + // Plan Requests routes (placeholder) + Route::get('plan-requests', function () { + return Inertia::render('plans/plan-requests'); + })->name('plan-requests.index'); + + // Companies routes + Route::middleware(['checksaas', 'permission:manage-companies'])->group(function () { + Route::get('companies', [CompanyController::class, 'index'])->middleware('permission:manage-companies')->name('companies.index'); + Route::post('companies', [CompanyController::class, 'store'])->middleware('permission:create-companies')->name('companies.store'); + Route::put('companies/{company}', [CompanyController::class, 'update'])->middleware('permission:edit-companies')->name('companies.update'); + Route::delete('companies/{company}', [CompanyController::class, 'destroy'])->middleware('permission:delete-companies')->name('companies.destroy'); + Route::put('companies/{company}/reset-password', [CompanyController::class, 'resetPassword'])->middleware('permission:reset-password-companies')->name('companies.reset-password'); + Route::put('companies/{company}/toggle-status', [CompanyController::class, 'toggleStatus'])->middleware('permission:toggle-status-companies')->name('companies.toggle-status'); + Route::get('companies/{company}/plans', [CompanyController::class, 'getPlans'])->middleware('permission:manage-plans-companies')->name('companies.plans'); + Route::put('companies/{company}/upgrade-plan', [CompanyController::class, 'upgradePlan'])->middleware('permission:upgrade-plan-companies')->name('companies.upgrade-plan'); + }); + + // Coupons routes + Route::middleware(['checksaas', 'permission:manage-coupons'])->group(function () { + Route::get('coupons', [CouponController::class, 'index'])->middleware('permission:manage-coupons')->name('coupons.index'); + Route::get('coupons/{coupon}', [CouponController::class, 'show'])->middleware('permission:view-coupons')->name('coupons.show'); + Route::post('coupons', [CouponController::class, 'store'])->middleware('permission:create-coupons')->name('coupons.store'); + Route::put('coupons/{coupon}', [CouponController::class, 'update'])->middleware('permission:edit-coupons')->name('coupons.update'); + Route::put('coupons/{coupon}/toggle-status', [CouponController::class, 'toggleStatus'])->middleware('permission:toggle-status-coupons')->name('coupons.toggle-status'); + Route::delete('coupons/{coupon}', [CouponController::class, 'destroy'])->middleware('permission:delete-coupons')->name('coupons.destroy'); + }); + + // Plan Requests routes + Route::middleware(['checksaas', 'permission:manage-plan-requests'])->group(function () { + Route::get('plan-requests', [PlanRequestController::class, 'index'])->middleware('permission:manage-plan-requests')->name('plan-requests.index'); + Route::post('plan-requests/{planRequest}/approve', [PlanRequestController::class, 'approve'])->middleware('permission:approve-plan-requests')->name('plan-requests.approve'); + Route::post('plan-requests/{planRequest}/reject', [PlanRequestController::class, 'reject'])->middleware('permission:reject-plan-requests')->name('plan-requests.reject'); + }); + + // Referral routes + Route::middleware(['checksaas', 'permission:manage-referral'])->group(function () { + Route::get('referral', [ReferralController::class, 'index'])->middleware('permission:manage-referral')->name('referral.index'); + Route::get('referral/referred-users', [ReferralController::class, 'getReferredUsers'])->middleware('permission:manage-users-referral')->name('referral.referred-users'); + Route::post('referral/settings', [ReferralController::class, 'updateSettings'])->middleware('permission:manage-setting-referral')->name('referral.settings.update'); + Route::post('referral/payout-request', [ReferralController::class, 'createPayoutRequest'])->middleware('permission:manage-payout-referral')->name('referral.payout-request.create'); + Route::post('referral/payout-request/{payoutRequest}/approve', [ReferralController::class, 'approvePayoutRequest'])->middleware('permission:approve-payout-referral')->name('referral.payout-request.approve'); + Route::post('referral/payout-request/{payoutRequest}/reject', [ReferralController::class, 'rejectPayoutRequest'])->middleware('permission:reject-payout-referral')->name('referral.payout-request.reject'); + }); + + // Currencies routes + Route::middleware('permission:manage-currencies')->group(function () { + Route::get('currencies', [CurrencyController::class, 'index'])->middleware('permission:manage-currencies')->name('currencies.index'); + Route::post('currencies', [CurrencyController::class, 'store'])->middleware('permission:create-currencies')->name('currencies.store'); + Route::put('currencies/{currency}', [CurrencyController::class, 'update'])->middleware('permission:edit-currencies')->name('currencies.update'); + Route::delete('currencies/{currency}', [CurrencyController::class, 'destroy'])->middleware('permission:delete-currencies')->name('currencies.destroy'); + }); + + // ChatGPT routes + Route::post('api/chatgpt/generate', [ChatGptController::class, 'generate'])->name('chatgpt.generate'); + + // Language management + Route::get('manage-language/{lang?}', [LanguageController::class, 'managePage'])->middleware('permission:manage-language')->name('manage-language'); + Route::get('language/load', [LanguageController::class, 'load'])->name('language.load'); + Route::match(['POST', 'PATCH'], 'language/save', [LanguageController::class, 'save'])->middleware('permission:edit-language')->name('language.save'); + Route::post('languages/change', [LanguageController::class, 'changeLanguage'])->name('languages.change'); + Route::post('/languages/create', [LanguageController::class, 'createLanguage'])->name('languages.create'); + Route::delete('/languages/{languageCode}', [LanguageController::class, 'deleteLanguage'])->name('languages.delete'); + Route::patch('/languages/{languageCode}/toggle', [LanguageController::class, 'toggleLanguageStatus'])->name('languages.toggle'); + Route::post('/languages/{locale}/update', [LanguageController::class, 'updateTranslations'])->name('languages.update'); + + // Landing Page content management (Super Admin only) + Route::middleware('App\Http\Middleware\SuperAdminMiddleware')->group(function () { + Route::get('landing-page/settings', [LandingPageController::class, 'settings'])->name('landing-page.settings'); + Route::post('landing-page/settings', [LandingPageController::class, 'updateSettings'])->name('landing-page.settings.update'); + + // Custom Pages Routes + Route::get('custom-pages', [CustomPageController::class, 'index'])->name('landing-page.custom-pages.index'); + Route::get('custom-pages/create', [CustomPageController::class, 'create'])->name('landing-page.custom-pages.create'); + Route::post('custom-pages', [CustomPageController::class, 'store'])->name('landing-page.custom-pages.store'); + Route::get('custom-pages/{customPage}/edit', [CustomPageController::class, 'edit'])->name('landing-page.custom-pages.edit'); + Route::put('custom-pages/{customPage}', [CustomPageController::class, 'update'])->name('landing-page.custom-pages.update'); + Route::delete('custom-pages/{customPage}', [CustomPageController::class, 'destroy'])->name('landing-page.custom-pages.destroy'); + }); + + // Contact routes + Route::middleware('permission:manage-contacts')->group(function () { + Route::get('contacts', [ContactController::class, 'index'])->name('contacts.index'); + Route::put('contacts/{contact}/update-status', [ContactController::class, 'updateStatus'])->middleware('permission:update-contact-status')->name('contacts.update-status'); + Route::post('contacts/{contact}/send-reply', [ContactController::class, 'sendReply'])->middleware('permission:send-reply-contacts')->name('contacts.send-reply'); + Route::delete('contacts/{contact}', [ContactController::class, 'destroy'])->middleware('permission:delete-contacts')->name('contacts.destroy'); + }); + + // Newsletter routes + Route::middleware('permission:manage-newsletters')->group(function () { + Route::get('newsletters', [NewsletterController::class, 'index'])->name('newsletters.index'); + Route::delete('newsletters/{newsletter}', [NewsletterController::class, 'destroy'])->middleware('permission:delete-newsletters')->name('newsletters.destroy'); + }); + + // Calendar routes + Route::middleware('permission:view-calendar')->group(function () { + Route::get('calendar', [CalendarController::class, 'index'])->name('calendar.index'); + }); + + // Login History routes + Route::middleware('permission:manage-login-history')->group(function () { + Route::get('login-history', [LoginHistoryController::class, 'index'])->middleware('permission:show-login-history')->name('login-history.index'); + Route::delete('login-history/{loginDetail}', [LoginHistoryController::class, 'destroy'])->middleware('permission:delete-login-history')->name('login-history.destroy'); + }); + + // Impersonation routes + Route::middleware('App\Http\Middleware\SuperAdminMiddleware')->group(function () { + Route::get('impersonate/{userId}', [ImpersonateController::class, 'start'])->name('impersonate.start'); + }); + + Route::post('impersonate/leave', [ImpersonateController::class, 'leave'])->name('impersonate.leave'); + }); // End plan.access middleware group +}); + +require __DIR__ . '/settings.php'; +require __DIR__ . '/auth.php'; + +Route::match(['GET', 'POST'], 'payments/easebuzz/success', [EasebuzzPaymentController::class, 'success'])->name('easebuzz.success'); +Route::post('payments/easebuzz/callback', [EasebuzzPaymentController::class, 'callback'])->name('easebuzz.callback'); + +// Cookie consent routes +Route::post('/cookie-consent/store', [CookieConsentController::class, 'store'])->name('cookie.consent.store'); +Route::get('/cookie-consent/download', [CookieConsentController::class, 'download'])->name('cookie.consent.download'); diff --git a/storage/.DS_Store b/storage/.DS_Store new file mode 100755 index 000000000..ef16b6e30 Binary files /dev/null and b/storage/.DS_Store differ diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100755 index 000000000..fedb287fe --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore new file mode 100755 index 000000000..d6b7ef32c --- /dev/null +++ b/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore new file mode 100755 index 000000000..d6b7ef32c --- /dev/null +++ b/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/debugbar/01KNG722S3XTDHQTE4F3PMVJQD.json b/storage/debugbar/01KNG722S3XTDHQTE4F3PMVJQD.json new file mode 100644 index 000000000..e31fe7641 --- /dev/null +++ b/storage/debugbar/01KNG722S3XTDHQTE4F3PMVJQD.json @@ -0,0 +1 @@ +{"__meta":{"id":"01KNG722S3XTDHQTE4F3PMVJQD","datetime":"2026-04-06 01:38:34","utime":1775439514.403635,"method":"GET","uri":"\/install\/database","ip":"127.0.0.1"},"messages":{"count":0,"messages":[]},"time":{"count":4,"start":1775439510.258669,"end":1775439514.403642,"duration":4.144973039627075,"duration_str":"4.14s","measures":[{"label":"Booting","start":1775439510.258669,"relative_start":0,"end":1775439510.308083,"relative_end":1775439510.308083,"duration":0.04941415786743164,"duration_str":"49.41ms","memory":0,"memory_str":"0B","params":[],"collector":"time","group":null},{"label":"Application","start":1775439510.308086,"relative_start":0.04941701889038086,"end":1775439514.403642,"relative_end":0,"duration":4.095556020736694,"duration_str":"4.1s","memory":0,"memory_str":"0B","params":[],"collector":"time","group":null},{"label":"Routing","start":1775439510.318109,"relative_start":0.05944013595581055,"end":1775439510.318615,"relative_end":1775439510.318615,"duration":0.0005059242248535156,"duration_str":"506\u03bcs","memory":0,"memory_str":"0B","params":[],"collector":null,"group":null},{"label":"Preparing Response","start":1775439514.402141,"relative_start":4.143472194671631,"end":1775439514.402218,"relative_end":1775439514.402218,"duration":7.700920104980469e-5,"duration_str":"77\u03bcs","memory":0,"memory_str":"0B","params":[],"collector":null,"group":null}]},"memory":{"peak_usage":49912400,"peak_usage_str":"48MB"},"exceptions":{"count":0,"exceptions":[]},"laravel":{"version":"12.x","tooltip":{"Laravel Version":"12.20.0","PHP Version":"8.4.19","Environment":"local","Debug Mode":"Enabled","URL":"hrm.test","Timezone":"UTC","Locale":"en"}},"views":{"count":0,"nb_templates":0,"templates":[]},"queries":{"count":501,"nb_statements":2736,"nb_visible_statements":501,"nb_excluded_statements":2236,"nb_failed_statements":0,"accumulated_duration":2.240729999999998,"accumulated_duration_str":"2.24s","memory_usage":0,"memory_usage_str":null,"statements":[{"sql":"# Query soft and hard limit for Debugbar are reached. Only the first 100 queries show details. Queries after the first 500 are ignored. Limits can be raised in the config (debugbar.options.db.soft\/hard_limit).","type":"info"},{"sql":"Connection Established","type":"transaction","params":[],"bindings":[],"hints":null,"show_copy":false,"backtrace":[{"index":8,"namespace":null,"name":"vendor\/rachidlaasri\/laravel-installer\/src\/Helpers\/DatabaseManager.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/rachidlaasri\/laravel-installer\/src\/Helpers\/DatabaseManager.php","line":86},{"index":9,"namespace":null,"name":"vendor\/rachidlaasri\/laravel-installer\/src\/Helpers\/DatabaseManager.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/rachidlaasri\/laravel-installer\/src\/Helpers\/DatabaseManager.php","line":23},{"index":10,"namespace":null,"name":"vendor\/rachidlaasri\/laravel-installer\/src\/Controllers\/DatabaseController.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/rachidlaasri\/laravel-installer\/src\/Controllers\/DatabaseController.php","line":30},{"index":11,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Routing\/Controller.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Routing\/Controller.php","line":54},{"index":12,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Routing\/ControllerDispatcher.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Routing\/ControllerDispatcher.php","line":43}],"start":1775439510.324183,"duration":0,"duration_str":"","memory":0,"memory_str":null,"filename":"DatabaseManager.php:86","source":{"index":8,"namespace":null,"name":"vendor\/rachidlaasri\/laravel-installer\/src\/Helpers\/DatabaseManager.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/rachidlaasri\/laravel-installer\/src\/Helpers\/DatabaseManager.php","line":86},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Frachidlaasri%2Flaravel-installer%2Fsrc%2FHelpers%2FDatabaseManager.php&line=86","ajax":false,"filename":"DatabaseManager.php","line":"86"},"connection":"hrm","explain":null,"start_percent":0,"width_percent":0},{"sql":"select exists (select 1 from information_schema.tables where table_schema = schema() and table_name = 'migrations' and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":23,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":24,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":25,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":26,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.357053,"duration":0.009269999999999999,"duration_str":"9.27ms","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":0,"width_percent":0.414},{"sql":"select exists (select 1 from information_schema.tables where table_schema = schema() and table_name = 'migrations' and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":14,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":15,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":16,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":17,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.371066,"duration":0.00136,"duration_str":"1.36ms","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":14,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":0.414,"width_percent":0.061},{"sql":"create table `migrations` (`id` int unsigned not null auto_increment primary key, `migration` varchar(255) not null, `batch` int not null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":14,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":15,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":16,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":17,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.373417,"duration":0.02621,"duration_str":"26.21ms","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":14,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":0.474,"width_percent":1.17},{"sql":"select exists (select 1 from information_schema.tables where table_schema = schema() and table_name = 'migrations' and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":23,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":24,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.400436,"duration":0.0010500000000000002,"duration_str":"1.05ms","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":1.644,"width_percent":0.047},{"sql":"select `migration` from `migrations` order by `batch` asc, `migration` asc","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":23,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":24,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.4035268,"duration":0.00218,"duration_str":"2.18ms","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":1.691,"width_percent":0.097},{"sql":"select `migration` from `migrations` order by `batch` asc, `migration` asc","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":23,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.406241,"duration":0.00014000000000000001,"duration_str":"140\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":1.788,"width_percent":0.006},{"sql":"select max(`batch`) as aggregate from `migrations`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":23,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":24,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":25,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":26,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.418495,"duration":0.0005,"duration_str":"500\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":23,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":1.795,"width_percent":0.022},{"sql":"create table `users` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `email` varchar(255) not null, `email_verified_at` timestamp null, `password` varchar(255) null, `remember_token` varchar(100) null, `lang` varchar(255) null default 'en', `avatar` varchar(255) null, `type` varchar(20) not null default 'company', `created_by` int not null default '0', `mode` varchar(255) not null default 'light', `is_enable_login` int not null default '1', `google2fa_enable` int not null default '0', `google2fa_secret` text null, `status` varchar(255) not null default 'active', `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":15},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.420211,"duration":0.012289999999999999,"duration_str":"12.29ms","memory":0,"memory_str":null,"filename":"0001_01_01_000000_create_users_table.php:15","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":15},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000000_create_users_table.php&line=15","ajax":false,"filename":"0001_01_01_000000_create_users_table.php","line":"15"},"connection":"hrm","explain":null,"start_percent":1.817,"width_percent":0.548},{"sql":"alter table `users` add unique `users_email_unique`(`email`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":15},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.4326038,"duration":0.018510000000000002,"duration_str":"18.51ms","memory":0,"memory_str":null,"filename":"0001_01_01_000000_create_users_table.php:15","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":15},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000000_create_users_table.php&line=15","ajax":false,"filename":"0001_01_01_000000_create_users_table.php","line":"15"},"connection":"hrm","explain":null,"start_percent":2.365,"width_percent":0.826},{"sql":"create table `password_reset_tokens` (`email` varchar(255) not null, `token` varchar(255) not null, `created_at` timestamp null, primary key (`email`)) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":52},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.451283,"duration":0.00425,"duration_str":"4.25ms","memory":0,"memory_str":null,"filename":"0001_01_01_000000_create_users_table.php:52","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":52},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000000_create_users_table.php&line=52","ajax":false,"filename":"0001_01_01_000000_create_users_table.php","line":"52"},"connection":"hrm","explain":null,"start_percent":3.191,"width_percent":0.19},{"sql":"create table `sessions` (`id` varchar(255) not null, `user_id` bigint unsigned null, `ip_address` varchar(45) null, `user_agent` text null, `payload` longtext not null, `last_activity` int not null, primary key (`id`)) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":58},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.45592,"duration":0.00231,"duration_str":"2.31ms","memory":0,"memory_str":null,"filename":"0001_01_01_000000_create_users_table.php:58","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":58},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000000_create_users_table.php&line=58","ajax":false,"filename":"0001_01_01_000000_create_users_table.php","line":"58"},"connection":"hrm","explain":null,"start_percent":3.381,"width_percent":0.103},{"sql":"alter table `sessions` add index `sessions_user_id_index`(`user_id`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":58},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.458333,"duration":0.00537,"duration_str":"5.37ms","memory":0,"memory_str":null,"filename":"0001_01_01_000000_create_users_table.php:58","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":58},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000000_create_users_table.php&line=58","ajax":false,"filename":"0001_01_01_000000_create_users_table.php","line":"58"},"connection":"hrm","explain":null,"start_percent":3.484,"width_percent":0.24},{"sql":"alter table `sessions` add index `sessions_last_activity_index`(`last_activity`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":58},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.463804,"duration":0.00926,"duration_str":"9.26ms","memory":0,"memory_str":null,"filename":"0001_01_01_000000_create_users_table.php:58","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000000_create_users_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000000_create_users_table.php","line":58},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000000_create_users_table.php&line=58","ajax":false,"filename":"0001_01_01_000000_create_users_table.php","line":"58"},"connection":"hrm","explain":null,"start_percent":3.724,"width_percent":0.413},{"sql":"insert into `migrations` (`migration`, `batch`) values ('0001_01_01_000000_create_users_table', 1)","type":"query","params":[],"bindings":["0001_01_01_000000_create_users_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.473635,"duration":0.00051,"duration_str":"510\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":4.137,"width_percent":0.023},{"sql":"create table `cache` (`key` varchar(255) not null, `value` mediumtext not null, `expiration` int not null, primary key (`key`)) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000001_create_cache_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000001_create_cache_table.php","line":14},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.474462,"duration":0.00475,"duration_str":"4.75ms","memory":0,"memory_str":null,"filename":"0001_01_01_000001_create_cache_table.php:14","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000001_create_cache_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000001_create_cache_table.php","line":14},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000001_create_cache_table.php&line=14","ajax":false,"filename":"0001_01_01_000001_create_cache_table.php","line":"14"},"connection":"hrm","explain":null,"start_percent":4.16,"width_percent":0.212},{"sql":"create table `cache_locks` (`key` varchar(255) not null, `owner` varchar(255) not null, `expiration` int not null, primary key (`key`)) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000001_create_cache_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000001_create_cache_table.php","line":20},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.479397,"duration":0.00248,"duration_str":"2.48ms","memory":0,"memory_str":null,"filename":"0001_01_01_000001_create_cache_table.php:20","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000001_create_cache_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000001_create_cache_table.php","line":20},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000001_create_cache_table.php&line=20","ajax":false,"filename":"0001_01_01_000001_create_cache_table.php","line":"20"},"connection":"hrm","explain":null,"start_percent":4.372,"width_percent":0.111},{"sql":"insert into `migrations` (`migration`, `batch`) values ('0001_01_01_000001_create_cache_table', 1)","type":"query","params":[],"bindings":["0001_01_01_000001_create_cache_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.482159,"duration":0.00073,"duration_str":"730\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":4.482,"width_percent":0.033},{"sql":"create table `jobs` (`id` bigint unsigned not null auto_increment primary key, `queue` varchar(255) not null, `payload` longtext not null, `attempts` tinyint unsigned not null, `reserved_at` int unsigned null, `available_at` int unsigned not null, `created_at` int unsigned not null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":14},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.4832509,"duration":0.00219,"duration_str":"2.19ms","memory":0,"memory_str":null,"filename":"0001_01_01_000002_create_jobs_table.php:14","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":14},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000002_create_jobs_table.php&line=14","ajax":false,"filename":"0001_01_01_000002_create_jobs_table.php","line":"14"},"connection":"hrm","explain":null,"start_percent":4.515,"width_percent":0.098},{"sql":"alter table `jobs` add index `jobs_queue_index`(`queue`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":14},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.485548,"duration":0.0051600000000000005,"duration_str":"5.16ms","memory":0,"memory_str":null,"filename":"0001_01_01_000002_create_jobs_table.php:14","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":14},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000002_create_jobs_table.php&line=14","ajax":false,"filename":"0001_01_01_000002_create_jobs_table.php","line":"14"},"connection":"hrm","explain":null,"start_percent":4.613,"width_percent":0.23},{"sql":"create table `job_batches` (`id` varchar(255) not null, `name` varchar(255) not null, `total_jobs` int not null, `pending_jobs` int not null, `failed_jobs` int not null, `failed_job_ids` longtext not null, `options` mediumtext null, `cancelled_at` int null, `created_at` int not null, `finished_at` int null, primary key (`id`)) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":24},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.490978,"duration":0.004200000000000001,"duration_str":"4.2ms","memory":0,"memory_str":null,"filename":"0001_01_01_000002_create_jobs_table.php:24","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":24},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000002_create_jobs_table.php&line=24","ajax":false,"filename":"0001_01_01_000002_create_jobs_table.php","line":"24"},"connection":"hrm","explain":null,"start_percent":4.843,"width_percent":0.187},{"sql":"create table `failed_jobs` (`id` bigint unsigned not null auto_increment primary key, `uuid` varchar(255) not null, `connection` text not null, `queue` text not null, `payload` longtext not null, `exception` longtext not null, `failed_at` timestamp not null default CURRENT_TIMESTAMP) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":37},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.495824,"duration":0.00413,"duration_str":"4.13ms","memory":0,"memory_str":null,"filename":"0001_01_01_000002_create_jobs_table.php:37","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":37},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000002_create_jobs_table.php&line=37","ajax":false,"filename":"0001_01_01_000002_create_jobs_table.php","line":"37"},"connection":"hrm","explain":null,"start_percent":5.031,"width_percent":0.184},{"sql":"alter table `failed_jobs` add unique `failed_jobs_uuid_unique`(`uuid`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":37},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.500095,"duration":0.00464,"duration_str":"4.64ms","memory":0,"memory_str":null,"filename":"0001_01_01_000002_create_jobs_table.php:37","source":{"index":13,"namespace":null,"name":"database\/migrations\/0001_01_01_000002_create_jobs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/0001_01_01_000002_create_jobs_table.php","line":37},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F0001_01_01_000002_create_jobs_table.php&line=37","ajax":false,"filename":"0001_01_01_000002_create_jobs_table.php","line":"37"},"connection":"hrm","explain":null,"start_percent":5.215,"width_percent":0.207},{"sql":"insert into `migrations` (`migration`, `batch`) values ('0001_01_01_000002_create_jobs_table', 1)","type":"query","params":[],"bindings":["0001_01_01_000002_create_jobs_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.505033,"duration":0.00055,"duration_str":"550\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":5.422,"width_percent":0.025},{"sql":"create table `leave_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `max_days_per_year` int not null default '0', `is_paid` tinyint(1) not null default '1', `color` varchar(7) not null default '#3B82F6', `status` enum('active', 'inactive') not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000001_create_leave_types_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000001_create_leave_types_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.5062401,"duration":0.0034300000000000003,"duration_str":"3.43ms","memory":0,"memory_str":null,"filename":"2024_01_15_000001_create_leave_types_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000001_create_leave_types_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000001_create_leave_types_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000001_create_leave_types_table.php&line=11","ajax":false,"filename":"2024_01_15_000001_create_leave_types_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":5.446,"width_percent":0.153},{"sql":"alter table `leave_types` add constraint `leave_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000001_create_leave_types_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000001_create_leave_types_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.510004,"duration":0.03095,"duration_str":"30.95ms","memory":0,"memory_str":null,"filename":"2024_01_15_000001_create_leave_types_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000001_create_leave_types_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000001_create_leave_types_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000001_create_leave_types_table.php&line=11","ajax":false,"filename":"2024_01_15_000001_create_leave_types_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":5.6,"width_percent":1.381},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000001_create_leave_types_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000001_create_leave_types_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.541189,"duration":0.00037,"duration_str":"370\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":6.981,"width_percent":0.017},{"sql":"create table `leave_policies` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `leave_type_id` bigint unsigned not null, `accrual_type` enum('monthly', 'yearly') not null default 'yearly', `accrual_rate` decimal(8, 2) not null default '0', `carry_forward_limit` int not null default '0', `min_days_per_application` int not null default '1', `max_days_per_application` int not null default '30', `requires_approval` tinyint(1) not null default '1', `status` enum('active', 'inactive') not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.5419798,"duration":0.00272,"duration_str":"2.72ms","memory":0,"memory_str":null,"filename":"2024_01_15_000002_create_leave_policies_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000002_create_leave_policies_table.php&line=11","ajax":false,"filename":"2024_01_15_000002_create_leave_policies_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":6.997,"width_percent":0.121},{"sql":"alter table `leave_policies` add constraint `leave_policies_leave_type_id_foreign` foreign key (`leave_type_id`) references `leave_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.5448,"duration":0.01084,"duration_str":"10.84ms","memory":0,"memory_str":null,"filename":"2024_01_15_000002_create_leave_policies_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000002_create_leave_policies_table.php&line=11","ajax":false,"filename":"2024_01_15_000002_create_leave_policies_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":7.119,"width_percent":0.484},{"sql":"alter table `leave_policies` add constraint `leave_policies_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.5557208,"duration":0.01007,"duration_str":"10.07ms","memory":0,"memory_str":null,"filename":"2024_01_15_000002_create_leave_policies_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000002_create_leave_policies_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000002_create_leave_policies_table.php&line=11","ajax":false,"filename":"2024_01_15_000002_create_leave_policies_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":7.602,"width_percent":0.449},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000002_create_leave_policies_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000002_create_leave_policies_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.565943,"duration":0.00044,"duration_str":"440\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":8.052,"width_percent":0.02},{"sql":"create table `leave_applications` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `leave_type_id` bigint unsigned not null, `leave_policy_id` bigint unsigned not null, `start_date` date not null, `end_date` date not null, `total_days` int not null, `reason` text not null, `attachment` varchar(255) null, `status` enum('pending', 'approved', 'rejected') not null default 'pending', `manager_comments` text null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.566743,"duration":0.00726,"duration_str":"7.26ms","memory":0,"memory_str":null,"filename":"2024_01_15_000003_create_leave_applications_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000003_create_leave_applications_table.php&line=11","ajax":false,"filename":"2024_01_15_000003_create_leave_applications_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":8.071,"width_percent":0.324},{"sql":"alter table `leave_applications` add constraint `leave_applications_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.574222,"duration":0.00962,"duration_str":"9.62ms","memory":0,"memory_str":null,"filename":"2024_01_15_000003_create_leave_applications_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000003_create_leave_applications_table.php&line=11","ajax":false,"filename":"2024_01_15_000003_create_leave_applications_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":8.395,"width_percent":0.429},{"sql":"alter table `leave_applications` add constraint `leave_applications_leave_type_id_foreign` foreign key (`leave_type_id`) references `leave_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.584036,"duration":0.00877,"duration_str":"8.77ms","memory":0,"memory_str":null,"filename":"2024_01_15_000003_create_leave_applications_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000003_create_leave_applications_table.php&line=11","ajax":false,"filename":"2024_01_15_000003_create_leave_applications_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":8.825,"width_percent":0.391},{"sql":"alter table `leave_applications` add constraint `leave_applications_leave_policy_id_foreign` foreign key (`leave_policy_id`) references `leave_policies` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.593043,"duration":0.009630000000000001,"duration_str":"9.63ms","memory":0,"memory_str":null,"filename":"2024_01_15_000003_create_leave_applications_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000003_create_leave_applications_table.php&line=11","ajax":false,"filename":"2024_01_15_000003_create_leave_applications_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":9.216,"width_percent":0.43},{"sql":"alter table `leave_applications` add constraint `leave_applications_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.6031048,"duration":0.010060000000000001,"duration_str":"10.06ms","memory":0,"memory_str":null,"filename":"2024_01_15_000003_create_leave_applications_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000003_create_leave_applications_table.php&line=11","ajax":false,"filename":"2024_01_15_000003_create_leave_applications_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":9.646,"width_percent":0.449},{"sql":"alter table `leave_applications` add constraint `leave_applications_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.613698,"duration":0.012369999999999999,"duration_str":"12.37ms","memory":0,"memory_str":null,"filename":"2024_01_15_000003_create_leave_applications_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000003_create_leave_applications_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000003_create_leave_applications_table.php&line=11","ajax":false,"filename":"2024_01_15_000003_create_leave_applications_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":10.095,"width_percent":0.552},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000003_create_leave_applications_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000003_create_leave_applications_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.6267278,"duration":0.00026000000000000003,"duration_str":"260\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":10.647,"width_percent":0.012},{"sql":"create table `leave_balances` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `leave_type_id` bigint unsigned not null, `leave_policy_id` bigint unsigned not null, `year` year not null, `allocated_days` decimal(8, 2) not null default '0', `used_days` decimal(8, 2) not null default '0', `remaining_days` decimal(8, 2) not null default '0', `carried_forward` decimal(8, 2) not null default '0', `manual_adjustment` decimal(8, 2) not null default '0', `adjustment_reason` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.628711,"duration":0.0042699999999999995,"duration_str":"4.27ms","memory":0,"memory_str":null,"filename":"2024_01_15_000004_create_leave_balances_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000004_create_leave_balances_table.php&line=11","ajax":false,"filename":"2024_01_15_000004_create_leave_balances_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":10.659,"width_percent":0.191},{"sql":"alter table `leave_balances` add constraint `leave_balances_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.633493,"duration":0.00756,"duration_str":"7.56ms","memory":0,"memory_str":null,"filename":"2024_01_15_000004_create_leave_balances_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000004_create_leave_balances_table.php&line=11","ajax":false,"filename":"2024_01_15_000004_create_leave_balances_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":10.849,"width_percent":0.337},{"sql":"alter table `leave_balances` add constraint `leave_balances_leave_type_id_foreign` foreign key (`leave_type_id`) references `leave_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.641541,"duration":0.00828,"duration_str":"8.28ms","memory":0,"memory_str":null,"filename":"2024_01_15_000004_create_leave_balances_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000004_create_leave_balances_table.php&line=11","ajax":false,"filename":"2024_01_15_000004_create_leave_balances_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":11.187,"width_percent":0.37},{"sql":"alter table `leave_balances` add constraint `leave_balances_leave_policy_id_foreign` foreign key (`leave_policy_id`) references `leave_policies` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.650301,"duration":0.00843,"duration_str":"8.43ms","memory":0,"memory_str":null,"filename":"2024_01_15_000004_create_leave_balances_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000004_create_leave_balances_table.php&line=11","ajax":false,"filename":"2024_01_15_000004_create_leave_balances_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":11.556,"width_percent":0.376},{"sql":"alter table `leave_balances` add constraint `leave_balances_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.659205,"duration":0.00788,"duration_str":"7.88ms","memory":0,"memory_str":null,"filename":"2024_01_15_000004_create_leave_balances_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000004_create_leave_balances_table.php&line=11","ajax":false,"filename":"2024_01_15_000004_create_leave_balances_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":11.932,"width_percent":0.352},{"sql":"alter table `leave_balances` add unique `leave_balances_employee_id_leave_type_id_year_unique`(`employee_id`, `leave_type_id`, `year`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.667545,"duration":0.00256,"duration_str":"2.56ms","memory":0,"memory_str":null,"filename":"2024_01_15_000004_create_leave_balances_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000004_create_leave_balances_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000004_create_leave_balances_table.php&line=11","ajax":false,"filename":"2024_01_15_000004_create_leave_balances_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":12.284,"width_percent":0.114},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000004_create_leave_balances_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000004_create_leave_balances_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.6712282,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":12.398,"width_percent":0.013},{"sql":"create table `shifts` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `start_time` time not null, `end_time` time not null, `break_duration` int not null default '0', `break_start_time` time null, `break_end_time` time null, `grace_period` int not null default '0', `is_night_shift` tinyint(1) not null default '0', `status` enum('active', 'inactive') not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000005_create_shifts_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000005_create_shifts_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.6736271,"duration":0.0023799999999999997,"duration_str":"2.38ms","memory":0,"memory_str":null,"filename":"2024_01_15_000005_create_shifts_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000005_create_shifts_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000005_create_shifts_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000005_create_shifts_table.php&line=11","ajax":false,"filename":"2024_01_15_000005_create_shifts_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":12.411,"width_percent":0.106},{"sql":"alter table `shifts` add constraint `shifts_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000005_create_shifts_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000005_create_shifts_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.6764522,"duration":0.01626,"duration_str":"16.26ms","memory":0,"memory_str":null,"filename":"2024_01_15_000005_create_shifts_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000005_create_shifts_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000005_create_shifts_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000005_create_shifts_table.php&line=11","ajax":false,"filename":"2024_01_15_000005_create_shifts_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":12.517,"width_percent":0.726},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000005_create_shifts_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000005_create_shifts_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.694009,"duration":0.00276,"duration_str":"2.76ms","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":13.243,"width_percent":0.123},{"sql":"create table `attendance_policies` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `late_arrival_grace` int not null default '15', `early_departure_grace` int not null default '15', `half_day_threshold` decimal(5, 2) not null default '4', `overtime_rate_per_hour` decimal(8, 2) not null default '150', `status` enum('active', 'inactive') not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000006_create_attendance_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000006_create_attendance_policies_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.6989238,"duration":0.0023599999999999997,"duration_str":"2.36ms","memory":0,"memory_str":null,"filename":"2024_01_15_000006_create_attendance_policies_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000006_create_attendance_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000006_create_attendance_policies_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000006_create_attendance_policies_table.php&line=11","ajax":false,"filename":"2024_01_15_000006_create_attendance_policies_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":13.366,"width_percent":0.105},{"sql":"alter table `attendance_policies` add constraint `attendance_policies_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000006_create_attendance_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000006_create_attendance_policies_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.701664,"duration":0.00675,"duration_str":"6.75ms","memory":0,"memory_str":null,"filename":"2024_01_15_000006_create_attendance_policies_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000006_create_attendance_policies_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000006_create_attendance_policies_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000006_create_attendance_policies_table.php&line=11","ajax":false,"filename":"2024_01_15_000006_create_attendance_policies_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":13.472,"width_percent":0.301},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000006_create_attendance_policies_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000006_create_attendance_policies_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.7094798,"duration":0.00061,"duration_str":"610\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":13.773,"width_percent":0.027},{"sql":"create table `attendance_records` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `shift_id` bigint unsigned null, `attendance_policy_id` bigint unsigned null, `date` date not null, `clock_in` time null, `clock_out` time null, `total_hours` decimal(5, 2) not null default '0', `break_hours` decimal(5, 2) not null default '0', `overtime_hours` decimal(5, 2) not null default '0', `overtime_amount` decimal(8, 2) not null default '0', `is_late` tinyint(1) not null default '0', `is_early_departure` tinyint(1) not null default '0', `is_absent` tinyint(1) not null default '0', `is_holiday` tinyint(1) not null default '0', `is_weekend` tinyint(1) not null default '0', `status` enum('present', 'absent', 'half_day', 'on_leave', 'holiday') not null default 'present', `notes` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.7125602,"duration":0.00204,"duration_str":"2.04ms","memory":0,"memory_str":null,"filename":"2024_01_15_000007_create_attendance_records_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000007_create_attendance_records_table.php&line=11","ajax":false,"filename":"2024_01_15_000007_create_attendance_records_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":13.8,"width_percent":0.091},{"sql":"alter table `attendance_records` add constraint `attendance_records_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.715161,"duration":0.00907,"duration_str":"9.07ms","memory":0,"memory_str":null,"filename":"2024_01_15_000007_create_attendance_records_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000007_create_attendance_records_table.php&line=11","ajax":false,"filename":"2024_01_15_000007_create_attendance_records_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":13.891,"width_percent":0.405},{"sql":"alter table `attendance_records` add constraint `attendance_records_shift_id_foreign` foreign key (`shift_id`) references `shifts` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.724726,"duration":0.00711,"duration_str":"7.11ms","memory":0,"memory_str":null,"filename":"2024_01_15_000007_create_attendance_records_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000007_create_attendance_records_table.php&line=11","ajax":false,"filename":"2024_01_15_000007_create_attendance_records_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":14.296,"width_percent":0.317},{"sql":"alter table `attendance_records` add constraint `attendance_records_attendance_policy_id_foreign` foreign key (`attendance_policy_id`) references `attendance_policies` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.732184,"duration":0.0055,"duration_str":"5.5ms","memory":0,"memory_str":null,"filename":"2024_01_15_000007_create_attendance_records_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000007_create_attendance_records_table.php&line=11","ajax":false,"filename":"2024_01_15_000007_create_attendance_records_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":14.613,"width_percent":0.245},{"sql":"alter table `attendance_records` add constraint `attendance_records_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.7381861,"duration":0.007690000000000001,"duration_str":"7.69ms","memory":0,"memory_str":null,"filename":"2024_01_15_000007_create_attendance_records_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000007_create_attendance_records_table.php&line=11","ajax":false,"filename":"2024_01_15_000007_create_attendance_records_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":14.859,"width_percent":0.343},{"sql":"alter table `attendance_records` add unique `attendance_records_employee_id_date_unique`(`employee_id`, `date`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.746407,"duration":0.00441,"duration_str":"4.41ms","memory":0,"memory_str":null,"filename":"2024_01_15_000007_create_attendance_records_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000007_create_attendance_records_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000007_create_attendance_records_table.php&line=11","ajax":false,"filename":"2024_01_15_000007_create_attendance_records_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":15.202,"width_percent":0.197},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000007_create_attendance_records_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000007_create_attendance_records_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.752067,"duration":0.00035,"duration_str":"350\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":15.399,"width_percent":0.016},{"sql":"create table `attendance_regularizations` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `attendance_record_id` bigint unsigned not null, `date` date not null, `requested_clock_in` time null, `requested_clock_out` time null, `original_clock_in` time null, `original_clock_out` time null, `reason` text not null, `status` enum('pending', 'approved', 'rejected') not null default 'pending', `manager_comments` text null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.755002,"duration":0.0019399999999999999,"duration_str":"1.94ms","memory":0,"memory_str":null,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000008_create_attendance_regularizations_table.php&line=11","ajax":false,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":15.414,"width_percent":0.087},{"sql":"alter table `attendance_regularizations` add constraint `attendance_regularizations_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.757324,"duration":0.0050999999999999995,"duration_str":"5.1ms","memory":0,"memory_str":null,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000008_create_attendance_regularizations_table.php&line=11","ajax":false,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":15.501,"width_percent":0.228},{"sql":"alter table `attendance_regularizations` add constraint `attendance_regularizations_attendance_record_id_foreign` foreign key (`attendance_record_id`) references `attendance_records` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.762723,"duration":0.005730000000000001,"duration_str":"5.73ms","memory":0,"memory_str":null,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000008_create_attendance_regularizations_table.php&line=11","ajax":false,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":15.728,"width_percent":0.256},{"sql":"alter table `attendance_regularizations` add constraint `attendance_regularizations_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.768748,"duration":0.00739,"duration_str":"7.39ms","memory":0,"memory_str":null,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000008_create_attendance_regularizations_table.php&line=11","ajax":false,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":15.984,"width_percent":0.33},{"sql":"alter table `attendance_regularizations` add constraint `attendance_regularizations_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.776675,"duration":0.00731,"duration_str":"7.31ms","memory":0,"memory_str":null,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000008_create_attendance_regularizations_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000008_create_attendance_regularizations_table.php&line=11","ajax":false,"filename":"2024_01_15_000008_create_attendance_regularizations_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":16.314,"width_percent":0.326},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000008_create_attendance_regularizations_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000008_create_attendance_regularizations_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.784887,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":16.64,"width_percent":0.013},{"sql":"create table `time_entries` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `date` date not null, `hours` decimal(5, 2) not null, `description` text not null, `project` varchar(255) null, `status` enum('pending', 'approved', 'rejected') not null default 'pending', `manager_comments` text null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000009_create_time_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000009_create_time_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.786927,"duration":0.00156,"duration_str":"1.56ms","memory":0,"memory_str":null,"filename":"2024_01_15_000009_create_time_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000009_create_time_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000009_create_time_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000009_create_time_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000009_create_time_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":16.653,"width_percent":0.07},{"sql":"alter table `time_entries` add constraint `time_entries_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000009_create_time_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000009_create_time_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.788871,"duration":0.00571,"duration_str":"5.71ms","memory":0,"memory_str":null,"filename":"2024_01_15_000009_create_time_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000009_create_time_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000009_create_time_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000009_create_time_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000009_create_time_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":16.723,"width_percent":0.255},{"sql":"alter table `time_entries` add constraint `time_entries_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000009_create_time_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000009_create_time_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.7948909,"duration":0.0052699999999999995,"duration_str":"5.27ms","memory":0,"memory_str":null,"filename":"2024_01_15_000009_create_time_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000009_create_time_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000009_create_time_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000009_create_time_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000009_create_time_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":16.978,"width_percent":0.235},{"sql":"alter table `time_entries` add constraint `time_entries_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000009_create_time_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000009_create_time_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.800483,"duration":0.0056500000000000005,"duration_str":"5.65ms","memory":0,"memory_str":null,"filename":"2024_01_15_000009_create_time_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000009_create_time_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000009_create_time_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000009_create_time_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000009_create_time_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":17.213,"width_percent":0.252},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000009_create_time_entries_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000009_create_time_entries_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.807343,"duration":0.0005899999999999999,"duration_str":"590\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":17.465,"width_percent":0.026},{"sql":"create table `salary_components` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `type` enum('earning', 'deduction') not null default 'earning', `calculation_type` enum('fixed', 'percentage') not null default 'fixed', `default_amount` decimal(10, 2) not null default '0', `percentage_of_basic` decimal(5, 2) null, `is_taxable` tinyint(1) not null default '1', `is_mandatory` tinyint(1) not null default '0', `status` enum('active', 'inactive') not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000010_create_salary_components_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000010_create_salary_components_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.809505,"duration":0.00171,"duration_str":"1.71ms","memory":0,"memory_str":null,"filename":"2024_01_15_000010_create_salary_components_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000010_create_salary_components_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000010_create_salary_components_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000010_create_salary_components_table.php&line=11","ajax":false,"filename":"2024_01_15_000010_create_salary_components_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":17.491,"width_percent":0.076},{"sql":"alter table `salary_components` add constraint `salary_components_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000010_create_salary_components_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000010_create_salary_components_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.811514,"duration":0.00583,"duration_str":"5.83ms","memory":0,"memory_str":null,"filename":"2024_01_15_000010_create_salary_components_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000010_create_salary_components_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000010_create_salary_components_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000010_create_salary_components_table.php&line=11","ajax":false,"filename":"2024_01_15_000010_create_salary_components_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":17.567,"width_percent":0.26},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000010_create_salary_components_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000010_create_salary_components_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.8179898,"duration":0.00023,"duration_str":"230\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":17.828,"width_percent":0.01},{"sql":"create table `employee_salaries` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `basic_salary` decimal(10, 2) null, `components` json null, `is_active` tinyint(1) not null default '1', `calculation_status` enum('pending', 'calculated') not null default 'pending', `notes` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.8191252,"duration":0.0015,"duration_str":"1.5ms","memory":0,"memory_str":null,"filename":"2024_01_15_000011_create_employee_salaries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000011_create_employee_salaries_table.php&line=11","ajax":false,"filename":"2024_01_15_000011_create_employee_salaries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":17.838,"width_percent":0.067},{"sql":"alter table `employee_salaries` add constraint `employee_salaries_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.820857,"duration":0.0076,"duration_str":"7.6ms","memory":0,"memory_str":null,"filename":"2024_01_15_000011_create_employee_salaries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000011_create_employee_salaries_table.php&line=11","ajax":false,"filename":"2024_01_15_000011_create_employee_salaries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":17.905,"width_percent":0.339},{"sql":"alter table `employee_salaries` add constraint `employee_salaries_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.8287451,"duration":0.00626,"duration_str":"6.26ms","memory":0,"memory_str":null,"filename":"2024_01_15_000011_create_employee_salaries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000011_create_employee_salaries_table.php&line=11","ajax":false,"filename":"2024_01_15_000011_create_employee_salaries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":18.244,"width_percent":0.279},{"sql":"alter table `employee_salaries` add unique `employee_salaries_employee_id_unique`(`employee_id`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.835342,"duration":0.00213,"duration_str":"2.13ms","memory":0,"memory_str":null,"filename":"2024_01_15_000011_create_employee_salaries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000011_create_employee_salaries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000011_create_employee_salaries_table.php&line=11","ajax":false,"filename":"2024_01_15_000011_create_employee_salaries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":18.523,"width_percent":0.095},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000011_create_employee_salaries_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000011_create_employee_salaries_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.838147,"duration":0.00032,"duration_str":"320\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":18.618,"width_percent":0.014},{"sql":"create table `payroll_runs` (`id` bigint unsigned not null auto_increment primary key, `title` varchar(255) not null, `payroll_frequency` enum('weekly', 'biweekly', 'monthly') not null default 'monthly', `pay_period_start` date not null, `pay_period_end` date not null, `pay_date` date not null, `total_gross_pay` decimal(12, 2) not null default '0', `total_deductions` decimal(12, 2) not null default '0', `total_net_pay` decimal(12, 2) not null default '0', `employee_count` int not null default '0', `status` enum('draft', 'processing', 'completed', 'cancelled') not null default 'draft', `notes` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000012_create_payroll_runs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000012_create_payroll_runs_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.839641,"duration":0.00262,"duration_str":"2.62ms","memory":0,"memory_str":null,"filename":"2024_01_15_000012_create_payroll_runs_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000012_create_payroll_runs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000012_create_payroll_runs_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000012_create_payroll_runs_table.php&line=11","ajax":false,"filename":"2024_01_15_000012_create_payroll_runs_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":18.633,"width_percent":0.117},{"sql":"alter table `payroll_runs` add constraint `payroll_runs_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000012_create_payroll_runs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000012_create_payroll_runs_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.8425848,"duration":0.006900000000000001,"duration_str":"6.9ms","memory":0,"memory_str":null,"filename":"2024_01_15_000012_create_payroll_runs_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000012_create_payroll_runs_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000012_create_payroll_runs_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000012_create_payroll_runs_table.php&line=11","ajax":false,"filename":"2024_01_15_000012_create_payroll_runs_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":18.75,"width_percent":0.308},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000012_create_payroll_runs_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000012_create_payroll_runs_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.850031,"duration":0.00022,"duration_str":"220\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":19.058,"width_percent":0.01},{"sql":"create table `payroll_entries` (`id` bigint unsigned not null auto_increment primary key, `payroll_run_id` bigint unsigned not null, `employee_id` bigint unsigned not null, `basic_salary` decimal(10, 2) not null default '0', `component_earnings` decimal(10, 2) not null default '0', `total_earnings` decimal(10, 2) not null default '0', `total_deductions` decimal(10, 2) not null default '0', `gross_pay` decimal(10, 2) not null default '0', `net_pay` decimal(10, 2) not null default '0', `overtime_amount` decimal(10, 2) not null default '0', `per_day_salary` decimal(10, 2) not null default '0', `unpaid_leave_deduction` decimal(10, 2) not null default '0', `working_days` int not null default '0', `present_days` decimal(5, 2) not null default '0', `full_present_days` int not null default '0', `half_days` decimal(5, 2) not null default '0', `holiday_days` int not null default '0', `paid_leave_days` decimal(5, 2) not null default '0', `unpaid_leave_days` decimal(5, 2) not null default '0', `absent_days` int not null default '0', `overtime_hours` decimal(5, 2) not null default '0', `earnings_breakdown` json null, `deductions_breakdown` json null, `notes` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.8516161,"duration":0.00504,"duration_str":"5.04ms","memory":0,"memory_str":null,"filename":"2024_01_15_000013_create_payroll_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000013_create_payroll_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000013_create_payroll_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":19.067,"width_percent":0.225},{"sql":"alter table `payroll_entries` add constraint `payroll_entries_payroll_run_id_foreign` foreign key (`payroll_run_id`) references `payroll_runs` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.85692,"duration":0.03576,"duration_str":"35.76ms","memory":0,"memory_str":null,"filename":"2024_01_15_000013_create_payroll_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000013_create_payroll_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000013_create_payroll_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":19.292,"width_percent":1.596},{"sql":"alter table `payroll_entries` add constraint `payroll_entries_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.892991,"duration":0.00607,"duration_str":"6.07ms","memory":0,"memory_str":null,"filename":"2024_01_15_000013_create_payroll_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000013_create_payroll_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000013_create_payroll_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":20.888,"width_percent":0.271},{"sql":"alter table `payroll_entries` add constraint `payroll_entries_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.8993192,"duration":0.00975,"duration_str":"9.75ms","memory":0,"memory_str":null,"filename":"2024_01_15_000013_create_payroll_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000013_create_payroll_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000013_create_payroll_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":21.159,"width_percent":0.435},{"sql":"alter table `payroll_entries` add unique `payroll_entries_payroll_run_id_employee_id_unique`(`payroll_run_id`, `employee_id`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.909352,"duration":0.0035499999999999998,"duration_str":"3.55ms","memory":0,"memory_str":null,"filename":"2024_01_15_000013_create_payroll_entries_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000013_create_payroll_entries_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000013_create_payroll_entries_table.php&line=11","ajax":false,"filename":"2024_01_15_000013_create_payroll_entries_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":21.594,"width_percent":0.158},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000013_create_payroll_entries_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000013_create_payroll_entries_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.91346,"duration":0.00027,"duration_str":"270\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":21.753,"width_percent":0.012},{"sql":"create table `payslips` (`id` bigint unsigned not null auto_increment primary key, `payroll_entry_id` bigint unsigned not null, `employee_id` bigint unsigned not null, `payslip_number` varchar(255) not null, `pay_period_start` date not null, `pay_period_end` date not null, `pay_date` date not null, `file_path` varchar(255) null, `status` enum('generated', 'sent', 'downloaded') not null default 'generated', `sent_at` timestamp null, `downloaded_at` timestamp null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.914771,"duration":0.00318,"duration_str":"3.18ms","memory":0,"memory_str":null,"filename":"2024_01_15_000014_create_payslips_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000014_create_payslips_table.php&line=11","ajax":false,"filename":"2024_01_15_000014_create_payslips_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":21.765,"width_percent":0.142},{"sql":"alter table `payslips` add constraint `payslips_payroll_entry_id_foreign` foreign key (`payroll_entry_id`) references `payroll_entries` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.918221,"duration":0.00519,"duration_str":"5.19ms","memory":0,"memory_str":null,"filename":"2024_01_15_000014_create_payslips_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000014_create_payslips_table.php&line=11","ajax":false,"filename":"2024_01_15_000014_create_payslips_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":21.907,"width_percent":0.232},{"sql":"alter table `payslips` add constraint `payslips_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.923697,"duration":0.00788,"duration_str":"7.88ms","memory":0,"memory_str":null,"filename":"2024_01_15_000014_create_payslips_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000014_create_payslips_table.php&line=11","ajax":false,"filename":"2024_01_15_000014_create_payslips_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":22.138,"width_percent":0.352},{"sql":"alter table `payslips` add constraint `payslips_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.931928,"duration":0.00684,"duration_str":"6.84ms","memory":0,"memory_str":null,"filename":"2024_01_15_000014_create_payslips_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000014_create_payslips_table.php&line=11","ajax":false,"filename":"2024_01_15_000014_create_payslips_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":22.49,"width_percent":0.305},{"sql":"alter table `payslips` add unique `payslips_payslip_number_unique`(`payslip_number`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.939299,"duration":0.0019199999999999998,"duration_str":"1.92ms","memory":0,"memory_str":null,"filename":"2024_01_15_000014_create_payslips_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2024_01_15_000014_create_payslips_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2024_01_15_000014_create_payslips_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2024_01_15_000014_create_payslips_table.php&line=11","ajax":false,"filename":"2024_01_15_000014_create_payslips_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":22.795,"width_percent":0.086},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2024_01_15_000014_create_payslips_table', 1)","type":"query","params":[],"bindings":["2024_01_15_000014_create_payslips_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.941838,"duration":0.0005600000000000001,"duration_str":"560\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":22.881,"width_percent":0.025},{"sql":"create table `landing_page_settings` (`id` bigint unsigned not null auto_increment primary key, `company_name` varchar(255) null default 'HRM', `contact_email` varchar(255) null default 'support@hrm.com', `contact_phone` varchar(255) null default '+1 (555) 123-4567', `contact_address` varchar(255) null default 'San Francisco, CA', `config_sections` json null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2025_01_27_084150_create_landing_page_settings_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_01_27_084150_create_landing_page_settings_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.943309,"duration":0.0033399999999999997,"duration_str":"3.34ms","memory":0,"memory_str":null,"filename":"2025_01_27_084150_create_landing_page_settings_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2025_01_27_084150_create_landing_page_settings_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_01_27_084150_create_landing_page_settings_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2025_01_27_084150_create_landing_page_settings_table.php&line=11","ajax":false,"filename":"2025_01_27_084150_create_landing_page_settings_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":22.906,"width_percent":0.149},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2025_01_27_084150_create_landing_page_settings_table', 1)","type":"query","params":[],"bindings":["2025_01_27_084150_create_landing_page_settings_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.947009,"duration":0.00011999999999999999,"duration_str":"120\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":23.055,"width_percent":0.005},{"sql":"create table `webhooks` (`id` bigint unsigned not null auto_increment primary key, `user_id` bigint unsigned not null, `module` enum('New User', 'New Appointment') not null, `method` enum('GET', 'POST') not null, `url` varchar(255) not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2025_01_28_000001_create_webhooks_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_01_28_000001_create_webhooks_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.947777,"duration":0.00195,"duration_str":"1.95ms","memory":0,"memory_str":null,"filename":"2025_01_28_000001_create_webhooks_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2025_01_28_000001_create_webhooks_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_01_28_000001_create_webhooks_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2025_01_28_000001_create_webhooks_table.php&line=11","ajax":false,"filename":"2025_01_28_000001_create_webhooks_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":23.06,"width_percent":0.087},{"sql":"alter table `webhooks` add constraint `webhooks_user_id_foreign` foreign key (`user_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2025_01_28_000001_create_webhooks_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_01_28_000001_create_webhooks_table.php","line":11},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.9499488,"duration":0.00394,"duration_str":"3.94ms","memory":0,"memory_str":null,"filename":"2025_01_28_000001_create_webhooks_table.php:11","source":{"index":13,"namespace":null,"name":"database\/migrations\/2025_01_28_000001_create_webhooks_table.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_01_28_000001_create_webhooks_table.php","line":11},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2025_01_28_000001_create_webhooks_table.php&line=11","ajax":false,"filename":"2025_01_28_000001_create_webhooks_table.php","line":"11"},"connection":"hrm","explain":null,"start_percent":23.147,"width_percent":0.176},{"sql":"insert into `migrations` (`migration`, `batch`) values ('2025_01_28_000001_create_webhooks_table', 1)","type":"query","params":[],"bindings":["2025_01_28_000001_create_webhooks_table",1],"hints":null,"show_copy":true,"backtrace":[{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":19,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":20,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96},{"index":21,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":35},{"index":22,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Container.php","line":754}],"start":1775439510.954454,"duration":0.00022,"duration_str":"220\u03bcs","memory":0,"memory_str":null,"filename":"BoundMethod.php:36","source":{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Flaravel%2Fframework%2Fsrc%2FIlluminate%2FContainer%2FBoundMethod.php&line=36","ajax":false,"filename":"BoundMethod.php","line":"36"},"connection":"hrm","explain":null,"start_percent":23.323,"width_percent":0.01},{"sql":"create table `permissions` (`id` bigint unsigned not null auto_increment primary key, `module` varchar(255) null, `name` varchar(255) not null, `guard_name` varchar(255) not null, `label` varchar(255) null, `description` text null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2025_05_25_000000_create_permission_tables.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_05_25_000000_create_permission_tables.php","line":27},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.955711,"duration":0.0017800000000000001,"duration_str":"1.78ms","memory":0,"memory_str":null,"filename":"2025_05_25_000000_create_permission_tables.php:27","source":{"index":13,"namespace":null,"name":"database\/migrations\/2025_05_25_000000_create_permission_tables.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_05_25_000000_create_permission_tables.php","line":27},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2025_05_25_000000_create_permission_tables.php&line=27","ajax":false,"filename":"2025_05_25_000000_create_permission_tables.php","line":"27"},"connection":"hrm","explain":null,"start_percent":23.333,"width_percent":0.079},{"sql":"alter table `permissions` add unique `permissions_name_guard_name_unique`(`name`, `guard_name`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2025_05_25_000000_create_permission_tables.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_05_25_000000_create_permission_tables.php","line":27},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.957731,"duration":0.00173,"duration_str":"1.73ms","memory":0,"memory_str":null,"filename":"2025_05_25_000000_create_permission_tables.php:27","source":{"index":13,"namespace":null,"name":"database\/migrations\/2025_05_25_000000_create_permission_tables.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_05_25_000000_create_permission_tables.php","line":27},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2025_05_25_000000_create_permission_tables.php&line=27","ajax":false,"filename":"2025_05_25_000000_create_permission_tables.php","line":"27"},"connection":"hrm","explain":null,"start_percent":23.412,"width_percent":0.077},{"sql":"create table `roles` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `guard_name` varchar(255) not null, `label` varchar(255) null, `description` text null, `created_by` bigint unsigned null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2025_05_25_000000_create_permission_tables.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_05_25_000000_create_permission_tables.php","line":39},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.9599469,"duration":0.00196,"duration_str":"1.96ms","memory":0,"memory_str":null,"filename":"2025_05_25_000000_create_permission_tables.php:39","source":{"index":13,"namespace":null,"name":"database\/migrations\/2025_05_25_000000_create_permission_tables.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_05_25_000000_create_permission_tables.php","line":39},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2025_05_25_000000_create_permission_tables.php&line=39","ajax":false,"filename":"2025_05_25_000000_create_permission_tables.php","line":"39"},"connection":"hrm","explain":null,"start_percent":23.49,"width_percent":0.087},{"sql":"alter table `roles` add constraint `roles_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[{"index":13,"namespace":null,"name":"database\/migrations\/2025_05_25_000000_create_permission_tables.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_05_25_000000_create_permission_tables.php","line":39},{"index":18,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Console\/View\/Components\/Task.php","line":41},{"index":27,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":36},{"index":28,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/Util.php","line":43},{"index":29,"namespace":null,"name":"vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","file":"\/Users\/dvapp\/Documents\/HRM\/vendor\/laravel\/framework\/src\/Illuminate\/Container\/BoundMethod.php","line":96}],"start":1775439510.962085,"duration":0.00492,"duration_str":"4.92ms","memory":0,"memory_str":null,"filename":"2025_05_25_000000_create_permission_tables.php:39","source":{"index":13,"namespace":null,"name":"database\/migrations\/2025_05_25_000000_create_permission_tables.php","file":"\/Users\/dvapp\/Documents\/HRM\/database\/migrations\/2025_05_25_000000_create_permission_tables.php","line":39},"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fdatabase%2Fmigrations%2F2025_05_25_000000_create_permission_tables.php&line=39","ajax":false,"filename":"2025_05_25_000000_create_permission_tables.php","line":"39"},"connection":"hrm","explain":null,"start_percent":23.577,"width_percent":0.22},{"sql":"create table `model_has_permissions` (`permission_id` bigint unsigned not null, `model_type` varchar(255) not null, `model_id` bigint unsigned not null, primary key (`permission_id`, `model_id`, `model_type`)) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.967401,"duration":0.0022299999999999998,"duration_str":"2.23ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":23.797,"width_percent":0.1},{"sql":"alter table `model_has_permissions` add index `model_has_permissions_model_id_model_type_index`(`model_id`, `model_type`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.969678,"duration":0.00195,"duration_str":"1.95ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":23.896,"width_percent":0.087},{"sql":"alter table `model_has_permissions` add constraint `model_has_permissions_permission_id_foreign` foreign key (`permission_id`) references `permissions` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.971671,"duration":0.00541,"duration_str":"5.41ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":23.983,"width_percent":0.241},{"sql":"create table `model_has_roles` (`role_id` bigint unsigned not null, `model_type` varchar(255) not null, `model_id` bigint unsigned not null, primary key (`role_id`, `model_id`, `model_type`)) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.977303,"duration":0.00107,"duration_str":"1.07ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":24.225,"width_percent":0.048},{"sql":"alter table `model_has_roles` add index `model_has_roles_model_id_model_type_index`(`model_id`, `model_type`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.978407,"duration":0.00093,"duration_str":"930\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":24.272,"width_percent":0.042},{"sql":"alter table `model_has_roles` add constraint `model_has_roles_role_id_foreign` foreign key (`role_id`) references `roles` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.9793692,"duration":0.00558,"duration_str":"5.58ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":24.314,"width_percent":0.249},{"sql":"create table `role_has_permissions` (`permission_id` bigint unsigned not null, `role_id` bigint unsigned not null, primary key (`permission_id`, `role_id`)) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.985143,"duration":0.00189,"duration_str":"1.89ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":24.563,"width_percent":0.084},{"sql":"alter table `role_has_permissions` add constraint `role_has_permissions_permission_id_foreign` foreign key (`permission_id`) references `permissions` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.987074,"duration":0.00443,"duration_str":"4.43ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":24.647,"width_percent":0.198},{"sql":"alter table `role_has_permissions` add constraint `role_has_permissions_role_id_foreign` foreign key (`role_id`) references `roles` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.991549,"duration":0.00513,"duration_str":"5.13ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":24.845,"width_percent":0.229},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.9982688,"duration":0.00023,"duration_str":"230\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.074,"width_percent":0.01},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439510.999131,"duration":0.00019,"duration_str":"190\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.084,"width_percent":0.008},{"sql":"create table `settings` (`id` bigint unsigned not null auto_increment primary key, `user_id` bigint unsigned not null, `key` varchar(255) not null, `value` text null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.006355,"duration":0.00145,"duration_str":"1.45ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.093,"width_percent":0.065},{"sql":"alter table `settings` add constraint `settings_user_id_foreign` foreign key (`user_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0078619,"duration":0.00535,"duration_str":"5.35ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.157,"width_percent":0.239},{"sql":"alter table `settings` add unique `settings_user_id_key_unique`(`user_id`, `key`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.013271,"duration":0.00226,"duration_str":"2.26ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.396,"width_percent":0.101},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0159569,"duration":0.0002,"duration_str":"200\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.497,"width_percent":0.009},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0168712,"duration":0.00045,"duration_str":"450\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.506,"width_percent":0.02},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.017818,"duration":0.00022,"duration_str":"220\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.526,"width_percent":0.01},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.018485,"duration":0.00014000000000000001,"duration_str":"140\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.536,"width_percent":0.006},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.019303,"duration":0.00011999999999999999,"duration_str":"120\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.542,"width_percent":0.005},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0197651,"duration":0.00011,"duration_str":"110\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.547,"width_percent":0.005},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0201702,"duration":0.0001,"duration_str":"100\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.552,"width_percent":0.004},{"sql":"create table `currencies` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `code` varchar(10) not null, `symbol` varchar(10) null, `description` text null, `is_default` tinyint(1) not null default '0', `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.020685,"duration":0.0011200000000000001,"duration_str":"1.12ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.557,"width_percent":0.05},{"sql":"alter table `currencies` add unique `currencies_code_unique`(`code`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0218449,"duration":0.0035499999999999998,"duration_str":"3.55ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.607,"width_percent":0.158},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.025804,"duration":0.0005,"duration_str":"500\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.765,"width_percent":0.022},{"sql":"create table `payment_settings` (`id` bigint unsigned not null auto_increment primary key, `user_id` bigint unsigned not null, `key` varchar(255) not null, `value` text null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.026886,"duration":0.00164,"duration_str":"1.64ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.788,"width_percent":0.073},{"sql":"alter table `payment_settings` add unique `payment_settings_user_id_key_unique`(`user_id`, `key`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.02856,"duration":0.0013,"duration_str":"1.3ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.861,"width_percent":0.058},{"sql":"alter table `payment_settings` add constraint `payment_settings_user_id_foreign` foreign key (`user_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.029919,"duration":0.00615,"duration_str":"6.15ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":25.919,"width_percent":0.274},{"sql":"alter table `payment_settings` add index `payment_settings_user_id_key_index`(`user_id`, `key`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0361211,"duration":0.0016899999999999999,"duration_str":"1.69ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.193,"width_percent":0.075},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0384161,"duration":0.00027,"duration_str":"270\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.269,"width_percent":0.012},{"sql":"create table `media_items` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.039484,"duration":0.00263,"duration_str":"2.63ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.281,"width_percent":0.117},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.042759,"duration":0.00037,"duration_str":"370\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.398,"width_percent":0.017},{"sql":"create table `email_templates` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `from` varchar(255) null, `user_id` bigint unsigned not null default '1', `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.043921,"duration":0.0018700000000000001,"duration_str":"1.87ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.415,"width_percent":0.083},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.04632,"duration":0.0003,"duration_str":"300\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.498,"width_percent":0.013},{"sql":"create table `email_template_langs` (`id` bigint unsigned not null auto_increment primary key, `parent_id` bigint unsigned not null, `lang` varchar(255) not null, `subject` varchar(255) not null, `content` longtext not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.04746,"duration":0.00177,"duration_str":"1.77ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.511,"width_percent":0.079},{"sql":"alter table `email_template_langs` add constraint `email_template_langs_parent_id_foreign` foreign key (`parent_id`) references `email_templates` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.049309,"duration":0.00429,"duration_str":"4.29ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.59,"width_percent":0.191},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.054102,"duration":0.00035999999999999997,"duration_str":"360\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.782,"width_percent":0.016},{"sql":"create table `user_email_templates` (`id` bigint unsigned not null auto_increment primary key, `template_id` bigint unsigned not null, `user_id` bigint unsigned not null, `is_active` tinyint(1) not null default '1', `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.055513,"duration":0.00131,"duration_str":"1.31ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.798,"width_percent":0.058},{"sql":"alter table `user_email_templates` add constraint `user_email_templates_template_id_foreign` foreign key (`template_id`) references `email_templates` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.056904,"duration":0.00511,"duration_str":"5.11ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":26.856,"width_percent":0.228},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0625439,"duration":0.00023,"duration_str":"230\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.084,"width_percent":0.01},{"sql":"create table `landing_page_custom_pages` (`id` bigint unsigned not null auto_increment primary key, `title` varchar(255) not null, `slug` varchar(255) not null, `content` longtext not null, `meta_title` varchar(255) null, `meta_description` text null, `is_active` tinyint(1) not null default '1', `sort_order` int not null default '0', `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.064135,"duration":0.00417,"duration_str":"4.17ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.095,"width_percent":0.186},{"sql":"alter table `landing_page_custom_pages` add unique `landing_page_custom_pages_slug_unique`(`slug`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.068652,"duration":0.006030000000000001,"duration_str":"6.03ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.281,"width_percent":0.269},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.075442,"duration":0.00031,"duration_str":"310\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.55,"width_percent":0.014},{"sql":"select exists (select 1 from information_schema.tables where table_schema = schema() and table_name = 'contacts' and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.076157,"duration":0.00044,"duration_str":"440\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.564,"width_percent":0.02},{"sql":"create table `contacts` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `email` varchar(255) not null, `subject` varchar(255) not null, `message` text not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.076919,"duration":0.00413,"duration_str":"4.13ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.583,"width_percent":0.184},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.081492,"duration":0.00025,"duration_str":"250\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.768,"width_percent":0.011},{"sql":"create table `branches` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `address` text null, `city` varchar(255) null, `state` varchar(255) null, `country` varchar(255) null, `zip_code` varchar(255) null, `phone` varchar(255) null, `email` varchar(255) null, `status` enum('active', 'inactive') not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0842981,"duration":0.00133,"duration_str":"1.33ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.779,"width_percent":0.059},{"sql":"alter table `branches` add constraint `branches_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.0856829,"duration":0.00428,"duration_str":"4.28ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":27.838,"width_percent":0.191},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.090606,"duration":0.0003,"duration_str":"300\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":28.029,"width_percent":0.013},{"sql":"create table `departments` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `branch_id` bigint unsigned not null, `description` text null, `status` enum('active', 'inactive') not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.092007,"duration":0.0028399999999999996,"duration_str":"2.84ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":28.043,"width_percent":0.127},{"sql":"alter table `departments` add constraint `departments_branch_id_foreign` foreign key (`branch_id`) references `branches` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.095016,"duration":0.005019999999999999,"duration_str":"5.02ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":28.169,"width_percent":0.224},{"sql":"alter table `departments` add constraint `departments_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.100116,"duration":0.006730000000000001,"duration_str":"6.73ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":28.393,"width_percent":0.3},{"sql":"alter table `departments` add unique `departments_name_branch_id_unique`(`name`, `branch_id`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.106918,"duration":0.00186,"duration_str":"1.86ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":28.694,"width_percent":0.083},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.1094491,"duration":0.00027,"duration_str":"270\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":28.777,"width_percent":0.012},{"sql":"create table `designations` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `department_id` bigint unsigned not null, `status` enum('active', 'inactive') not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.1109362,"duration":0.0015400000000000001,"duration_str":"1.54ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":28.789,"width_percent":0.069},{"sql":"alter table `designations` add constraint `designations_department_id_foreign` foreign key (`department_id`) references `departments` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.11256,"duration":0.00419,"duration_str":"4.19ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":28.858,"width_percent":0.187},{"sql":"alter table `designations` add constraint `designations_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.11684,"duration":0.00845,"duration_str":"8.45ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":29.045,"width_percent":0.377},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.1259758,"duration":0.00057,"duration_str":"570\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":29.422,"width_percent":0.025},{"sql":"create table `document_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `is_required` tinyint(1) not null default '0', `description` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.127563,"duration":0.00152,"duration_str":"1.52ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":29.447,"width_percent":0.068},{"sql":"alter table `document_types` add constraint `document_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.129149,"duration":0.004940000000000001,"duration_str":"4.94ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":29.515,"width_percent":0.22},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.1346972,"duration":0.00027,"duration_str":"270\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":29.735,"width_percent":0.012},{"sql":"create table `employees` (`id` bigint unsigned not null auto_increment primary key, `employee_id` varchar(255) not null, `phone` varchar(255) null, `date_of_birth` date null, `gender` enum('male', 'female', 'other') null, `branch_id` bigint unsigned null, `department_id` bigint unsigned null, `designation_id` bigint unsigned null, `shift_id` bigint unsigned null, `attendance_policy_id` bigint unsigned null, `date_of_joining` date null, `employment_type` varchar(255) null, `address_line_1` varchar(255) null, `address_line_2` varchar(255) null, `city` varchar(255) null, `state` varchar(255) null, `country` varchar(255) null, `postal_code` varchar(255) null, `emergency_contact_name` varchar(255) null, `emergency_contact_relationship` varchar(255) null, `emergency_contact_number` varchar(255) null, `bank_name` varchar(255) null, `account_holder_name` varchar(255) null, `account_number` varchar(255) null, `bank_identifier_code` varchar(255) null, `bank_branch` varchar(255) null, `tax_payer_id` varchar(255) null, `user_id` bigint unsigned not null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.137373,"duration":0.00277,"duration_str":"2.77ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":29.747,"width_percent":0.124},{"sql":"alter table `employees` add constraint `employees_branch_id_foreign` foreign key (`branch_id`) references `branches` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.1402922,"duration":0.006059999999999999,"duration_str":"6.06ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":29.871,"width_percent":0.27},{"sql":"alter table `employees` add constraint `employees_department_id_foreign` foreign key (`department_id`) references `departments` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.146428,"duration":0.00683,"duration_str":"6.83ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":30.142,"width_percent":0.305},{"sql":"alter table `employees` add constraint `employees_designation_id_foreign` foreign key (`designation_id`) references `designations` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.153331,"duration":0.00878,"duration_str":"8.78ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":30.446,"width_percent":0.392},{"sql":"alter table `employees` add constraint `employees_shift_id_foreign` foreign key (`shift_id`) references `shifts` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.162174,"duration":0.009710000000000002,"duration_str":"9.71ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":30.838,"width_percent":0.433},{"sql":"alter table `employees` add constraint `employees_attendance_policy_id_foreign` foreign key (`attendance_policy_id`) references `attendance_policies` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.1719391,"duration":0.00715,"duration_str":"7.15ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":31.272,"width_percent":0.319},{"sql":"alter table `employees` add constraint `employees_user_id_foreign` foreign key (`user_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.1792,"duration":0.01175,"duration_str":"11.75ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":31.591,"width_percent":0.524},{"sql":"alter table `employees` add constraint `employees_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.1910381,"duration":0.00922,"duration_str":"9.22ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":32.115,"width_percent":0.411},{"sql":"alter table `employees` add unique `employees_employee_id_unique`(`employee_id`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.200316,"duration":0.00439,"duration_str":"4.39ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":32.526,"width_percent":0.196},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.2054522,"duration":0.00028000000000000003,"duration_str":"280\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":32.722,"width_percent":0.012},{"sql":"create table `employee_documents` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `document_type_id` bigint unsigned not null, `file_path` varchar(255) not null, `expiry_date` date null, `verification_status` enum('pending', 'verified', 'rejected') not null default 'pending', `notes` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.2072122,"duration":0.0016799999999999999,"duration_str":"1.68ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":32.735,"width_percent":0.075},{"sql":"alter table `employee_documents` add constraint `employee_documents_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.2089531,"duration":0.00539,"duration_str":"5.39ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":32.81,"width_percent":0.241},{"sql":"alter table `employee_documents` add constraint `employee_documents_document_type_id_foreign` foreign key (`document_type_id`) references `document_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.21441,"duration":0.00722,"duration_str":"7.22ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":33.05,"width_percent":0.322},{"sql":"alter table `employee_documents` add constraint `employee_documents_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.221713,"duration":0.00615,"duration_str":"6.15ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":33.373,"width_percent":0.274},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.228452,"duration":0.00033,"duration_str":"330\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":33.647,"width_percent":0.015},{"sql":"create table `award_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.229888,"duration":0.00215,"duration_str":"2.15ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":33.662,"width_percent":0.096},{"sql":"alter table `award_types` add constraint `award_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.232113,"duration":0.00691,"duration_str":"6.91ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":33.758,"width_percent":0.308},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.2397082,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":34.066,"width_percent":0.013},{"sql":"create table `awards` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `award_type_id` bigint unsigned not null, `award_date` date not null, `gift` varchar(255) null, `monetary_value` decimal(15, 2) null, `description` text null, `certificate` varchar(255) null, `photo` varchar(255) null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.241706,"duration":0.00232,"duration_str":"2.32ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":34.079,"width_percent":0.104},{"sql":"alter table `awards` add constraint `awards_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.244158,"duration":0.00652,"duration_str":"6.52ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":34.183,"width_percent":0.291},{"sql":"alter table `awards` add constraint `awards_award_type_id_foreign` foreign key (`award_type_id`) references `award_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.250761,"duration":0.00948,"duration_str":"9.48ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":34.474,"width_percent":0.423},{"sql":"alter table `awards` add constraint `awards_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.260311,"duration":0.00726,"duration_str":"7.26ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":34.897,"width_percent":0.324},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.268322,"duration":0.00020999999999999998,"duration_str":"210\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":35.221,"width_percent":0.009},{"sql":"create table `performance_indicator_categories` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.2695909,"duration":0.00131,"duration_str":"1.31ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":35.23,"width_percent":0.058},{"sql":"alter table `performance_indicator_categories` add constraint `performance_indicator_categories_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.2709792,"duration":0.00731,"duration_str":"7.31ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":35.288,"width_percent":0.326},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.2785559,"duration":0.00020999999999999998,"duration_str":"210\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":35.615,"width_percent":0.009},{"sql":"create table `performance_indicators` (`id` bigint unsigned not null auto_increment primary key, `category_id` bigint unsigned not null, `name` varchar(255) not null, `description` text null, `measurement_unit` varchar(255) null, `target_value` varchar(255) null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.27987,"duration":0.0018700000000000001,"duration_str":"1.87ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":35.624,"width_percent":0.083},{"sql":"alter table `performance_indicators` add constraint `performance_indicators_category_id_foreign` foreign key (`category_id`) references `performance_indicator_categories` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.281811,"duration":0.00628,"duration_str":"6.28ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":35.708,"width_percent":0.28},{"sql":"alter table `performance_indicators` add constraint `performance_indicators_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.288153,"duration":0.007019999999999999,"duration_str":"7.02ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":35.988,"width_percent":0.313},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.295617,"duration":0.0005200000000000001,"duration_str":"520\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":36.301,"width_percent":0.023},{"sql":"create table `goal_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.297078,"duration":0.00123,"duration_str":"1.23ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":36.324,"width_percent":0.055},{"sql":"alter table `goal_types` add constraint `goal_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.298389,"duration":0.00524,"duration_str":"5.24ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":36.379,"width_percent":0.234},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.304363,"duration":0.00035,"duration_str":"350\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":36.613,"width_percent":0.016},{"sql":"create table `employee_goals` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `goal_type_id` bigint unsigned not null, `title` varchar(255) not null, `description` text null, `start_date` date not null, `end_date` date not null, `target` varchar(255) null, `progress` int not null default '0', `status` varchar(255) not null default 'not_started', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.306027,"duration":0.0016,"duration_str":"1.6ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":36.629,"width_percent":0.071},{"sql":"alter table `employee_goals` add constraint `employee_goals_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.307705,"duration":0.00564,"duration_str":"5.64ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":36.7,"width_percent":0.252},{"sql":"alter table `employee_goals` add constraint `employee_goals_goal_type_id_foreign` foreign key (`goal_type_id`) references `goal_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.3134148,"duration":0.00567,"duration_str":"5.67ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":36.952,"width_percent":0.253},{"sql":"alter table `employee_goals` add constraint `employee_goals_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.319142,"duration":0.0059299999999999995,"duration_str":"5.93ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":37.205,"width_percent":0.265},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.325507,"duration":0.00043,"duration_str":"430\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":37.469,"width_percent":0.019},{"sql":"create table `review_cycles` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `frequency` varchar(255) not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.326577,"duration":0.0013700000000000001,"duration_str":"1.37ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":37.489,"width_percent":0.061},{"sql":"alter table `review_cycles` add constraint `review_cycles_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.3280082,"duration":0.00644,"duration_str":"6.44ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":37.55,"width_percent":0.287},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.334748,"duration":0.00013000000000000002,"duration_str":"130\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":37.837,"width_percent":0.006},{"sql":"create table `employee_reviews` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `reviewer_id` bigint unsigned not null, `review_cycle_id` bigint unsigned not null, `review_date` date not null, `completion_date` date null, `overall_rating` decimal(3, 1) null, `comments` text null, `status` varchar(255) not null default 'scheduled', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.336062,"duration":0.00188,"duration_str":"1.88ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":37.843,"width_percent":0.084},{"sql":"alter table `employee_reviews` add constraint `employee_reviews_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.338003,"duration":0.0058,"duration_str":"5.8ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":37.927,"width_percent":0.259},{"sql":"alter table `employee_reviews` add constraint `employee_reviews_reviewer_id_foreign` foreign key (`reviewer_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.343876,"duration":0.00636,"duration_str":"6.36ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":38.186,"width_percent":0.284},{"sql":"alter table `employee_reviews` add constraint `employee_reviews_review_cycle_id_foreign` foreign key (`review_cycle_id`) references `review_cycles` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.350289,"duration":0.0069500000000000004,"duration_str":"6.95ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":38.47,"width_percent":0.31},{"sql":"alter table `employee_reviews` add constraint `employee_reviews_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.357282,"duration":0.00619,"duration_str":"6.19ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":38.78,"width_percent":0.276},{"sql":"create table `employee_review_ratings` (`id` bigint unsigned not null auto_increment primary key, `employee_review_id` bigint unsigned not null, `performance_indicator_id` bigint unsigned not null, `rating` decimal(3, 1) not null, `comments` text null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.3639479,"duration":0.00302,"duration_str":"3.02ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":39.056,"width_percent":0.135},{"sql":"alter table `employee_review_ratings` add constraint `employee_review_ratings_employee_review_id_foreign` foreign key (`employee_review_id`) references `employee_reviews` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.367022,"duration":0.00549,"duration_str":"5.49ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":39.191,"width_percent":0.245},{"sql":"alter table `employee_review_ratings` add constraint `employee_review_ratings_performance_indicator_id_foreign` foreign key (`performance_indicator_id`) references `performance_indicators` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.372582,"duration":0.00679,"duration_str":"6.79ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":39.436,"width_percent":0.303},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.3800201,"duration":0.00028000000000000003,"duration_str":"280\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":39.739,"width_percent":0.012},{"sql":"create table `resignations` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `resignation_date` date not null, `last_working_day` date not null, `notice_period` varchar(255) null, `reason` varchar(255) null, `description` text null, `status` varchar(255) not null default 'pending', `documents` varchar(255) null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `exit_feedback` text null, `exit_interview_conducted` tinyint(1) not null default '0', `exit_interview_date` date null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.3820071,"duration":0.0016699999999999998,"duration_str":"1.67ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":39.751,"width_percent":0.075},{"sql":"alter table `resignations` add constraint `resignations_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.383738,"duration":0.00487,"duration_str":"4.87ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":39.826,"width_percent":0.217},{"sql":"alter table `resignations` add constraint `resignations_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.388675,"duration":0.0062,"duration_str":"6.2ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":40.043,"width_percent":0.277},{"sql":"alter table `resignations` add constraint `resignations_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.394954,"duration":0.0069299999999999995,"duration_str":"6.93ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":40.32,"width_percent":0.309},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.40258,"duration":0.00055,"duration_str":"550\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":40.629,"width_percent":0.025},{"sql":"create table `promotions` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `previous_designation` varchar(255) null, `designation_id` bigint unsigned not null, `promotion_date` date not null, `effective_date` date not null, `salary_adjustment` decimal(15, 2) null, `reason` text null, `document` varchar(255) null, `status` enum('pending', 'approved', 'rejected') not null default 'pending', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.404618,"duration":0.00227,"duration_str":"2.27ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":40.654,"width_percent":0.101},{"sql":"alter table `promotions` add constraint `promotions_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.4069648,"duration":0.005,"duration_str":"5ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":40.755,"width_percent":0.223},{"sql":"alter table `promotions` add constraint `promotions_designation_id_foreign` foreign key (`designation_id`) references `designations` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.412045,"duration":0.00737,"duration_str":"7.37ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":40.978,"width_percent":0.329},{"sql":"alter table `promotions` add constraint `promotions_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.4195,"duration":0.0057599999999999995,"duration_str":"5.76ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":41.307,"width_percent":0.257},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.4259832,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":41.564,"width_percent":0.013},{"sql":"create table `terminations` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `termination_type` varchar(255) not null, `termination_date` date not null, `notice_date` date not null, `notice_period` varchar(255) null, `reason` varchar(255) null, `description` text null, `status` varchar(255) not null default 'planned', `documents` varchar(255) null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `exit_interview_conducted` tinyint(1) not null default '0', `exit_interview_date` date null, `exit_feedback` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.427853,"duration":0.00202,"duration_str":"2.02ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":41.577,"width_percent":0.09},{"sql":"alter table `terminations` add constraint `terminations_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.429971,"duration":0.00561,"duration_str":"5.61ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":41.667,"width_percent":0.25},{"sql":"alter table `terminations` add constraint `terminations_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.435683,"duration":0.00596,"duration_str":"5.96ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":41.918,"width_percent":0.266},{"sql":"alter table `terminations` add constraint `terminations_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.4417229,"duration":0.00724,"duration_str":"7.24ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":42.184,"width_percent":0.323},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.449991,"duration":0.00033,"duration_str":"330\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":42.507,"width_percent":0.015},{"sql":"create table `warnings` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `warning_by` bigint unsigned not null, `warning_type` varchar(255) not null, `subject` varchar(255) not null, `severity` varchar(255) not null, `warning_date` date not null, `description` text null, `status` varchar(255) not null default 'draft', `documents` varchar(255) null, `acknowledgment_date` date null, `employee_response` text null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `expiry_date` date null, `has_improvement_plan` tinyint(1) not null default '0', `improvement_plan_goals` text null, `improvement_plan_start_date` date null, `improvement_plan_end_date` date null, `improvement_plan_progress` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.452039,"duration":0.00333,"duration_str":"3.33ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":42.521,"width_percent":0.149},{"sql":"alter table `warnings` add constraint `warnings_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.455444,"duration":0.0060999999999999995,"duration_str":"6.1ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":42.67,"width_percent":0.272},{"sql":"alter table `warnings` add constraint `warnings_warning_by_foreign` foreign key (`warning_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.461611,"duration":0.0073,"duration_str":"7.3ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":42.942,"width_percent":0.326},{"sql":"alter table `warnings` add constraint `warnings_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.468994,"duration":0.00899,"duration_str":"8.99ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":43.268,"width_percent":0.401},{"sql":"alter table `warnings` add constraint `warnings_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.478047,"duration":0.00868,"duration_str":"8.68ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":43.669,"width_percent":0.387},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.487212,"duration":0.00058,"duration_str":"580\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":44.057,"width_percent":0.026},{"sql":"create table `trips` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `purpose` varchar(255) not null, `destination` varchar(255) not null, `start_date` date not null, `end_date` date not null, `description` text null, `expected_outcomes` text null, `status` varchar(255) not null default 'planned', `documents` varchar(255) null, `advance_amount` decimal(15, 2) null, `advance_status` varchar(255) null, `total_expenses` decimal(15, 2) null, `reimbursement_status` varchar(255) null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `trip_report` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.48888,"duration":0.00317,"duration_str":"3.17ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":44.083,"width_percent":0.141},{"sql":"alter table `trips` add constraint `trips_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.492138,"duration":0.0076,"duration_str":"7.6ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":44.224,"width_percent":0.339},{"sql":"alter table `trips` add constraint `trips_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.499824,"duration":0.0063,"duration_str":"6.3ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":44.563,"width_percent":0.281},{"sql":"alter table `trips` add constraint `trips_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.5061948,"duration":0.00795,"duration_str":"7.95ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":44.844,"width_percent":0.355},{"sql":"create table `trip_expenses` (`id` bigint unsigned not null auto_increment primary key, `trip_id` bigint unsigned not null, `expense_type` varchar(255) not null, `expense_date` date not null, `amount` decimal(15, 2) not null, `currency` varchar(255) not null default 'USD', `description` text null, `receipt` varchar(255) null, `is_reimbursable` tinyint(1) not null default '1', `status` varchar(255) not null default 'pending', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.515221,"duration":0.00202,"duration_str":"2.02ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":45.199,"width_percent":0.09},{"sql":"alter table `trip_expenses` add constraint `trip_expenses_trip_id_foreign` foreign key (`trip_id`) references `trips` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.517324,"duration":0.00515,"duration_str":"5.15ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":45.289,"width_percent":0.23},{"sql":"alter table `trip_expenses` add constraint `trip_expenses_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.522544,"duration":0.00562,"duration_str":"5.62ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":45.519,"width_percent":0.251},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.529014,"duration":0.00028000000000000003,"duration_str":"280\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":45.77,"width_percent":0.012},{"sql":"create table `complaints` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `against_employee_id` bigint unsigned null, `complaint_type` varchar(255) not null, `subject` varchar(255) not null, `complaint_date` date not null, `description` text null, `status` varchar(255) not null default 'submitted', `documents` varchar(255) null, `is_anonymous` tinyint(1) not null default '0', `assigned_to` bigint unsigned null, `resolution_deadline` date null, `investigation_notes` text null, `resolution_action` text null, `resolution_date` date null, `follow_up_action` text null, `follow_up_date` date null, `feedback` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.531122,"duration":0.00254,"duration_str":"2.54ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":45.782,"width_percent":0.113},{"sql":"alter table `complaints` add constraint `complaints_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.533755,"duration":0.00958,"duration_str":"9.58ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":45.896,"width_percent":0.428},{"sql":"alter table `complaints` add constraint `complaints_against_employee_id_foreign` foreign key (`against_employee_id`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.5434089,"duration":0.006679999999999999,"duration_str":"6.68ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":46.323,"width_percent":0.298},{"sql":"alter table `complaints` add constraint `complaints_assigned_to_foreign` foreign key (`assigned_to`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.550202,"duration":0.0076,"duration_str":"7.6ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":46.621,"width_percent":0.339},{"sql":"alter table `complaints` add constraint `complaints_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.557908,"duration":0.0105,"duration_str":"10.5ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":46.961,"width_percent":0.469},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.56919,"duration":0.00045,"duration_str":"450\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":47.429,"width_percent":0.02},{"sql":"create table `employee_transfers` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `from_branch_id` bigint unsigned null, `to_branch_id` bigint unsigned null, `from_department_id` bigint unsigned null, `to_department_id` bigint unsigned null, `from_designation_id` bigint unsigned null, `to_designation_id` bigint unsigned null, `transfer_date` date not null, `effective_date` date not null, `reason` text null, `status` varchar(255) not null default 'pending', `documents` varchar(255) null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `notes` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.571302,"duration":0.00207,"duration_str":"2.07ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":47.449,"width_percent":0.092},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.573431,"duration":0.00634,"duration_str":"6.34ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":47.542,"width_percent":0.283},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_from_branch_id_foreign` foreign key (`from_branch_id`) references `branches` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.5798159,"duration":0.00777,"duration_str":"7.77ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":47.825,"width_percent":0.347},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_to_branch_id_foreign` foreign key (`to_branch_id`) references `branches` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.587639,"duration":0.01018,"duration_str":"10.18ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":48.171,"width_percent":0.454},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_from_department_id_foreign` foreign key (`from_department_id`) references `departments` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.597909,"duration":0.010230000000000001,"duration_str":"10.23ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":48.626,"width_percent":0.457},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_to_department_id_foreign` foreign key (`to_department_id`) references `departments` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.608259,"duration":0.00846,"duration_str":"8.46ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":49.082,"width_percent":0.378},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_from_designation_id_foreign` foreign key (`from_designation_id`) references `designations` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.6168258,"duration":0.01024,"duration_str":"10.24ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":49.46,"width_percent":0.457},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_to_designation_id_foreign` foreign key (`to_designation_id`) references `designations` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.6271522,"duration":0.012039999999999999,"duration_str":"12.04ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":49.917,"width_percent":0.537},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.639274,"duration":0.01124,"duration_str":"11.24ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":50.454,"width_percent":0.502},{"sql":"alter table `employee_transfers` add constraint `employee_transfers_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.650592,"duration":0.013630000000000001,"duration_str":"13.63ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":50.956,"width_percent":0.608},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.6648898,"duration":0.0006,"duration_str":"600\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":51.564,"width_percent":0.027},{"sql":"create table `holidays` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `start_date` date not null, `end_date` date null, `category` varchar(255) not null, `description` text null, `is_recurring` tinyint(1) not null default '0', `is_paid` tinyint(1) not null default '1', `is_half_day` tinyint(1) not null default '0', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.66717,"duration":0.0020499999999999997,"duration_str":"2.05ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":51.591,"width_percent":0.091},{"sql":"alter table `holidays` add constraint `holidays_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.669289,"duration":0.00647,"duration_str":"6.47ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":51.682,"width_percent":0.289},{"sql":"create table `holiday_branch` (`id` bigint unsigned not null auto_increment primary key, `holiday_id` bigint unsigned not null, `branch_id` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.676336,"duration":0.00185,"duration_str":"1.85ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":51.971,"width_percent":0.083},{"sql":"alter table `holiday_branch` add constraint `holiday_branch_holiday_id_foreign` foreign key (`holiday_id`) references `holidays` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.678267,"duration":0.01516,"duration_str":"15.16ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":52.054,"width_percent":0.677},{"sql":"alter table `holiday_branch` add constraint `holiday_branch_branch_id_foreign` foreign key (`branch_id`) references `branches` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.693548,"duration":0.08187,"duration_str":"81.87ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":52.73,"width_percent":3.654},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.77624,"duration":0.00034,"duration_str":"340\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":56.384,"width_percent":0.015},{"sql":"create table `announcements` (`id` bigint unsigned not null auto_increment primary key, `title` varchar(255) not null, `category` varchar(255) not null, `description` text null, `content` longtext not null, `start_date` date not null, `end_date` date null, `attachments` varchar(255) null, `is_featured` tinyint(1) not null default '0', `is_high_priority` tinyint(1) not null default '0', `is_company_wide` tinyint(1) not null default '1', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.778173,"duration":0.0106,"duration_str":"10.6ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":56.399,"width_percent":0.473},{"sql":"alter table `announcements` add constraint `announcements_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.788871,"duration":0.00468,"duration_str":"4.68ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":56.872,"width_percent":0.209},{"sql":"create table `announcement_department` (`id` bigint unsigned not null auto_increment primary key, `announcement_id` bigint unsigned not null, `department_id` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.794209,"duration":0.00099,"duration_str":"990\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":57.081,"width_percent":0.044},{"sql":"alter table `announcement_department` add constraint `announcement_department_announcement_id_foreign` foreign key (`announcement_id`) references `announcements` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.795264,"duration":0.00298,"duration_str":"2.98ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":57.125,"width_percent":0.133},{"sql":"alter table `announcement_department` add constraint `announcement_department_department_id_foreign` foreign key (`department_id`) references `departments` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.798317,"duration":0.00364,"duration_str":"3.64ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":57.258,"width_percent":0.162},{"sql":"create table `announcement_branch` (`id` bigint unsigned not null auto_increment primary key, `announcement_id` bigint unsigned not null, `branch_id` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.802522,"duration":0.00141,"duration_str":"1.41ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":57.421,"width_percent":0.063},{"sql":"alter table `announcement_branch` add constraint `announcement_branch_announcement_id_foreign` foreign key (`announcement_id`) references `announcements` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.803998,"duration":0.00467,"duration_str":"4.67ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":57.483,"width_percent":0.208},{"sql":"alter table `announcement_branch` add constraint `announcement_branch_branch_id_foreign` foreign key (`branch_id`) references `branches` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8087242,"duration":0.0036,"duration_str":"3.6ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":57.692,"width_percent":0.161},{"sql":"create table `announcement_views` (`id` bigint unsigned not null auto_increment primary key, `announcement_id` bigint unsigned not null, `employee_id` bigint unsigned not null, `viewed_at` timestamp not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.812898,"duration":0.0013,"duration_str":"1.3ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":57.853,"width_percent":0.058},{"sql":"alter table `announcement_views` add constraint `announcement_views_announcement_id_foreign` foreign key (`announcement_id`) references `announcements` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8142662,"duration":0.00361,"duration_str":"3.61ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":57.911,"width_percent":0.161},{"sql":"alter table `announcement_views` add constraint `announcement_views_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8179238,"duration":0.00447,"duration_str":"4.47ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":58.072,"width_percent":0.199},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8230321,"duration":0.00031,"duration_str":"310\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":58.271,"width_percent":0.014},{"sql":"create table `asset_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8243701,"duration":0.00102,"duration_str":"1.02ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":58.285,"width_percent":0.046},{"sql":"alter table `asset_types` add constraint `asset_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.825475,"duration":0.00524,"duration_str":"5.24ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":58.331,"width_percent":0.234},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.831438,"duration":0.00031,"duration_str":"310\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":58.564,"width_percent":0.014},{"sql":"create table `assets` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `asset_type_id` bigint unsigned not null, `serial_number` varchar(255) null, `asset_code` varchar(255) null, `purchase_date` date null, `purchase_cost` decimal(15, 2) null, `status` varchar(255) not null default 'available', `condition` varchar(255) null, `description` text null, `location` varchar(255) null, `supplier` varchar(255) null, `warranty_info` varchar(255) null, `warranty_expiry_date` date null, `images` varchar(255) null, `documents` varchar(255) null, `qr_code` varchar(255) null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8337739,"duration":0.00149,"duration_str":"1.49ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":58.578,"width_percent":0.066},{"sql":"alter table `assets` add constraint `assets_asset_type_id_foreign` foreign key (`asset_type_id`) references `asset_types` (`id`) on delete restrict","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8353329,"duration":0.004059999999999999,"duration_str":"4.06ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":58.645,"width_percent":0.181},{"sql":"alter table `assets` add constraint `assets_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8394458,"duration":0.0057,"duration_str":"5.7ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":58.826,"width_percent":0.254},{"sql":"create table `asset_assignments` (`id` bigint unsigned not null auto_increment primary key, `asset_id` bigint unsigned not null, `employee_id` bigint unsigned not null, `checkout_date` date not null, `expected_return_date` date null, `checkin_date` date null, `checkout_condition` varchar(255) null, `checkin_condition` varchar(255) null, `notes` text null, `is_acknowledged` tinyint(1) not null default '0', `acknowledged_at` timestamp null, `assigned_by` bigint unsigned not null, `received_by` bigint unsigned null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.845976,"duration":0.00155,"duration_str":"1.55ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":59.08,"width_percent":0.069},{"sql":"alter table `asset_assignments` add constraint `asset_assignments_asset_id_foreign` foreign key (`asset_id`) references `assets` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.847573,"duration":0.0047599999999999995,"duration_str":"4.76ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":59.149,"width_percent":0.212},{"sql":"alter table `asset_assignments` add constraint `asset_assignments_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.852382,"duration":0.00535,"duration_str":"5.35ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":59.362,"width_percent":0.239},{"sql":"alter table `asset_assignments` add constraint `asset_assignments_assigned_by_foreign` foreign key (`assigned_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8578088,"duration":0.006030000000000001,"duration_str":"6.03ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":59.601,"width_percent":0.269},{"sql":"alter table `asset_assignments` add constraint `asset_assignments_received_by_foreign` foreign key (`received_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.8639219,"duration":0.00636,"duration_str":"6.36ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":59.87,"width_percent":0.284},{"sql":"create table `asset_maintenances` (`id` bigint unsigned not null auto_increment primary key, `asset_id` bigint unsigned not null, `maintenance_type` varchar(255) not null, `start_date` date not null, `end_date` date null, `cost` decimal(15, 2) null, `status` varchar(255) not null default 'scheduled', `details` text null, `completion_notes` text null, `supplier` varchar(255) null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.871379,"duration":0.0012900000000000001,"duration_str":"1.29ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":60.154,"width_percent":0.058},{"sql":"alter table `asset_maintenances` add constraint `asset_maintenances_asset_id_foreign` foreign key (`asset_id`) references `assets` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.872755,"duration":0.0038,"duration_str":"3.8ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":60.211,"width_percent":0.17},{"sql":"alter table `asset_maintenances` add constraint `asset_maintenances_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.876628,"duration":0.00526,"duration_str":"5.26ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":60.381,"width_percent":0.235},{"sql":"create table `asset_depreciations` (`id` bigint unsigned not null auto_increment primary key, `asset_id` bigint unsigned not null, `method` varchar(255) not null, `useful_life_years` int not null, `salvage_value` decimal(15, 2) null, `current_value` decimal(15, 2) not null, `last_calculated_date` date not null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.882903,"duration":0.00155,"duration_str":"1.55ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":60.616,"width_percent":0.069},{"sql":"alter table `asset_depreciations` add constraint `asset_depreciations_asset_id_foreign` foreign key (`asset_id`) references `assets` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.884565,"duration":0.0069500000000000004,"duration_str":"6.95ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":60.685,"width_percent":0.31},{"sql":"alter table `asset_depreciations` add constraint `asset_depreciations_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.891588,"duration":0.00511,"duration_str":"5.11ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":60.995,"width_percent":0.228},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.89729,"duration":0.00034,"duration_str":"340\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":61.223,"width_percent":0.015},{"sql":"create table `training_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `branch_id` bigint unsigned not null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.898398,"duration":0.00182,"duration_str":"1.82ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":61.238,"width_percent":0.081},{"sql":"alter table `training_types` add constraint `training_types_branch_id_foreign` foreign key (`branch_id`) references `branches` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.900283,"duration":0.0037,"duration_str":"3.7ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":61.319,"width_percent":0.165},{"sql":"alter table `training_types` add constraint `training_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.904042,"duration":0.00639,"duration_str":"6.39ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":61.484,"width_percent":0.285},{"sql":"create table `training_type_department` (`id` bigint unsigned not null auto_increment primary key, `training_type_id` bigint unsigned not null, `department_id` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.911058,"duration":0.0012,"duration_str":"1.2ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":61.77,"width_percent":0.054},{"sql":"alter table `training_type_department` add constraint `training_type_department_training_type_id_foreign` foreign key (`training_type_id`) references `training_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.912315,"duration":0.00272,"duration_str":"2.72ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":61.823,"width_percent":0.121},{"sql":"alter table `training_type_department` add constraint `training_type_department_department_id_foreign` foreign key (`department_id`) references `departments` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.915108,"duration":0.00618,"duration_str":"6.18ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":61.945,"width_percent":0.276},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.921973,"duration":0.00023999999999999998,"duration_str":"240\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":62.22,"width_percent":0.011},{"sql":"create table `training_programs` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `training_type_id` bigint unsigned not null, `description` text null, `duration` int null, `cost` decimal(15, 2) null, `capacity` int null, `status` varchar(255) not null default 'draft', `materials` varchar(255) null, `prerequisites` text null, `is_mandatory` tinyint(1) not null default '0', `is_self_enrollment` tinyint(1) not null default '0', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.9237769,"duration":0.00166,"duration_str":"1.66ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":62.231,"width_percent":0.074},{"sql":"alter table `training_programs` add constraint `training_programs_training_type_id_foreign` foreign key (`training_type_id`) references `training_types` (`id`) on delete restrict","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.925495,"duration":0.00429,"duration_str":"4.29ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":62.305,"width_percent":0.191},{"sql":"alter table `training_programs` add constraint `training_programs_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.929825,"duration":0.00519,"duration_str":"5.19ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":62.497,"width_percent":0.232},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.935379,"duration":0.00016,"duration_str":"160\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":62.728,"width_percent":0.007},{"sql":"create table `training_sessions` (`id` bigint unsigned not null auto_increment primary key, `training_program_id` bigint unsigned not null, `name` varchar(255) null, `start_date` datetime not null, `end_date` datetime not null, `location` varchar(255) null, `location_type` varchar(255) not null default 'physical', `meeting_link` varchar(255) null, `status` varchar(255) not null default 'scheduled', `notes` text null, `is_recurring` tinyint(1) not null default '0', `recurrence_pattern` varchar(255) null, `recurrence_count` int null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.936503,"duration":0.00126,"duration_str":"1.26ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":62.735,"width_percent":0.056},{"sql":"alter table `training_sessions` add constraint `training_sessions_training_program_id_foreign` foreign key (`training_program_id`) references `training_programs` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.937813,"duration":0.00443,"duration_str":"4.43ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":62.792,"width_percent":0.198},{"sql":"alter table `training_sessions` add constraint `training_sessions_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.942312,"duration":0.0074199999999999995,"duration_str":"7.42ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":62.989,"width_percent":0.331},{"sql":"create table `training_session_trainer` (`id` bigint unsigned not null auto_increment primary key, `training_session_id` bigint unsigned not null, `employee_id` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.950357,"duration":0.0012,"duration_str":"1.2ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":63.32,"width_percent":0.054},{"sql":"alter table `training_session_trainer` add constraint `training_session_trainer_training_session_id_foreign` foreign key (`training_session_id`) references `training_sessions` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.951613,"duration":0.00392,"duration_str":"3.92ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":63.374,"width_percent":0.175},{"sql":"alter table `training_session_trainer` add constraint `training_session_trainer_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.955591,"duration":0.00591,"duration_str":"5.91ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":63.549,"width_percent":0.264},{"sql":"create table `training_session_attendance` (`id` bigint unsigned not null auto_increment primary key, `training_session_id` bigint unsigned not null, `employee_id` bigint unsigned not null, `is_present` tinyint(1) not null default '0', `notes` text null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.962106,"duration":0.00099,"duration_str":"990\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":63.813,"width_percent":0.044},{"sql":"alter table `training_session_attendance` add constraint `training_session_attendance_training_session_id_foreign` foreign key (`training_session_id`) references `training_sessions` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.9631782,"duration":0.005030000000000001,"duration_str":"5.03ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":63.857,"width_percent":0.224},{"sql":"alter table `training_session_attendance` add constraint `training_session_attendance_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.968289,"duration":0.00633,"duration_str":"6.33ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":64.081,"width_percent":0.282},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.975191,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":64.364,"width_percent":0.013},{"sql":"create table `employee_trainings` (`id` bigint unsigned not null auto_increment primary key, `employee_id` bigint unsigned not null, `training_program_id` bigint unsigned not null, `status` varchar(255) not null default 'assigned', `assigned_date` date not null, `completion_date` date null, `certification` varchar(255) null, `score` decimal(5, 2) null, `is_passed` tinyint(1) null, `feedback` text null, `notes` text null, `assigned_by` bigint unsigned not null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.977129,"duration":0.00136,"duration_str":"1.36ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":64.377,"width_percent":0.061},{"sql":"alter table `employee_trainings` add constraint `employee_trainings_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.978577,"duration":0.004730000000000001,"duration_str":"4.73ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":64.437,"width_percent":0.211},{"sql":"alter table `employee_trainings` add constraint `employee_trainings_training_program_id_foreign` foreign key (`training_program_id`) references `training_programs` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.9833832,"duration":0.00452,"duration_str":"4.52ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":64.649,"width_percent":0.202},{"sql":"alter table `employee_trainings` add constraint `employee_trainings_assigned_by_foreign` foreign key (`assigned_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.9879699,"duration":0.00762,"duration_str":"7.62ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":64.85,"width_percent":0.34},{"sql":"alter table `employee_trainings` add constraint `employee_trainings_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439511.995653,"duration":0.00604,"duration_str":"6.04ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":65.19,"width_percent":0.27},{"sql":"create table `training_assessments` (`id` bigint unsigned not null auto_increment primary key, `training_program_id` bigint unsigned not null, `name` varchar(255) not null, `description` text null, `type` varchar(255) not null, `passing_score` decimal(5, 2) not null default '70', `criteria` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.002526,"duration":0.0016200000000000001,"duration_str":"1.62ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":65.46,"width_percent":0.072},{"sql":"alter table `training_assessments` add constraint `training_assessments_training_program_id_foreign` foreign key (`training_program_id`) references `training_programs` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.004242,"duration":0.00412,"duration_str":"4.12ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":65.532,"width_percent":0.184},{"sql":"alter table `training_assessments` add constraint `training_assessments_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.008465,"duration":0.00571,"duration_str":"5.71ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":65.716,"width_percent":0.255},{"sql":"create table `employee_assessment_results` (`id` bigint unsigned not null auto_increment primary key, `employee_training_id` bigint unsigned not null, `training_assessment_id` bigint unsigned not null, `score` decimal(5, 2) not null, `is_passed` tinyint(1) not null, `feedback` text null, `assessment_date` date not null, `assessed_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.015192,"duration":0.00133,"duration_str":"1.33ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":65.971,"width_percent":0.059},{"sql":"alter table `employee_assessment_results` add constraint `employee_assessment_results_employee_training_id_foreign` foreign key (`employee_training_id`) references `employee_trainings` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.016595,"duration":0.00492,"duration_str":"4.92ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":66.03,"width_percent":0.22},{"sql":"alter table `employee_assessment_results` add constraint `employee_assessment_results_training_assessment_id_foreign` foreign key (`training_assessment_id`) references `training_assessments` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.0216029,"duration":0.00392,"duration_str":"3.92ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":66.25,"width_percent":0.175},{"sql":"alter table `employee_assessment_results` add constraint `employee_assessment_results_assessed_by_foreign` foreign key (`assessed_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.0256052,"duration":0.00571,"duration_str":"5.71ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":66.425,"width_percent":0.255},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.031931,"duration":0.00023,"duration_str":"230\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":66.68,"width_percent":0.01},{"sql":"create table `job_categories` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.033354,"duration":0.00103,"duration_str":"1.03ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":66.69,"width_percent":0.046},{"sql":"alter table `job_categories` add constraint `job_categories_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.0344422,"duration":0.005019999999999999,"duration_str":"5.02ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":66.736,"width_percent":0.224},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.040122,"duration":0.00033,"duration_str":"330\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":66.96,"width_percent":0.015},{"sql":"create table `job_requisitions` (`id` bigint unsigned not null auto_increment primary key, `requisition_code` varchar(255) not null, `title` varchar(255) not null, `job_category_id` bigint unsigned not null, `department_id` bigint unsigned null, `positions_count` int not null default '1', `budget_min` decimal(15, 2) null, `budget_max` decimal(15, 2) null, `skills_required` text null, `education_required` text null, `experience_required` text null, `description` text null, `responsibilities` text null, `status` enum('Draft', 'Pending Approval', 'Approved', 'On Hold', 'Closed') not null default 'Draft', `approved_by` bigint unsigned null, `approval_date` timestamp null, `priority` enum('Low', 'Medium', 'High') not null default 'Medium', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.042262,"duration":0.00155,"duration_str":"1.55ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":66.975,"width_percent":0.069},{"sql":"alter table `job_requisitions` add constraint `job_requisitions_job_category_id_foreign` foreign key (`job_category_id`) references `job_categories` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.043899,"duration":0.0060999999999999995,"duration_str":"6.1ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":67.044,"width_percent":0.272},{"sql":"alter table `job_requisitions` add constraint `job_requisitions_department_id_foreign` foreign key (`department_id`) references `departments` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.050093,"duration":0.0049900000000000005,"duration_str":"4.99ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":67.316,"width_percent":0.223},{"sql":"alter table `job_requisitions` add constraint `job_requisitions_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.055172,"duration":0.00745,"duration_str":"7.45ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":67.539,"width_percent":0.332},{"sql":"alter table `job_requisitions` add constraint `job_requisitions_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.062706,"duration":0.009859999999999999,"duration_str":"9.86ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":67.871,"width_percent":0.44},{"sql":"alter table `job_requisitions` add unique `job_requisitions_requisition_code_unique`(`requisition_code`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.072654,"duration":0.00262,"duration_str":"2.62ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":68.311,"width_percent":0.117},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.075933,"duration":0.00033,"duration_str":"330\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":68.428,"width_percent":0.015},{"sql":"create table `job_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.077399,"duration":0.00136,"duration_str":"1.36ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":68.443,"width_percent":0.061},{"sql":"alter table `job_types` add constraint `job_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.0788372,"duration":0.00519,"duration_str":"5.19ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":68.504,"width_percent":0.232},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.0847862,"duration":0.00064,"duration_str":"640\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":68.735,"width_percent":0.029},{"sql":"create table `job_locations` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `address` text null, `city` varchar(255) null, `state` varchar(255) null, `country` varchar(255) null, `postal_code` varchar(255) null, `is_remote` tinyint(1) not null default '0', `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.086852,"duration":0.00115,"duration_str":"1.15ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":68.764,"width_percent":0.051},{"sql":"alter table `job_locations` add constraint `job_locations_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.088076,"duration":0.00474,"duration_str":"4.74ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":68.815,"width_percent":0.212},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.093467,"duration":0.00028000000000000003,"duration_str":"280\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":69.027,"width_percent":0.012},{"sql":"create table `job_postings` (`id` bigint unsigned not null auto_increment primary key, `requisition_id` bigint unsigned not null, `job_code` varchar(255) not null, `title` varchar(255) not null, `job_type_id` bigint unsigned not null, `location_id` bigint unsigned not null, `department_id` bigint unsigned null, `min_experience` int not null default '0', `max_experience` int null, `min_salary` decimal(15, 2) null, `max_salary` decimal(15, 2) null, `description` text null, `requirements` text null, `benefits` text null, `application_deadline` date null, `is_published` tinyint(1) not null default '0', `publish_date` timestamp null, `is_featured` tinyint(1) not null default '0', `status` enum('Draft', 'Published', 'Closed') not null default 'Draft', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.0954301,"duration":0.0016699999999999998,"duration_str":"1.67ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":69.039,"width_percent":0.075},{"sql":"alter table `job_postings` add constraint `job_postings_requisition_id_foreign` foreign key (`requisition_id`) references `job_requisitions` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.097187,"duration":0.00864,"duration_str":"8.64ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":69.114,"width_percent":0.386},{"sql":"alter table `job_postings` add constraint `job_postings_job_type_id_foreign` foreign key (`job_type_id`) references `job_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.105902,"duration":0.00675,"duration_str":"6.75ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":69.499,"width_percent":0.301},{"sql":"alter table `job_postings` add constraint `job_postings_location_id_foreign` foreign key (`location_id`) references `job_locations` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.112701,"duration":0.00929,"duration_str":"9.29ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":69.8,"width_percent":0.415},{"sql":"alter table `job_postings` add constraint `job_postings_department_id_foreign` foreign key (`department_id`) references `departments` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.122075,"duration":0.00714,"duration_str":"7.14ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":70.215,"width_percent":0.319},{"sql":"alter table `job_postings` add constraint `job_postings_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.129306,"duration":0.00858,"duration_str":"8.58ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":70.534,"width_percent":0.383},{"sql":"alter table `job_postings` add unique `job_postings_job_code_unique`(`job_code`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.137966,"duration":0.0033599999999999997,"duration_str":"3.36ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":70.917,"width_percent":0.15},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.142082,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":71.067,"width_percent":0.013},{"sql":"create table `candidate_sources` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.143465,"duration":0.00222,"duration_str":"2.22ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":71.08,"width_percent":0.099},{"sql":"alter table `candidate_sources` add constraint `candidate_sources_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.145739,"duration":0.00475,"duration_str":"4.75ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":71.179,"width_percent":0.212},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.15106,"duration":0.00028000000000000003,"duration_str":"280\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":71.391,"width_percent":0.012},{"sql":"create table `candidates` (`id` bigint unsigned not null auto_increment primary key, `job_id` bigint unsigned not null, `source_id` bigint unsigned not null, `first_name` varchar(255) not null, `last_name` varchar(255) not null, `email` varchar(255) not null, `phone` varchar(255) null, `current_company` varchar(255) null, `current_position` varchar(255) null, `experience_years` int not null default '0', `current_salary` decimal(15, 2) null, `expected_salary` decimal(15, 2) null, `notice_period` varchar(255) null, `resume_path` varchar(255) null, `cover_letter_path` varchar(255) null, `skills` text null, `education` text null, `portfolio_url` varchar(255) null, `linkedin_url` varchar(255) null, `referral_employee_id` bigint unsigned null, `status` enum('New', 'Screening', 'Interview', 'Offer', 'Hired', 'Rejected') not null default 'New', `application_date` date not null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.1533978,"duration":0.002,"duration_str":"2ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":71.403,"width_percent":0.089},{"sql":"alter table `candidates` add constraint `candidates_job_id_foreign` foreign key (`job_id`) references `job_postings` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.15547,"duration":0.00652,"duration_str":"6.52ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":71.492,"width_percent":0.291},{"sql":"alter table `candidates` add constraint `candidates_source_id_foreign` foreign key (`source_id`) references `candidate_sources` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.162065,"duration":0.0075899999999999995,"duration_str":"7.59ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":71.783,"width_percent":0.339},{"sql":"alter table `candidates` add constraint `candidates_referral_employee_id_foreign` foreign key (`referral_employee_id`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.169749,"duration":0.00889,"duration_str":"8.89ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":72.122,"width_percent":0.397},{"sql":"alter table `candidates` add constraint `candidates_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.178727,"duration":0.00929,"duration_str":"9.29ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":72.519,"width_percent":0.415},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.188756,"duration":0.00028000000000000003,"duration_str":"280\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":72.933,"width_percent":0.012},{"sql":"create table `interview_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.1903,"duration":0.0011,"duration_str":"1.1ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":72.946,"width_percent":0.049},{"sql":"alter table `interview_types` add constraint `interview_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.191485,"duration":0.00611,"duration_str":"6.11ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":72.995,"width_percent":0.273},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.198235,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":73.268,"width_percent":0.013},{"sql":"create table `interview_rounds` (`id` bigint unsigned not null auto_increment primary key, `job_id` bigint unsigned not null, `name` varchar(255) not null, `sequence_number` int not null, `description` text null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.200116,"duration":0.00138,"duration_str":"1.38ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":73.281,"width_percent":0.062},{"sql":"alter table `interview_rounds` add constraint `interview_rounds_job_id_foreign` foreign key (`job_id`) references `job_postings` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.201568,"duration":0.0049900000000000005,"duration_str":"4.99ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":73.342,"width_percent":0.223},{"sql":"alter table `interview_rounds` add constraint `interview_rounds_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.206625,"duration":0.00528,"duration_str":"5.28ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":73.565,"width_percent":0.236},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.2124481,"duration":0.00035,"duration_str":"350\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":73.801,"width_percent":0.016},{"sql":"create table `interviews` (`id` bigint unsigned not null auto_increment primary key, `candidate_id` bigint unsigned not null, `job_id` bigint unsigned not null, `round_id` bigint unsigned not null, `interview_type_id` bigint unsigned not null, `scheduled_date` date not null, `scheduled_time` time not null, `duration` int not null default '60', `location` varchar(255) null, `meeting_link` varchar(255) null, `interviewers` json not null, `status` enum('Scheduled', 'Completed', 'Cancelled', 'No-show') not null default 'Scheduled', `feedback_submitted` tinyint(1) not null default '0', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.214415,"duration":0.00133,"duration_str":"1.33ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":73.816,"width_percent":0.059},{"sql":"alter table `interviews` add constraint `interviews_candidate_id_foreign` foreign key (`candidate_id`) references `candidates` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.215836,"duration":0.00457,"duration_str":"4.57ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":73.875,"width_percent":0.204},{"sql":"alter table `interviews` add constraint `interviews_job_id_foreign` foreign key (`job_id`) references `job_postings` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.220483,"duration":0.007730000000000001,"duration_str":"7.73ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":74.079,"width_percent":0.345},{"sql":"alter table `interviews` add constraint `interviews_round_id_foreign` foreign key (`round_id`) references `interview_rounds` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.228284,"duration":0.00658,"duration_str":"6.58ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":74.424,"width_percent":0.294},{"sql":"alter table `interviews` add constraint `interviews_interview_type_id_foreign` foreign key (`interview_type_id`) references `interview_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.234931,"duration":0.00746,"duration_str":"7.46ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":74.718,"width_percent":0.333},{"sql":"alter table `interviews` add constraint `interviews_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.24246,"duration":0.01071,"duration_str":"10.71ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":75.051,"width_percent":0.478},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.253869,"duration":0.00033,"duration_str":"330\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":75.529,"width_percent":0.015},{"sql":"create table `interview_feedback` (`id` bigint unsigned not null auto_increment primary key, `interview_id` bigint unsigned not null, `interviewer_id` varchar(255) null, `technical_rating` int null, `communication_rating` int null, `cultural_fit_rating` int null, `overall_rating` int null, `strengths` text null, `weaknesses` text null, `comments` text null, `recommendation` enum('Strong Hire', 'Hire', 'Maybe', 'Reject', 'Strong Reject') null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.255634,"duration":0.0014,"duration_str":"1.4ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":75.544,"width_percent":0.062},{"sql":"alter table `interview_feedback` add constraint `interview_feedback_interview_id_foreign` foreign key (`interview_id`) references `interviews` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.2571092,"duration":0.00435,"duration_str":"4.35ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":75.606,"width_percent":0.194},{"sql":"alter table `interview_feedback` add constraint `interview_feedback_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.2615259,"duration":0.0070999999999999995,"duration_str":"7.1ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":75.8,"width_percent":0.317},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.269131,"duration":0.00031,"duration_str":"310\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":76.117,"width_percent":0.014},{"sql":"create table `candidate_assessments` (`id` bigint unsigned not null auto_increment primary key, `candidate_id` bigint unsigned not null, `assessment_name` varchar(255) not null, `score` int null, `max_score` int null, `pass_fail_status` enum('Pass', 'Fail', 'Pending') not null default 'Pending', `comments` text null, `conducted_by` bigint unsigned not null, `assessment_date` date not null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.2707639,"duration":0.00161,"duration_str":"1.61ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":76.131,"width_percent":0.072},{"sql":"alter table `candidate_assessments` add constraint `candidate_assessments_candidate_id_foreign` foreign key (`candidate_id`) references `candidates` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.272447,"duration":0.00562,"duration_str":"5.62ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":76.203,"width_percent":0.251},{"sql":"alter table `candidate_assessments` add constraint `candidate_assessments_conducted_by_foreign` foreign key (`conducted_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.2781382,"duration":0.00571,"duration_str":"5.71ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":76.454,"width_percent":0.255},{"sql":"alter table `candidate_assessments` add constraint `candidate_assessments_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.2839131,"duration":0.0066,"duration_str":"6.6ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":76.708,"width_percent":0.295},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.291001,"duration":0.00027,"duration_str":"270\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":77.003,"width_percent":0.012},{"sql":"create table `offer_templates` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `template_content` longtext not null, `variables` json null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.292391,"duration":0.0010500000000000002,"duration_str":"1.05ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":77.015,"width_percent":0.047},{"sql":"alter table `offer_templates` add constraint `offer_templates_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.2935321,"duration":0.00537,"duration_str":"5.37ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":77.062,"width_percent":0.24},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.299778,"duration":0.00035,"duration_str":"350\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":77.302,"width_percent":0.016},{"sql":"create table `offers` (`id` bigint unsigned not null auto_increment primary key, `candidate_id` bigint unsigned not null, `job_id` bigint unsigned not null, `offer_date` date not null, `position` varchar(255) not null, `department_id` bigint unsigned null, `salary` decimal(15, 2) not null, `bonus` decimal(15, 2) null, `equity` varchar(255) null, `benefits` text null, `start_date` date not null, `expiration_date` date not null, `offer_letter_path` varchar(255) null, `status` enum('Draft', 'Sent', 'Accepted', 'Negotiating', 'Declined', 'Expired') not null default 'Draft', `response_date` date null, `decline_reason` text null, `created_by` bigint unsigned not null, `approved_by` bigint unsigned null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.301913,"duration":0.0018700000000000001,"duration_str":"1.87ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":77.317,"width_percent":0.083},{"sql":"alter table `offers` add constraint `offers_candidate_id_foreign` foreign key (`candidate_id`) references `candidates` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.3038568,"duration":0.0057,"duration_str":"5.7ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":77.401,"width_percent":0.254},{"sql":"alter table `offers` add constraint `offers_job_id_foreign` foreign key (`job_id`) references `job_postings` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.309629,"duration":0.00635,"duration_str":"6.35ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":77.655,"width_percent":0.283},{"sql":"alter table `offers` add constraint `offers_department_id_foreign` foreign key (`department_id`) references `departments` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.316053,"duration":0.00534,"duration_str":"5.34ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":77.938,"width_percent":0.238},{"sql":"alter table `offers` add constraint `offers_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.32146,"duration":0.00822,"duration_str":"8.22ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":78.177,"width_percent":0.367},{"sql":"alter table `offers` add constraint `offers_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.329757,"duration":0.01005,"duration_str":"10.05ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":78.544,"width_percent":0.449},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.340606,"duration":0.00035,"duration_str":"350\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":78.992,"width_percent":0.016},{"sql":"create table `onboarding_checklists` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `is_default` tinyint(1) not null default '0', `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.342228,"duration":0.00122,"duration_str":"1.22ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.008,"width_percent":0.054},{"sql":"alter table `onboarding_checklists` add constraint `onboarding_checklists_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.3435268,"duration":0.00657,"duration_str":"6.57ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.062,"width_percent":0.293},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.350618,"duration":0.00022,"duration_str":"220\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.355,"width_percent":0.01},{"sql":"create table `checklist_items` (`id` bigint unsigned not null auto_increment primary key, `checklist_id` bigint unsigned not null, `task_name` varchar(255) not null, `description` text null, `category` enum('Documentation', 'IT Setup', 'Training', 'HR', 'Facilities', 'Other') not null default 'Other', `assigned_to_role` varchar(255) null, `due_day` int not null default '1', `is_required` tinyint(1) not null default '1', `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.3522189,"duration":0.00177,"duration_str":"1.77ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.365,"width_percent":0.079},{"sql":"alter table `checklist_items` add constraint `checklist_items_checklist_id_foreign` foreign key (`checklist_id`) references `onboarding_checklists` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.354085,"duration":0.00454,"duration_str":"4.54ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.444,"width_percent":0.203},{"sql":"alter table `checklist_items` add constraint `checklist_items_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.358705,"duration":0.00529,"duration_str":"5.29ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.647,"width_percent":0.236},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.364574,"duration":0.00032,"duration_str":"320\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.883,"width_percent":0.014},{"sql":"create table `candidate_onboarding` (`id` bigint unsigned not null auto_increment primary key, `candidate_id` bigint unsigned not null, `checklist_id` bigint unsigned not null, `start_date` date not null, `buddy_employee_id` bigint unsigned null, `status` enum('Pending', 'In Progress', 'Completed') not null default 'Pending', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.366709,"duration":0.00116,"duration_str":"1.16ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.897,"width_percent":0.052},{"sql":"alter table `candidate_onboarding` add constraint `candidate_onboarding_candidate_id_foreign` foreign key (`candidate_id`) references `candidates` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.3679361,"duration":0.00419,"duration_str":"4.19ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":79.949,"width_percent":0.187},{"sql":"alter table `candidate_onboarding` add constraint `candidate_onboarding_checklist_id_foreign` foreign key (`checklist_id`) references `onboarding_checklists` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.372169,"duration":0.00467,"duration_str":"4.67ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":80.136,"width_percent":0.208},{"sql":"alter table `candidate_onboarding` add constraint `candidate_onboarding_buddy_employee_id_foreign` foreign key (`buddy_employee_id`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.37689,"duration":0.0064,"duration_str":"6.4ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":80.344,"width_percent":0.286},{"sql":"alter table `candidate_onboarding` add constraint `candidate_onboarding_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.383347,"duration":0.008039999999999999,"duration_str":"8.04ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":80.63,"width_percent":0.359},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.391658,"duration":0.00015,"duration_str":"150\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":80.989,"width_percent":0.007},{"sql":"create table `meeting_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `color` varchar(255) not null default '#3B82F6', `default_duration` int not null default '60', `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.3924391,"duration":0.0021000000000000003,"duration_str":"2.1ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":80.995,"width_percent":0.094},{"sql":"alter table `meeting_types` add constraint `meeting_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.3945851,"duration":0.00519,"duration_str":"5.19ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":81.089,"width_percent":0.232},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.400202,"duration":0.00016,"duration_str":"160\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":81.321,"width_percent":0.007},{"sql":"create table `meeting_rooms` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `type` enum('Physical', 'Virtual') not null default 'Physical', `location` varchar(255) null, `capacity` int not null, `equipment` json null, `booking_url` varchar(255) null, `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.401216,"duration":0.0013700000000000001,"duration_str":"1.37ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":81.328,"width_percent":0.061},{"sql":"alter table `meeting_rooms` add constraint `meeting_rooms_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.402637,"duration":0.0059299999999999995,"duration_str":"5.93ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":81.389,"width_percent":0.265},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.409446,"duration":0.00022,"duration_str":"220\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":81.654,"width_percent":0.01},{"sql":"create table `meetings` (`id` bigint unsigned not null auto_increment primary key, `title` varchar(255) not null, `description` text null, `type_id` bigint unsigned not null, `room_id` bigint unsigned null, `meeting_date` date not null, `start_time` time not null, `end_time` time not null, `duration` int not null, `agenda` text null, `status` enum('Scheduled', 'In Progress', 'Completed', 'Cancelled') not null default 'Scheduled', `recurrence` enum('None', 'Daily', 'Weekly', 'Monthly') not null default 'None', `recurrence_end_date` date null, `organizer_id` bigint unsigned not null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.410768,"duration":0.00164,"duration_str":"1.64ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":81.664,"width_percent":0.073},{"sql":"alter table `meetings` add constraint `meetings_type_id_foreign` foreign key (`type_id`) references `meeting_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.412456,"duration":0.00543,"duration_str":"5.43ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":81.737,"width_percent":0.242},{"sql":"alter table `meetings` add constraint `meetings_room_id_foreign` foreign key (`room_id`) references `meeting_rooms` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.417971,"duration":0.0064,"duration_str":"6.4ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":81.979,"width_percent":0.286},{"sql":"alter table `meetings` add constraint `meetings_organizer_id_foreign` foreign key (`organizer_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.424447,"duration":0.007,"duration_str":"7ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":82.265,"width_percent":0.312},{"sql":"alter table `meetings` add constraint `meetings_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.431548,"duration":0.00753,"duration_str":"7.53ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":82.577,"width_percent":0.336},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.439801,"duration":0.00023,"duration_str":"230\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":82.913,"width_percent":0.01},{"sql":"create table `meeting_attendees` (`id` bigint unsigned not null auto_increment primary key, `meeting_id` bigint unsigned not null, `user_id` bigint unsigned not null, `type` enum('Required', 'Optional') not null default 'Required', `rsvp_status` enum('Pending', 'Accepted', 'Declined', 'Tentative') not null default 'Pending', `attendance_status` enum('Not Attended', 'Present', 'Late', 'Left Early') not null default 'Not Attended', `rsvp_date` timestamp null, `decline_reason` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.440994,"duration":0.00115,"duration_str":"1.15ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":82.923,"width_percent":0.051},{"sql":"alter table `meeting_attendees` add constraint `meeting_attendees_meeting_id_foreign` foreign key (`meeting_id`) references `meetings` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.442241,"duration":0.00458,"duration_str":"4.58ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":82.975,"width_percent":0.204},{"sql":"alter table `meeting_attendees` add constraint `meeting_attendees_user_id_foreign` foreign key (`user_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.446925,"duration":0.00738,"duration_str":"7.38ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":83.179,"width_percent":0.329},{"sql":"alter table `meeting_attendees` add constraint `meeting_attendees_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.454377,"duration":0.007,"duration_str":"7ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":83.508,"width_percent":0.312},{"sql":"alter table `meeting_attendees` add unique `meeting_attendees_meeting_id_user_id_unique`(`meeting_id`, `user_id`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.461448,"duration":0.00307,"duration_str":"3.07ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":83.821,"width_percent":0.137},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.464931,"duration":0.00016,"duration_str":"160\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":83.958,"width_percent":0.007},{"sql":"create table `meeting_minutes` (`id` bigint unsigned not null auto_increment primary key, `meeting_id` bigint unsigned not null, `topic` varchar(255) not null, `content` longtext not null, `type` enum('Discussion', 'Decision', 'Action Item', 'Note') not null default 'Discussion', `recorded_by` bigint unsigned not null, `recorded_at` timestamp not null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.466247,"duration":0.00191,"duration_str":"1.91ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":83.965,"width_percent":0.085},{"sql":"alter table `meeting_minutes` add constraint `meeting_minutes_meeting_id_foreign` foreign key (`meeting_id`) references `meetings` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.468214,"duration":0.00501,"duration_str":"5.01ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":84.05,"width_percent":0.224},{"sql":"alter table `meeting_minutes` add constraint `meeting_minutes_recorded_by_foreign` foreign key (`recorded_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.473287,"duration":0.00521,"duration_str":"5.21ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":84.274,"width_percent":0.233},{"sql":"alter table `meeting_minutes` add constraint `meeting_minutes_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.478561,"duration":0.00639,"duration_str":"6.39ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":84.506,"width_percent":0.285},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.485274,"duration":0.00016,"duration_str":"160\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":84.792,"width_percent":0.007},{"sql":"create table `action_items` (`id` bigint unsigned not null auto_increment primary key, `meeting_id` bigint unsigned not null, `title` varchar(255) not null, `description` text null, `assigned_to` bigint unsigned not null, `due_date` date not null, `priority` enum('Low', 'Medium', 'High', 'Critical') not null default 'Medium', `status` enum('Not Started', 'In Progress', 'Completed', 'Overdue') not null default 'Not Started', `progress_percentage` int not null default '0', `notes` text null, `completed_date` date null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.4862149,"duration":0.00141,"duration_str":"1.41ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":84.799,"width_percent":0.063},{"sql":"alter table `action_items` add constraint `action_items_meeting_id_foreign` foreign key (`meeting_id`) references `meetings` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.487688,"duration":0.00453,"duration_str":"4.53ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":84.862,"width_percent":0.202},{"sql":"alter table `action_items` add constraint `action_items_assigned_to_foreign` foreign key (`assigned_to`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.492275,"duration":0.0059299999999999995,"duration_str":"5.93ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":85.064,"width_percent":0.265},{"sql":"alter table `action_items` add constraint `action_items_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.498302,"duration":0.00701,"duration_str":"7.01ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":85.328,"width_percent":0.313},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.505788,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":85.641,"width_percent":0.013},{"sql":"create table `contract_types` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `default_duration_months` int null, `probation_period_months` int not null default '3', `notice_period_days` int not null default '30', `is_renewable` tinyint(1) not null default '1', `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.506894,"duration":0.00134,"duration_str":"1.34ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":85.654,"width_percent":0.06},{"sql":"alter table `contract_types` add constraint `contract_types_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.5082788,"duration":0.00498,"duration_str":"4.98ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":85.714,"width_percent":0.222},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.513582,"duration":0.00023999999999999998,"duration_str":"240\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":85.936,"width_percent":0.011},{"sql":"create table `employee_contracts` (`id` bigint unsigned not null auto_increment primary key, `contract_number` varchar(255) not null, `employee_id` bigint unsigned not null, `contract_type_id` bigint unsigned not null, `start_date` date not null, `end_date` date null, `basic_salary` decimal(10, 2) not null, `allowances` json null, `benefits` json null, `terms_conditions` text null, `status` enum('Draft', 'Pending Approval', 'Active', 'Expired', 'Terminated', 'Renewed') not null default 'Draft', `approved_by` bigint unsigned null, `approved_at` timestamp null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.515143,"duration":0.00525,"duration_str":"5.25ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":85.947,"width_percent":0.234},{"sql":"alter table `employee_contracts` add constraint `employee_contracts_employee_id_foreign` foreign key (`employee_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.5204651,"duration":0.005719999999999999,"duration_str":"5.72ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":86.181,"width_percent":0.255},{"sql":"alter table `employee_contracts` add constraint `employee_contracts_contract_type_id_foreign` foreign key (`contract_type_id`) references `contract_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.5262508,"duration":0.00656,"duration_str":"6.56ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":86.437,"width_percent":0.293},{"sql":"alter table `employee_contracts` add constraint `employee_contracts_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.532906,"duration":0.0065899999999999995,"duration_str":"6.59ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":86.729,"width_percent":0.294},{"sql":"alter table `employee_contracts` add constraint `employee_contracts_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.539599,"duration":0.006730000000000001,"duration_str":"6.73ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":87.023,"width_percent":0.3},{"sql":"alter table `employee_contracts` add unique `employee_contracts_contract_number_unique`(`contract_number`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.546395,"duration":0.0028399999999999996,"duration_str":"2.84ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":87.324,"width_percent":0.127},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.549653,"duration":0.00019,"duration_str":"190\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":87.451,"width_percent":0.008},{"sql":"create table `contract_renewals` (`id` bigint unsigned not null auto_increment primary key, `contract_id` bigint unsigned not null, `renewal_number` varchar(255) not null, `current_end_date` date not null, `new_start_date` date not null, `new_end_date` date not null, `new_basic_salary` decimal(10, 2) not null, `new_allowances` json null, `new_benefits` json null, `new_terms_conditions` text null, `changes_summary` text null, `status` enum('Pending', 'Approved', 'Rejected', 'Processed') not null default 'Pending', `reason` text null, `requested_by` bigint unsigned not null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `approval_notes` text null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.550901,"duration":0.0016200000000000001,"duration_str":"1.62ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":87.459,"width_percent":0.072},{"sql":"alter table `contract_renewals` add constraint `contract_renewals_contract_id_foreign` foreign key (`contract_id`) references `employee_contracts` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.552561,"duration":0.00501,"duration_str":"5.01ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":87.531,"width_percent":0.224},{"sql":"alter table `contract_renewals` add constraint `contract_renewals_requested_by_foreign` foreign key (`requested_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.5576289,"duration":0.00869,"duration_str":"8.69ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":87.755,"width_percent":0.388},{"sql":"alter table `contract_renewals` add constraint `contract_renewals_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.56638,"duration":0.00792,"duration_str":"7.92ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":88.143,"width_percent":0.353},{"sql":"alter table `contract_renewals` add constraint `contract_renewals_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.574357,"duration":0.0091,"duration_str":"9.1ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":88.496,"width_percent":0.406},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.5838568,"duration":0.00022,"duration_str":"220\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":88.902,"width_percent":0.01},{"sql":"create table `contract_templates` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `contract_type_id` bigint unsigned not null, `template_content` longtext not null, `variables` json null, `clauses` json null, `is_default` tinyint(1) not null default '0', `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.585377,"duration":0.00126,"duration_str":"1.26ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":88.912,"width_percent":0.056},{"sql":"alter table `contract_templates` add constraint `contract_templates_contract_type_id_foreign` foreign key (`contract_type_id`) references `contract_types` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.586692,"duration":0.00438,"duration_str":"4.38ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":88.968,"width_percent":0.195},{"sql":"alter table `contract_templates` add constraint `contract_templates_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.591131,"duration":0.008539999999999999,"duration_str":"8.54ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":89.164,"width_percent":0.381},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.600102,"duration":0.00026000000000000003,"duration_str":"260\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":89.545,"width_percent":0.012},{"sql":"create table `document_categories` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `color` varchar(7) not null default '#3B82F6', `icon` varchar(255) not null default 'FileText', `sort_order` int not null default '0', `is_mandatory` tinyint(1) not null default '0', `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.601512,"duration":0.00214,"duration_str":"2.14ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":89.557,"width_percent":0.096},{"sql":"alter table `document_categories` add constraint `document_categories_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.603719,"duration":0.00785,"duration_str":"7.85ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":89.652,"width_percent":0.35},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.6120079,"duration":0.00020999999999999998,"duration_str":"210\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":90.002,"width_percent":0.009},{"sql":"create table `hr_documents` (`id` bigint unsigned not null auto_increment primary key, `title` varchar(255) not null, `description` text null, `category_id` bigint unsigned not null, `file_name` varchar(255) not null, `file_path` varchar(255) not null, `file_type` varchar(255) not null, `file_size` int not null, `version` varchar(10) not null default '1.0', `status` enum('Draft', 'Under Review', 'Approved', 'Published', 'Archived', 'Expired') not null default 'Draft', `effective_date` date null, `expiry_date` date null, `requires_acknowledgment` tinyint(1) not null default '0', `download_count` int not null default '0', `uploaded_by` bigint unsigned not null, `approved_by` bigint unsigned null, `approved_at` timestamp null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.6138139,"duration":0.0025,"duration_str":"2.5ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":90.012,"width_percent":0.112},{"sql":"alter table `hr_documents` add constraint `hr_documents_category_id_foreign` foreign key (`category_id`) references `document_categories` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.616378,"duration":0.00519,"duration_str":"5.19ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":90.123,"width_percent":0.232},{"sql":"alter table `hr_documents` add constraint `hr_documents_uploaded_by_foreign` foreign key (`uploaded_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.6216278,"duration":0.00779,"duration_str":"7.79ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":90.355,"width_percent":0.348},{"sql":"alter table `hr_documents` add constraint `hr_documents_approved_by_foreign` foreign key (`approved_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.629486,"duration":0.00878,"duration_str":"8.78ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":90.703,"width_percent":0.392},{"sql":"alter table `hr_documents` add constraint `hr_documents_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.638344,"duration":0.00872,"duration_str":"8.72ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":91.094,"width_percent":0.389},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.647742,"duration":0.00031,"duration_str":"310\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":91.484,"width_percent":0.014},{"sql":"create table `document_acknowledgments` (`id` bigint unsigned not null auto_increment primary key, `document_id` bigint unsigned not null, `user_id` bigint unsigned not null, `status` enum('Pending', 'Acknowledged', 'Overdue', 'Exempted') not null default 'Pending', `acknowledged_at` timestamp null, `due_date` date null, `acknowledgment_note` text null, `ip_address` varchar(255) null, `user_agent` varchar(255) null, `assigned_by` bigint unsigned not null, `assigned_at` timestamp null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.649245,"duration":0.00197,"duration_str":"1.97ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":91.497,"width_percent":0.088},{"sql":"alter table `document_acknowledgments` add constraint `document_acknowledgments_document_id_foreign` foreign key (`document_id`) references `hr_documents` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.6512768,"duration":0.00479,"duration_str":"4.79ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":91.585,"width_percent":0.214},{"sql":"alter table `document_acknowledgments` add constraint `document_acknowledgments_user_id_foreign` foreign key (`user_id`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.656148,"duration":0.006679999999999999,"duration_str":"6.68ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":91.799,"width_percent":0.298},{"sql":"alter table `document_acknowledgments` add constraint `document_acknowledgments_assigned_by_foreign` foreign key (`assigned_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.662899,"duration":0.0065899999999999995,"duration_str":"6.59ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":92.097,"width_percent":0.294},{"sql":"alter table `document_acknowledgments` add constraint `document_acknowledgments_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.6695511,"duration":0.0068,"duration_str":"6.8ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":92.391,"width_percent":0.303},{"sql":"alter table `document_acknowledgments` add unique `document_acknowledgments_document_id_user_id_unique`(`document_id`, `user_id`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.676393,"duration":0.00357,"duration_str":"3.57ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":92.695,"width_percent":0.159},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.680277,"duration":0.00015,"duration_str":"150\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":92.854,"width_percent":0.007},{"sql":"create table `document_templates` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `description` text null, `category_id` bigint unsigned not null, `template_content` longtext not null, `placeholders` json null, `default_values` json null, `is_default` tinyint(1) not null default '0', `file_format` varchar(255) not null default 'pdf', `status` varchar(255) not null default 'active', `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.681227,"duration":0.00264,"duration_str":"2.64ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":92.861,"width_percent":0.118},{"sql":"alter table `document_templates` add constraint `document_templates_category_id_foreign` foreign key (`category_id`) references `document_categories` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.683981,"duration":0.04739,"duration_str":"47.39ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":92.979,"width_percent":2.115},{"sql":"alter table `document_templates` add constraint `document_templates_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.731506,"duration":0.018949999999999998,"duration_str":"18.95ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":95.094,"width_percent":0.846},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.7511349,"duration":0.00035999999999999997,"duration_str":"360\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":95.939,"width_percent":0.016},{"sql":"create table `media_directories` (`id` bigint unsigned not null auto_increment primary key, `name` varchar(255) not null, `slug` varchar(255) not null, `parent_id` bigint unsigned null, `created_by` bigint unsigned not null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.7528439,"duration":0.0053,"duration_str":"5.3ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":95.955,"width_percent":0.237},{"sql":"alter table `media_directories` add constraint `media_directories_parent_id_foreign` foreign key (`parent_id`) references `media_directories` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.7582521,"duration":0.02011,"duration_str":"20.11ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":96.192,"width_percent":0.897},{"sql":"alter table `media_directories` add constraint `media_directories_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.778444,"duration":0.00626,"duration_str":"6.26ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":97.089,"width_percent":0.279},{"sql":"alter table `media_directories` add unique `media_directories_slug_unique`(`slug`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.7847571,"duration":0.00256,"duration_str":"2.56ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":97.369,"width_percent":0.114},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.787895,"duration":0.00035,"duration_str":"350\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":97.483,"width_percent":0.016},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.7893891,"duration":0.00051,"duration_str":"510\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":97.499,"width_percent":0.023},{"sql":"create table `media` (`id` bigint unsigned not null auto_increment primary key, `model_type` varchar(255) not null, `model_id` bigint unsigned not null, `uuid` char(36) null, `collection_name` varchar(255) not null, `name` varchar(255) not null, `file_name` varchar(255) not null, `mime_type` varchar(255) null, `disk` varchar(255) not null, `conversions_disk` varchar(255) null, `size` bigint unsigned not null, `manipulations` json not null, `custom_properties` json not null, `generated_conversions` json not null, `responsive_images` json not null, `order_column` int unsigned null, `directory_id` bigint unsigned null, `created_by` bigint unsigned null, `created_at` timestamp null, `updated_at` timestamp null) default character set utf8mb4 collate 'utf8mb4_unicode_ci'","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.791178,"duration":0.00141,"duration_str":"1.41ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":97.521,"width_percent":0.063},{"sql":"alter table `media` add index `media_model_type_model_id_index`(`model_type`, `model_id`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.7926521,"duration":0.00361,"duration_str":"3.61ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":97.584,"width_percent":0.161},{"sql":"alter table `media` add constraint `media_directory_id_foreign` foreign key (`directory_id`) references `media_directories` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.796341,"duration":0.00561,"duration_str":"5.61ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":97.745,"width_percent":0.25},{"sql":"alter table `media` add constraint `media_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete set null","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.802025,"duration":0.00787,"duration_str":"7.87ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":97.996,"width_percent":0.351},{"sql":"alter table `media` add unique `media_uuid_unique`(`uuid`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.809951,"duration":0.00204,"duration_str":"2.04ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":98.347,"width_percent":0.091},{"sql":"alter table `media` add index `media_order_column_index`(`order_column`)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.8120332,"duration":0.00202,"duration_str":"2.02ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":98.438,"width_percent":0.09},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.81485,"duration":0.00029,"duration_str":"290\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":98.528,"width_percent":0.013},{"sql":"select exists (select 1 from information_schema.tables where table_schema = schema() and table_name = 'roles' and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.8157232,"duration":0.00062,"duration_str":"620\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":98.541,"width_percent":0.028},{"sql":"alter table `roles` drop foreign key `roles_created_by_foreign`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.8166192,"duration":0.00451,"duration_str":"4.51ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":98.569,"width_percent":0.201},{"sql":"alter table `roles` add constraint `roles_created_by_foreign` foreign key (`created_by`) references `users` (`id`) on delete cascade","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.821197,"duration":0.00564,"duration_str":"5.64ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":98.77,"width_percent":0.252},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.8274791,"duration":0.00028000000000000003,"duration_str":"280\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.022,"width_percent":0.012},{"sql":"select exists (select 1 from information_schema.tables where table_schema = schema() and table_name = 'employees' and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.828239,"duration":0.0005200000000000001,"duration_str":"520\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.034,"width_percent":0.023},{"sql":"select column_name as `name`, data_type as `type_name`, column_type as `type`, collation_name as `collation`, is_nullable as `nullable`, column_default as `default`, column_comment as `comment`, generation_expression as `expression`, extra as `extra` from information_schema.columns where table_schema = schema() and table_name = 'employees' order by ordinal_position asc","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.828881,"duration":0.00049,"duration_str":"490\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.057,"width_percent":0.022},{"sql":"select column_name as `name`, data_type as `type_name`, column_type as `type`, collation_name as `collation`, is_nullable as `nullable`, column_default as `default`, column_comment as `comment`, generation_expression as `expression`, extra as `extra` from information_schema.columns where table_schema = schema() and table_name = 'employees' order by ordinal_position asc","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.829517,"duration":0.0004,"duration_str":"400\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.079,"width_percent":0.018},{"sql":"alter table `employees` add `base_salary` decimal(10, 2) null after `date_of_joining`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.830208,"duration":0.00664,"duration_str":"6.64ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.097,"width_percent":0.296},{"sql":"alter table `employees` add `employee_status` enum('active', 'inactive', 'terminated', 'probation') not null default 'active' after `employment_type`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.8369281,"duration":0.0063,"duration_str":"6.3ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.394,"width_percent":0.281},{"sql":"insert into `migrations` (`migration`, `batch`) values (?, ?)","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.84379,"duration":0.00053,"duration_str":"530\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.675,"width_percent":0.024},{"sql":"select exists (select 1 from information_schema.tables where table_schema = schema() and table_name = 'employees' and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.84483,"duration":0.00051,"duration_str":"510\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.698,"width_percent":0.023},{"sql":"select column_name as `name`, data_type as `type_name`, column_type as `type`, collation_name as `collation`, is_nullable as `nullable`, column_default as `default`, column_comment as `comment`, generation_expression as `expression`, extra as `extra` from information_schema.columns where table_schema = schema() and table_name = 'employees' order by ordinal_position asc","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.845453,"duration":0.0005200000000000001,"duration_str":"520\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.721,"width_percent":0.023},{"sql":"alter table `employees` add `biometric_emp_id` varchar(255) null after `employee_id`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.8463778,"duration":0.00534,"duration_str":"5.34ms","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.744,"width_percent":0.238},{"sql":"select exists (select 1 from information_schema.tables where table_schema = schema() and table_name = 'attendance_records' and table_type in ('BASE TABLE', 'SYSTEM VERSIONED')) as `exists`","type":"query","params":[],"bindings":[],"hints":null,"show_copy":true,"backtrace":[],"start":1775439512.851848,"duration":0.00039,"duration_str":"390\u03bcs","memory":0,"memory_str":null,"filename":"","source":false,"xdebug_link":null,"connection":"hrm","explain":null,"start_percent":99.983,"width_percent":0.017},{"sql":"... 2236 additional queries are executed but now shown because of Debugbar query limits. Limits can be raised in the config (debugbar.options.db.soft\/hard_limit)","type":"info"}]},"models":{"data":{"Spatie\\Permission\\Models\\Permission":{"created":717,"retrieved":646,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Fspatie%2Flaravel-permission%2Fsrc%2FModels%2FPermission.php&line=1","ajax":false,"filename":"Permission.php","line":"?"}},"App\\Models\\Permission":{"retrieved":1173,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FPermission.php&line=1","ajax":false,"filename":"Permission.php","line":"?"}},"App\\Models\\EmailTemplateLang":{"created":256,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FEmailTemplateLang.php&line=1","ajax":false,"filename":"EmailTemplateLang.php","line":"?"}},"App\\Models\\Currency":{"created":109,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FCurrency.php&line=1","ajax":false,"filename":"Currency.php","line":"?"}},"App\\Models\\EmailTemplate":{"created":16,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FEmailTemplate.php&line=1","ajax":false,"filename":"EmailTemplate.php","line":"?"}},"App\\Models\\UserEmailTemplate":{"created":16,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FUserEmailTemplate.php&line=1","ajax":false,"filename":"UserEmailTemplate.php","line":"?"}},"App\\Models\\NocTemplate":{"created":16,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FNocTemplate.php&line=1","ajax":false,"filename":"NocTemplate.php","line":"?"}},"App\\Models\\JoiningLetterTemplate":{"created":16,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FJoiningLetterTemplate.php&line=1","ajax":false,"filename":"JoiningLetterTemplate.php","line":"?"}},"App\\Models\\ExperienceCertificateTemplate":{"created":16,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FExperienceCertificateTemplate.php&line=1","ajax":false,"filename":"ExperienceCertificateTemplate.php","line":"?"}},"App\\Models\\User":{"created":3,"retrieved":4,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FUser.php&line=1","ajax":false,"filename":"User.php","line":"?"}},"App\\Models\\Role":{"retrieved":3,"created":3,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FRole.php&line=1","ajax":false,"filename":"Role.php","line":"?"}},"App\\Models\\LandingPageCustomPage":{"created":6,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fapp%2FModels%2FLandingPageCustomPage.php&line=1","ajax":false,"filename":"LandingPageCustomPage.php","line":"?"}},"Spatie\\Permission\\Models\\Role":{"created":1,"xdebug_link":{"url":"phpstorm:\/\/open?file=%2FUsers%2Fdvapp%2FDocuments%2FHRM%2Fvendor%2Fspatie%2Flaravel-permission%2Fsrc%2FModels%2FRole.php&line=1","ajax":false,"filename":"Role.php","line":"?"}}},"count":3001,"key_map":{"retrieved":"Retrieved","created":"Created","updated":"Updated","deleted":"Deleted"},"is_counter":true,"badges":{"created":1175,"retrieved":1826}},"symfonymailer_mails":{"count":0,"mails":[]},"gate":{"count":0,"messages":[]},"request":{"data":{"status":"302 Found","full_url":"http:\/\/hrm.test\/install\/database","action_name":"LaravelInstaller::database","controller_action":"RachidLaasri\\LaravelInstaller\\Controllers\\DatabaseController@database","uri":"GET install\/database","controller":"RachidLaasri\\LaravelInstaller\\Controllers\\DatabaseController@database<\/a>","namespace":"RachidLaasri\\LaravelInstaller\\Controllers","prefix":"install","file":"vendor\/rachidlaasri\/laravel-installer\/src\/Controllers\/DatabaseController.php:28-34<\/a>","middleware":"web, install","duration":"4.15s","peak_memory":"52MB","response":"Redirect to http:\/\/hrm.test\/install\/final","request_format":"html","request_query":"
[]\n<\/pre>
+stopSection(); ?>
+make('vendor.installer.layouts.master', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
\ No newline at end of file
diff --git a/storage/framework/views/4fd344ff3567e9069f5f19db7a71a8db.php b/storage/framework/views/4fd344ff3567e9069f5f19db7a71a8db.php
new file mode 100644
index 000000000..5fc8a4d0c
--- /dev/null
+++ b/storage/framework/views/4fd344ff3567e9069f5f19db7a71a8db.php
@@ -0,0 +1,56 @@
+startSection('template_title'); ?>
+    
+
+stopSection(); ?>
+
+startSection('title'); ?>
+    
+
+stopSection(); ?>
+
+startSection('container'); ?>
+    
+stopSection(); ?>
+
+make('vendor.installer.layouts.master', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?>
\ No newline at end of file
diff --git a/storage/framework/views/5593d4d67f3ff648aeef4375699512e0.php b/storage/framework/views/5593d4d67f3ff648aeef4375699512e0.php
new file mode 100644
index 000000000..2cf80bbf7
--- /dev/null
+++ b/storage/framework/views/5593d4d67f3ff648aeef4375699512e0.php
@@ -0,0 +1,78 @@
+startSection('template_title'); ?>
+    
+
+stopSection(); ?>
+
+startSection('title'); ?>
+    
+    
+
+stopSection(); ?>
+
+startSection('container'); ?>
+    
+
+
+ +

Checking folder permissions required for the application to function properly.

+
+
+
+ +
+

+ + Directory Permissions +

+ +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $permission): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+
+ +
+
+ +
Required for file operations
+
+
+
+ + + + +
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ + +
+
+
+ + All permissions are correctly set! +
+
+ + + + + +
+ +
+
+
+ + Permission issues detected +
+

Please fix the folder permissions above before continuing.

+
+
+ +stopSection(); ?> + +make('vendor.installer.layouts.master', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/5a500e9e4ccd04f710536318565612cb.php b/storage/framework/views/5a500e9e4ccd04f710536318565612cb.php new file mode 100644 index 000000000..b4d9659a4 --- /dev/null +++ b/storage/framework/views/5a500e9e4ccd04f710536318565612cb.php @@ -0,0 +1,127 @@ + + + + + + + + <?php if(trim($__env->yieldContent('template_title'))): ?><?php echo $__env->yieldContent('template_title'); ?> | <?php endif; ?> <?php echo e(trans('installer_messages.title')); ?> + + + + yieldContent('style'); ?> + + + +
+
+ +
+

yieldContent('title'); ?>

+

+
+ + +
+
+
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+ + +
+ +
+
+ +

+ + + + + + + +

+
+
+ + + has('errors')): ?> +
+
+ +
+

+
    + all(); $__env->addLoop($__currentLoopData); foreach($__currentLoopData as $error): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
  • + popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ +
+
+ + + yieldContent('container'); ?> +
+
+
+ + yieldContent('scripts'); ?> + + + + \ No newline at end of file diff --git a/storage/framework/views/6cc4d5bb5b918560d20ade5771cc1c16.php b/storage/framework/views/6cc4d5bb5b918560d20ade5771cc1c16.php new file mode 100644 index 000000000..f9b05a108 --- /dev/null +++ b/storage/framework/views/6cc4d5bb5b918560d20ade5771cc1c16.php @@ -0,0 +1,78 @@ +startSection('template_title'); ?> + + +stopSection(); ?> + +startSection('title'); ?> + + + +stopSection(); ?> + +startSection('container'); ?> +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $type => $requirement): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+
+

+ + Requirements + + + (version required) + + +

+ +
+ + + + +
+ +
+
+ +
+ +
+ addLoop($__currentLoopData); foreach($__currentLoopData as $extension => $enabled): $__env->incrementLoopIndices(); $loop = $__env->getLastLoop(); ?> +
+ +
+ +
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+
+ popLoop(); $loop = $__env->getLastLoop(); ?> +
+ + +
+
+
+ + All requirements are satisfied! +
+
+ + + + + +
+ +
+
+
+ + Please fix the requirements above before continuing. +
+
+
+ +stopSection(); ?> +make('vendor.installer.layouts.master', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/framework/views/cfe4baef7a01a8e7df0321c9b2cf723c.php b/storage/framework/views/cfe4baef7a01a8e7df0321c9b2cf723c.php new file mode 100644 index 000000000..acee19594 --- /dev/null +++ b/storage/framework/views/cfe4baef7a01a8e7df0321c9b2cf723c.php @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + <?php echo e(config('app.name', 'Laravel')); ?> + + + + + generate(); ?> + environment('local')): ?> + reactRefresh(); ?> + + + + dispatch($page); } if ($__inertiaSsrResponse) { echo $__inertiaSsrResponse->head; } ?> + + + + dispatch($page); } if ($__inertiaSsrResponse) { echo $__inertiaSsrResponse->body; } else { ?>
+ + + + \ No newline at end of file diff --git a/storage/framework/views/e4b12272a79160bd8605b39bfa415d9b.php b/storage/framework/views/e4b12272a79160bd8605b39bfa415d9b.php new file mode 100644 index 000000000..6460a4fdf --- /dev/null +++ b/storage/framework/views/e4b12272a79160bd8605b39bfa415d9b.php @@ -0,0 +1,5 @@ +startSection('title', __('Not Found')); ?> +startSection('code', '404'); ?> +startSection('message', __('Not Found')); ?> + +make('errors::minimal', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?> \ No newline at end of file diff --git a/storage/installed b/storage/installed new file mode 100644 index 000000000..69dc65c81 --- /dev/null +++ b/storage/installed @@ -0,0 +1 @@ +Project successfully INSTALLED on 2026/04/06 01:38:34am diff --git a/storage/logs/laravel.log b/storage/logs/laravel.log new file mode 100755 index 000000000..ef98ab838 --- /dev/null +++ b/storage/logs/laravel.log @@ -0,0 +1,3 @@ +[2026-04-06 01:38:52] local.ERROR: Login error: auth.failed +[2026-04-06 01:39:25] local.ERROR: Login error: auth.failed +[2026-04-06 01:39:27] local.ERROR: Login error: auth.failed diff --git a/storage/uploads/sample/sample-asset.xlsx b/storage/uploads/sample/sample-asset.xlsx new file mode 100755 index 000000000..341c9155d --- /dev/null +++ b/storage/uploads/sample/sample-asset.xlsx @@ -0,0 +1,4 @@ +Name,Asset Type,Serial Number,Asset Code,Purchase Date,Purchase Cost,Status,Condition,Description,Location,Supplier,Warranty Info,Warranty Expiry Date +TP-Link Wireless Router,Network Equipment,TP001,NET002,2025-01-15,8000,assigned,good,Standard Network Equipment for business operations,Main Office,Tech Solutions Pvt Ltd,2 Year Manufacturer Warranty,2027-01-15 +Cisco Catalyst Switch,Network Equipment,CS001,NET001,2025-01-15,75000,assigned,new,Standard Network Equipment for business operations,Main Office,Tech Solutions Pvt Ltd,2 Year Manufacturer Warranty,2027-01-15 +Honda City 2022,Vehicles,HC001,VEH002,2025-01-15,1800000,available,good,Standard Vehicles for business operations,Main Office,Tech Solutions Pvt Ltd,2 Year Manufacturer Warranty,2027-01-15 diff --git a/storage/uploads/sample/sample-attendance-record.xlsx b/storage/uploads/sample/sample-attendance-record.xlsx new file mode 100755 index 000000000..f718add82 --- /dev/null +++ b/storage/uploads/sample/sample-attendance-record.xlsx @@ -0,0 +1,4 @@ +Employee,Date,Shift,Attedance Policy,Clock In,Clock Out +Dr. Rowland Murphy,2026-02-04,Morning Shift,Standard Attendance Policy,09:00,16:00 +James Anderson,2026-02-04,Morning Shift,Morning Shift,09:00,16:00 +Michael Thompson,2026-02-04,Morning Shift,Morning Shift,09:00,16:00 diff --git a/storage/uploads/sample/sample-employee.xlsx b/storage/uploads/sample/sample-employee.xlsx new file mode 100755 index 000000000..24daf4043 --- /dev/null +++ b/storage/uploads/sample/sample-employee.xlsx @@ -0,0 +1,3 @@ +Name,Email,Password,Biometric Employee Id,Phone,Department,Designation,Branch,Base Salary,Date of Joining,Date of Birth,Gender,Shift,Attedance Policy,Employement Type,Employement Status,City,State,Country,Postal Code,Address,Bank Name,Account Number,Bank Identifier Code,Bank Branch +Dr. Rowland Murphy,qwaters@example.com,1234,201,(936) 715-1938,Information Technology,System Administrator,Main Office,10000,2023-10-18,1989-06-03,male,Morning Shift,Standard Attendance Policy,Contract,active,Ludieland,Wyoming,Tokelau,37695,93126 Jerad Fords Suite 187,Kotak Bank,4314411023,CCIH7980373,New Jewell Branch +Antwon Schaefer,mills.demario@example.org,1234,202,515.760.5202,Human Resources,Software Developer,Downtown Branch,10000,2025-06-27,1978-04-26,male,Morning Shift,Standard Attendance Policy,Full-time,active,Jacobiton,Massachusetts,Senegal,98654,42588 Zieme Mission Suite 868,ICICI Bank,3179826104,FWHO0666477,Laurianneside Branch diff --git a/storage/uploads/sample/sample-payroll-run.xlsx b/storage/uploads/sample/sample-payroll-run.xlsx new file mode 100755 index 000000000..bf0298a7a --- /dev/null +++ b/storage/uploads/sample/sample-payroll-run.xlsx @@ -0,0 +1,6 @@ +Title,Payroll Frequency,Pay Period Start,Pay Period End,Pay Date,Notes +April 2025 Payroll,monthly,2025-04-01,2025-04-30,2025-05-05,Monthly payroll for April 2025 +May 2025 Payroll,monthly,2025-05-01,2025-05-31,2025-06-05,Monthly payroll for May 2025 +June 2025 Payroll,monthly,2025-06-01,2025-06-30,2025-07-05,Monthly payroll for June 2025 +July 2025 Payroll,monthly,2025-07-01,2025-07-31,2025-08-05,Monthly payroll for July 2025 +August 2025 Payroll,monthly,2025-08-01,2025-08-31,2025-09-05,Monthly payroll for August 2025 diff --git a/storage/uploads/sample/sample-time-entry.xlsx b/storage/uploads/sample/sample-time-entry.xlsx new file mode 100755 index 000000000..c6bf2e9d4 --- /dev/null +++ b/storage/uploads/sample/sample-time-entry.xlsx @@ -0,0 +1,4 @@ +Employee,Date,Hours,Project,Description +Dr. Rowland Murphy,2026-02-13,2,Website Development,Worked on frontend components and user interface improvements +Antwon Schaefer,2025-08-27,8,Website Development,Worked on frontend components and user interface improvements +Dr. Juana Stehr,2025-08-27,8,Website Development,Worked on frontend components and user interface improvements diff --git a/tests/Feature/Auth/AuthenticationTest.php b/tests/Feature/Auth/AuthenticationTest.php new file mode 100644 index 000000000..7d08f5015 --- /dev/null +++ b/tests/Feature/Auth/AuthenticationTest.php @@ -0,0 +1,41 @@ +get('/login'); + + $response->assertStatus(200); +}); + +test('users can authenticate using the login screen', function () { + $user = User::factory()->create(); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(route('dashboard', absolute: false)); +}); + +test('users can not authenticate with invalid password', function () { + $user = User::factory()->create(); + + $this->post('/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $this->assertGuest(); +}); + +test('users can logout', function () { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/logout'); + + $this->assertGuest(); + $response->assertRedirect('/'); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/EmailVerificationTest.php b/tests/Feature/Auth/EmailVerificationTest.php new file mode 100644 index 000000000..e303f4157 --- /dev/null +++ b/tests/Feature/Auth/EmailVerificationTest.php @@ -0,0 +1,46 @@ +unverified()->create(); + + $response = $this->actingAs($user)->get('/verify-email'); + + $response->assertStatus(200); +}); + +test('email can be verified', function () { + $user = User::factory()->unverified()->create(); + + Event::fake(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1($user->email)] + ); + + $response = $this->actingAs($user)->get($verificationUrl); + + Event::assertDispatched(Verified::class); + expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); + $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); +}); + +test('email is not verified with invalid hash', function () { + $user = User::factory()->unverified()->create(); + + $verificationUrl = URL::temporarySignedRoute( + 'verification.verify', + now()->addMinutes(60), + ['id' => $user->id, 'hash' => sha1('wrong-email')] + ); + + $this->actingAs($user)->get($verificationUrl); + + expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/PasswordConfirmationTest.php b/tests/Feature/Auth/PasswordConfirmationTest.php new file mode 100644 index 000000000..d5894f186 --- /dev/null +++ b/tests/Feature/Auth/PasswordConfirmationTest.php @@ -0,0 +1,32 @@ +create(); + + $response = $this->actingAs($user)->get('/confirm-password'); + + $response->assertStatus(200); +}); + +test('password can be confirmed', function () { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'password', + ]); + + $response->assertRedirect(); + $response->assertSessionHasNoErrors(); +}); + +test('password is not confirmed with invalid password', function () { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/confirm-password', [ + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrors(); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/PasswordResetTest.php b/tests/Feature/Auth/PasswordResetTest.php new file mode 100644 index 000000000..4d243b4c4 --- /dev/null +++ b/tests/Feature/Auth/PasswordResetTest.php @@ -0,0 +1,60 @@ +get('/forgot-password'); + + $response->assertStatus(200); +}); + +test('reset password link can be requested', function () { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class); +}); + +test('reset password screen can be rendered', function () { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) { + $response = $this->get('/reset-password/'.$notification->token); + + $response->assertStatus(200); + + return true; + }); +}); + +test('password can be reset with valid token', function () { + Notification::fake(); + + $user = User::factory()->create(); + + $this->post('/forgot-password', ['email' => $user->email]); + + Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { + $response = $this->post('/reset-password', [ + 'token' => $notification->token, + 'email' => $user->email, + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect(route('login')); + + return true; + }); +}); \ No newline at end of file diff --git a/tests/Feature/Auth/RegistrationTest.php b/tests/Feature/Auth/RegistrationTest.php new file mode 100644 index 000000000..f1d859c7d --- /dev/null +++ b/tests/Feature/Auth/RegistrationTest.php @@ -0,0 +1,19 @@ +get('/register'); + + $response->assertStatus(200); +}); + +test('new users can register', function () { + $response = $this->post('/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password', + 'password_confirmation' => 'password', + ]); + + $this->assertAuthenticated(); + $response->assertRedirect(route('dashboard', absolute: false)); +}); \ No newline at end of file diff --git a/tests/Feature/DashboardTest.php b/tests/Feature/DashboardTest.php new file mode 100644 index 000000000..15804df3a --- /dev/null +++ b/tests/Feature/DashboardTest.php @@ -0,0 +1,13 @@ +get('/dashboard')->assertRedirect('/login'); +}); + +test('authenticated users can visit the dashboard', function () { + $this->actingAs($user = User::factory()->create()); + + $this->get('/dashboard')->assertOk(); +}); \ No newline at end of file diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 000000000..61cd84c32 --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,5 @@ +toBeTrue(); +}); diff --git a/tests/Feature/PlanAccessTest.php b/tests/Feature/PlanAccessTest.php new file mode 100644 index 000000000..4763a846c --- /dev/null +++ b/tests/Feature/PlanAccessTest.php @@ -0,0 +1,121 @@ +create(['type' => 'superadmin']); + + $response = $this->actingAs($superadmin)->get('/dashboard'); + + $response->assertStatus(200); + } + + public function test_company_user_without_plan_redirects_to_plans() + { + $company = User::factory()->create(['type' => 'company', 'plan_id' => null]); + + $response = $this->actingAs($company)->get('/vcard-builder'); + + $response->assertRedirect('/plans'); + } + + public function test_company_user_with_expired_plan_redirects_to_plans() + { + $plan = Plan::factory()->create(); + $company = User::factory()->create([ + 'type' => 'company', + 'plan_id' => $plan->id, + 'plan_expire_date' => now()->subDay(), + 'is_trial' => 0 + ]); + + $response = $this->actingAs($company)->get('/vcard-builder'); + + $response->assertRedirect('/plans'); + } + + public function test_company_user_with_expired_trial_redirects_to_plans() + { + $plan = Plan::factory()->create(); + $company = User::factory()->create([ + 'type' => 'company', + 'plan_id' => $plan->id, + 'is_trial' => 1, + 'trial_expire_date' => now()->subDay() + ]); + + $response = $this->actingAs($company)->get('/vcard-builder'); + + $response->assertRedirect('/plans'); + } + + public function test_company_user_with_active_trial_has_access() + { + $plan = Plan::factory()->create(); + $company = User::factory()->create([ + 'type' => 'company', + 'plan_id' => $plan->id, + 'is_trial' => 1, + 'trial_expire_date' => now()->addDays(5) + ]); + + $response = $this->actingAs($company)->get('/vcard-builder'); + + $response->assertStatus(200); + } + + public function test_non_company_user_denied_access() + { + $user = User::factory()->create(['type' => 'user']); + + $response = $this->actingAs($user)->get('/vcard-builder'); + + $response->assertRedirect('/dashboard'); + } + + public function test_plans_page_accessible_without_plan() + { + $company = User::factory()->create(['type' => 'company', 'plan_id' => null]); + + $response = $this->actingAs($company)->get('/plans'); + + $response->assertStatus(200); + } + + public function test_settings_page_requires_plan() + { + $company = User::factory()->create(['type' => 'company', 'plan_id' => null]); + + $response = $this->actingAs($company)->get('/settings'); + + $response->assertRedirect('/plans'); + } + + public function test_dashboard_requires_plan() + { + $company = User::factory()->create(['type' => 'company', 'plan_id' => null]); + + $response = $this->actingAs($company)->get('/dashboard'); + + $response->assertRedirect('/plans'); + } + + public function test_examples_require_plan() + { + $company = User::factory()->create(['type' => 'company', 'plan_id' => null]); + + $response = $this->actingAs($company)->get('/examples/media-library-demo'); + + $response->assertRedirect('/plans'); + } +} \ No newline at end of file diff --git a/tests/Feature/RazorpaySettingsTest.php b/tests/Feature/RazorpaySettingsTest.php new file mode 100644 index 000000000..da972736c --- /dev/null +++ b/tests/Feature/RazorpaySettingsTest.php @@ -0,0 +1,45 @@ +create([ + 'type' => 'superadmin' + ]); + + // Login as the user + $this->actingAs($user); + + // Test data + $data = [ + 'is_razorpay_enabled' => true, + 'razorpay_key' => 'rzp_test_123456789', + 'razorpay_secret' => 'test_secret_key', + ]; + + // Submit the form + $response = $this->post(route('payment.settings'), $data); + + // Assert the response + $response->assertRedirect(); + $response->assertSessionHas('success'); + + // Check if the settings were saved correctly + $settings = PaymentSetting::where('user_id', $user->id)->get(); + + $this->assertTrue($settings->where('key', 'is_razorpay_enabled')->first()->value == '1'); + $this->assertEquals('rzp_test_123456789', $settings->where('key', 'razorpay_key')->first()->value); + $this->assertEquals('test_secret_key', $settings->where('key', 'razorpay_secret')->first()->value); + } +} \ No newline at end of file diff --git a/tests/Feature/Settings/PasswordUpdateTest.php b/tests/Feature/Settings/PasswordUpdateTest.php new file mode 100644 index 000000000..f4566fc89 --- /dev/null +++ b/tests/Feature/Settings/PasswordUpdateTest.php @@ -0,0 +1,40 @@ +create(); + + $response = $this + ->actingAs($user) + ->from('/settings/password') + ->put('/settings/password', [ + 'current_password' => 'password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/settings/password'); + + expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue(); +}); + +test('correct password must be provided to update password', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/settings/password') + ->put('/settings/password', [ + 'current_password' => 'wrong-password', + 'password' => 'new-password', + 'password_confirmation' => 'new-password', + ]); + + $response + ->assertSessionHasErrors('current_password') + ->assertRedirect('/settings/password'); +}); \ No newline at end of file diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php new file mode 100644 index 000000000..cdf17c227 --- /dev/null +++ b/tests/Feature/Settings/ProfileUpdateTest.php @@ -0,0 +1,85 @@ +create(); + + $response = $this + ->actingAs($user) + ->get('/settings/profile'); + + $response->assertOk(); +}); + +test('profile information can be updated', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/settings/profile', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/settings/profile'); + + $user->refresh(); + + expect($user->name)->toBe('Test User'); + expect($user->email)->toBe('test@example.com'); + expect($user->email_verified_at)->toBeNull(); +}); + +test('email verification status is unchanged when the email address is unchanged', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->patch('/settings/profile', [ + 'name' => 'Test User', + 'email' => $user->email, + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/settings/profile'); + + expect($user->refresh()->email_verified_at)->not->toBeNull(); +}); + +test('user can delete their account', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->delete('/settings/profile', [ + 'password' => 'password', + ]); + + $response + ->assertSessionHasNoErrors() + ->assertRedirect('/'); + + $this->assertGuest(); + expect($user->fresh())->toBeNull(); +}); + +test('correct password must be provided to delete account', function () { + $user = User::factory()->create(); + + $response = $this + ->actingAs($user) + ->from('/settings/profile') + ->delete('/settings/profile', [ + 'password' => 'wrong-password', + ]); + + $response + ->assertSessionHasErrors('password') + ->assertRedirect('/settings/profile'); + + expect($user->fresh())->not->toBeNull(); +}); \ No newline at end of file diff --git a/tests/Feature/StripePaymentTest.php b/tests/Feature/StripePaymentTest.php new file mode 100644 index 000000000..1d227fdb0 --- /dev/null +++ b/tests/Feature/StripePaymentTest.php @@ -0,0 +1,86 @@ +create(['type' => 'superadmin']); + + // Create payment settings + PaymentSetting::create([ + 'user_id' => $superAdmin->id, + 'key' => 'stripe_key', + 'value' => 'pk_test_123456789' + ]); + + PaymentSetting::create([ + 'user_id' => $superAdmin->id, + 'key' => 'stripe_secret', + 'value' => 'sk_test_123456789' + ]); + + PaymentSetting::create([ + 'user_id' => $superAdmin->id, + 'key' => 'is_stripe_enabled', + 'value' => '1' + ]); + + // Test payment methods API + $response = $this->get(route('payment.methods')); + + $response->assertStatus(200); + $data = $response->json(); + + $this->assertEquals('pk_test_123456789', $data['stripe_key']); + $this->assertEquals('sk_test_123456789', $data['stripe_secret']); + $this->assertTrue($data['is_stripe_enabled']); + } + + public function test_stripe_payment_validation() + { + // Create user and plan + $user = User::factory()->create(); + $plan = Plan::factory()->create(['price' => 10.00]); + + $this->actingAs($user); + + // Test without payment method ID + $response = $this->post(route('stripe.payment'), [ + 'plan_id' => $plan->id, + 'coupon_code' => '', + 'cardholder_name' => 'Test User', + ]); + + $response->assertSessionHasErrors(['payment_method_id']); + } + + public function test_stripe_payment_missing_configuration() + { + // Create user and plan + $user = User::factory()->create(); + $plan = Plan::factory()->create(['price' => 10.00]); + + $this->actingAs($user); + + // Test without Stripe configuration + $response = $this->post(route('stripe.payment'), [ + 'payment_method_id' => 'pm_test_123', + 'plan_id' => $plan->id, + 'coupon_code' => '', + 'cardholder_name' => 'Test User', + ]); + + $response->assertSessionHasErrors(['error']); + } +} \ No newline at end of file diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 000000000..40d096b52 --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,47 @@ +extend(Tests\TestCase::class) + ->use(Illuminate\Foundation\Testing\RefreshDatabase::class) + ->in('Feature'); + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +expect()->extend('toBeOne', function () { + return $this->toBe(1); +}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +function something() +{ + // .. +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 000000000..fe1ffc2ff --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,10 @@ +toBeTrue(); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..f905a3eef --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,122 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + + /* Language and Environment */ + "target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + + /* Modules */ + "module": "ESNext" /* Specify what module code is generated. */, + // "rootDir": "./", /* Specify the root folder within your source files. */ + "moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + + /* JavaScript Support */ + "allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */, + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + "noEmit": true /* Disable emitting files from a compilation. */, + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + // "outDir": "./", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + + /* Interop Constraints */ + "isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */, + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + /* Type Checking */ + "strict": true /* Enable all strict type-checking options. */, + "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */, + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */, + "baseUrl": ".", + "paths": { + "@/*": ["./resources/js/*"], + "ziggy-js": ["./vendor/tightenco/ziggy"] + }, + "jsx": "react-jsx" + }, + "include": [ + "resources/js/**/*.ts", + "resources/js/**/*.d.ts", + "resources/js/**/*.tsx", + ] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 000000000..d60c63f3a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,69 @@ +import tailwindcss from '@tailwindcss/vite'; +import laravel from 'laravel-vite-plugin'; +import { resolve } from 'node:path'; +import { defineConfig } from 'vite'; + +function stripUseClientDirective(): import('vite').Plugin { + return { + name: 'strip-use-client-directive', + enforce: 'pre' as const, + transform(code, id) { + if ( + id.endsWith('.js') || + id.endsWith('.ts') || + id.endsWith('.tsx') || + id.endsWith('.mjs') + ) { + if (code.includes('"use client"') || code.includes("'use client'")) { + return code.replace(/['"]use client['"];?\s*/g, ''); + } + } + }, + }; +} + +export default defineConfig({ + base: './', + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/css/dark-mode.css', 'resources/js/app.tsx'], + ssr: 'resources/js/ssr.tsx', + refresh: true, + }), + stripUseClientDirective(), + tailwindcss(), + ], + server: { + host: 'localhost', + headers: { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS', + 'Access-Control-Allow-Headers': '*', + }, + watch: { + ignored: ['**/vendor/**', '**/node_modules/**'] + } + }, + + esbuild: { + jsx: 'automatic', + jsxImportSource: 'react', + }, + resolve: { + alias: { + 'ziggy-js': resolve(__dirname, 'vendor/tightenco/ziggy'), + }, + }, + build: { + rollupOptions: { + output: { + manualChunks: { + vendor: ['react', 'react-dom'], + ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'], + utils: ['date-fns', 'clsx'] + } + }, + }, + assetsDir: 'assets', + } +}); \ No newline at end of file