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 = '';
+
+ 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